[MSX-C] Q&A official thread

Page 11/68
4 | 5 | 6 | 7 | 8 | 9 | 10 | | 12 | 13 | 14 | 15 | 16

Par Sylvester

Hero (593)

Portrait de Sylvester

07-09-2015, 21:12

@JaviLM, awesome example again!! Thanks, an other part for my useless dos2 utility solved Smile

Par AxelStone

Prophet (3199)

Portrait de AxelStone

07-09-2015, 22:49

Really a good work!!. Finally it seems that the function to use were callxx, allways at address 0x0005 (BDOS) as I can see, and the operation is contained in C register. It was not trivial to discover it, thanks for the source code Wink

Par anonymous

incognito ergo sum (116)

Portrait de anonymous

08-09-2015, 03:12

AxelStone wrote:

Definitively JIFFY solution is the best. Since it works as TIME in BASIC, you can handle the desired fps of your game simply looping this variable. For example:

vertscr() {
	TINY i;
	for(i=0;i<212;i++){
		JIFFY=0;
		wrtvdp(VREG23,i);
		/* Wait for interrupt */
		while (JIFFY<2) {}
	}
}

We waits for 2 cycles of VDP, this is, we are setting the game at 30fps instead of 60fps.

I think we should find a better solution. I'm not happy about this one because it keeps the CPU busy in a wait loop instead of doing something useful during that time.

Par anonymous

incognito ergo sum (116)

Portrait de anonymous

08-09-2015, 04:50

AxelStone wrote:

Really a good work!!. Finally it seems that the function to use were callxx, allways at address 0x0005 (BDOS) as I can see, and the operation is contained in C register. It was not trivial to discover it, thanks for the source code Wink

Correct. Which function you use depends on the parameters that you need to pass to the function you're calling:

When calling MSX-DOS(2) functions that require no arguments, or that take arguments only in DE and/or HL:

bdos(function[, de, hl]): Use when the MSX-DOS(2) call returns an 8-bit value in A (or when it doesn't return a value at all)
bdosh(function[, de, hl]): Use when the MSX-DOS(2) call returns a 16-bit value in HL

bdos()/bdosh() can be called with one, two, or three arguments: bdos(function), bdos(function, de), bdos(function, de, hl).

When calling routines at arbitrary addresses that take arguments in A, HL, BC, DE:

calla(address, a, hl, bc, de): Use when the routine at address returns an 8-bit value in A.
call(address, a, hl, bc, de): Use when the routine at address returns a 16-bit value in HL.

In this case all the arguments are required, so if the routine you're calling doesn't use any of them, just pass a dummy value.

When calling routines at arbitrary addresses that take arguments in any register:

Create a struct of type XREG, fill the values of the registers there, and call callxx() with the address of the routine and the address of the XREG struct:

callxx(function, pointer to XREG struct)

Also, if you want to be really fancy:

Calling assembler routines INLINE from MSX-C:

If you understand how MSX-C passes parameters to assembler routines (see this article), then you can just put all the parameters inline in your C program:

(*(VOID (*)())(0x0005))((TINY)0, 'x', (TINY)0x02);

The code above means call the function located at address 0005h, that returns a VOID, with parameters 0, 'x' and 002h. 0 is passed in register A (this is a dummy argument), 'x' is passed in register E, and 02h (_CONOUT) is passed in register C. This is the same as doing bdos(_CONOUT, (unsigned)'x').

I still can't see in what cases this last format will be useful, but maybe at some point it will be useful to put it in a macro or something.

Par AxelStone

Prophet (3199)

Portrait de AxelStone

08-09-2015, 10:08

JaviLM wrote:
AxelStone wrote:

Definitively JIFFY solution is the best. Since it works as TIME in BASIC, you can handle the desired fps of your game simply looping this variable. For example:

vertscr() {
	TINY i;
	for(i=0;i<212;i++){
		JIFFY=0;
		wrtvdp(VREG23,i);
		/* Wait for interrupt */
		while (JIFFY<2) {}
	}
}

We waits for 2 cycles of VDP, this is, we are setting the game at 30fps instead of 60fps.

I think we should find a better solution. I'm not happy about this one because it keeps the CPU busy in a wait loop instead of doing something useful during that time.

Hi Javi, this is the standard solution (and a good solution, believe me) to get constant frame rate in your game, I'll explain. Usually the VDP is the bottleneck in most games, since CPU has not so much work as the VDP. This is, the CPU must wait for next cycle to the VDP. We must consider that JIFFY check is needed in order to get vsync. Depending of the VDP load in your game, it could be a good idea to limit max frame rate.

If you don't control your game speed, you get games like Valis II (and much more) where there is a big difference in the speed between runs alone and runs with scroll and enemies. If you see that the VDP needs more than 1 cycle to complete its works, then we should wait for it. The wait line should be last line in your main loop. During all loop, CPU and VDP are working in parallel, at the end of the loop we check if VDP has finished its work. If you see that your game almost never runs at 60fps, it's a good idea limit it at 30fps in order to get a more stable frame rate and not the strange effect fast-slow-fast of games like Valis II (this technique is often used in modern consoles even). If the VDP can't finish its work in 1 cycle, we give 2 cycles.

Another solution that was discussed in this forum is the posibility of implement a skip frame method. In BASIC you can do this using ON INTERVAL GOSUB. The update frame logic is allways running and it's launched every cycle, whenever VDP has finished or not its work. However it could be a bad solution depending your game. If we take again Valis II and if it had this technique implemented, the gameplay would be awful since the game should have a terrible frame skip.

At least, there is no a universal technique to get your game runs at the best. Depending of your game and what you are looking for, you can decide what is the best way.

Par Grauw

Ascended (10821)

Portrait de Grauw

08-09-2015, 17:47

JaviLM wrote:

I think we should find a better solution. I'm not happy about this one because it keeps the CPU busy in a wait loop instead of doing something useful during that time.

In a VSYNCed game, you’ll need to anyway, because you can not exceed the 16.7 ms budget per frame (at 60 fps). The game engine must run synchronously to the VDP, so it must wait. If your game engine does not reliably stay under the budget, even if you would be able to snoop off some left-over time from the last frame, it would be difficult to guarantee a smooth scroll and gameplay.

Of course there are scenarios where one would want to independently process (part of) the game logic asynchronously on a separate thread, e.g. in the case of a chess game or perhaps even an RTS or something, where processing a full step of the game state may take much more than 16.7 ms… In such a case you could run the visual part of the engine synced off the interrupt and use the remaining time in the main loop outside the interrupt to run the game logic. However this introduces a lot of complexity to the architecture, visuals and logic need to be strictly separated and the logic state must allow multithreaded access (very difficult topic), etc.

As this does not apply to a typical game, a fully synchronous engine is preferable, to keep the game engine design simple.

Par AxelStone

Prophet (3199)

Portrait de AxelStone

08-09-2015, 21:33

JaviLM wrote:

Correct. Which function you use depends on the parameters that you need to pass to the function you're calling:

When calling MSX-DOS(2) functions that require no arguments, or that take arguments only in DE and/or HL:

bdos(function[, de, hl]): Use when the MSX-DOS(2) call returns an 8-bit value in A (or when it doesn't return a value at all)
bdosh(function[, de, hl]): Use when the MSX-DOS(2) call returns a 16-bit value in HL

bdos()/bdosh() can be called with one, two, or three arguments: bdos(function), bdos(function, de), bdos(function, de, hl).

When calling routines at arbitrary addresses that take arguments in A, HL, BC, DE:

calla(address, a, hl, bc, de): Use when the routine at address returns an 8-bit value in A.
call(address, a, hl, bc, de): Use when the routine at address returns a 16-bit value in HL.

In this case all the arguments are required, so if the routine you're calling doesn't use any of them, just pass a dummy value.

When calling routines at arbitrary addresses that take arguments in any register:

Create a struct of type XREG, fill the values of the registers there, and call callxx() with the address of the routine and the address of the XREG struct:

callxx(function, pointer to XREG struct)

Also, if you want to be really fancy:

Calling assembler routines INLINE from MSX-C:

If you understand how MSX-C passes parameters to assembler routines (see this article), then you can just put all the parameters inline in your C program:

So finally all functions were used. I put it into my manual ;) .

Grauw wrote:

In a VSYNCed game, you’ll need to anyway, because you can not exceed the 16.7 ms budget per frame (at 60 fps). The game engine must run synchronously to the VDP, so it must wait. If your game engine does not reliably stay under the budget, even if you would be able to snoop off some left-over time from the last frame, it would be difficult to guarantee a smooth scroll and gameplay.

Of course there are scenarios where one would want to independently process (part of) the game logic asynchronously on a separate thread, e.g. in the case of a chess game or perhaps even an RTS or something, where processing a full step of the game state may take much more than 16.7 ms… In such a case you could run the visual part of the engine synced off the interrupt and use the remaining time in the main loop outside the interrupt to run the game logic. However this introduces a lot of complexity to the architecture, visuals and logic need to be strictly separated and the logic state must allow multithreaded access (very difficult topic), etc.

As this does not apply to a typical game, a fully synchronous engine is preferable, to keep the game engine design simple.

As yo can see @JaviLM, Grauw has explained it much better than me ;)

Par DarkSchneider

Paragon (1030)

Portrait de DarkSchneider

09-09-2015, 10:14

I think @JaviLM means more an "elegant" solution rather than sync or performance issues. This is, CPU doing NOPs rather than read-RAM and the interrupt doing the work.

The solution could be a simple HALT. In ASM are 3 instructions, a simple function without parameters:

EI
HALT
RET

EI to be sure the HALT is working.

Then, to use it in that way:

loop {
jiffies = 0;
... // game logic
halt(); // optional, see below
while(jiffies < FRAMERATE) {
   halt(); }
... // fast drawing instructions, like switch page, copy SAT to VRAM, etc.
}

Set FRAMERATE to 1, 2, etc. for sync the game to 60, 30, 20 fps...
The "optional" halt() is because we have 2 cases:
- If set, we always have a synchronized drawing, but at cost of possible performance loss.
- If missing, we draw the next frame "as is" (probably incomplete) if we exceed the desired FRAMERATE in the game logic. See that in this case we do not run halt() because we don't enter inside the while(), and execute the drawing instructions at that point, not coinciding with the interrupt exactly.

Par Grauw

Ascended (10821)

Portrait de Grauw

09-09-2015, 13:19

Why do you want to add the HALT to the loop? On a Z80 there is no advantage to using HALT compared to a busy wait loop. On modern CPUs this makes a difference, but on systems like MSX, any power savings are imaginary.

So, putting the HALT there is just adding extra unnecessary complication to the wait loop. Also it creates confusion about what it is actually synchronizing to, e.g. in your example the second halt is inside a jiffy loop so although unnecessary at least it is properly checking the VBLANK. However the first halt could trigger on any interrupt, so it has no synchronisation value and your desired behaviour would break whenever the game code uses another type of interrupt (e.g. line ints), or if the user has any interrupt-generating device active in his set-up.

It’s best to just forget about the halts, in fact stay away from them because it can make things look like they are working when they are really not, and stick to checking JIFFY only.

Whether the loop exits immediately when the game code exceeds the frame budget or waits until the next blank (slowing down) depends on the place where you do the jiffies = 0. If you do it before the game code it will exit immediately, if you do it before the wait loop it will wait.

Par anonymous

incognito ergo sum (116)

Portrait de anonymous

09-09-2015, 13:31

Grauw wrote:

It’s best to just forget about the halts, in fact stay away from them because it can make things look like they are working when they are really not, and stick to checking JIFFY only.

I disagree. Yes, working with JIFFY is fine in most cases, but there are cases when you want to use HALTs. As you said, line interrupts is one of these cases.

Anything that works in overscan mode will depend on a couple of line interrupts, so these programs will need a nice HALT loop. As an example, this is the main loop of the scroll routine I wrote (yes, it runs in overscan), and it uses only line interrupts to control precisely when things happen:

; *************************
; ** PROGRAM STARTS HERE **
; *************************

start:		di
		ld	(oldstack),sp

		call	graphic3
		call	freezesys

		call	init

		; color ,,0
		xor	a
		ld	(vdpregs0+7),a
		vdp	7

		; clear pending interrupts
		rdvdp	1

		ei

loop:		halt
		jr	loop

So, while checking JIFFY will be fine in most cases, I can't agree with you regarding staying away from halts. ;-)

Page 11/68
4 | 5 | 6 | 7 | 8 | 9 | 10 | | 12 | 13 | 14 | 15 | 16