Measuring frames/VSYNC

The place for codemasters or beginners to talk about programming any language for the Spectrum.
Post Reply
User avatar
Prototron
Drutt
Posts: 11
Joined: Thu Mar 07, 2024 2:10 pm
Location: Glasgow, Scotland

Measuring frames/VSYNC

Post by Prototron »

Hi Folks!

So I've been experimenting with timing and screen syncing with a soft double buffer.

The code below seems like the most common way, but I'm not quite sure it's efficient? The HALT stops any additional screen tearing but it could just be unnecessary waiting (it all currently sits at the top of my main loop).

As a Z80 noob I'm wondering if there's a better way to measure frames? On The Amiga I just waited for the last line of the display using VPOSR and then I was in VBLANK right after, but I don't think there's a beam register on the Speccy, so I assume the code below is doing roughly the same thing?

Thanks!

Code: Select all


CURRENT_TIMER 	= 23672 	; $5C78

VWAIT:
	ei
	;---------------------------
	ld 		hl,TIMERPREV		; Previous time setting
   	ld	 	a,(CURRENT_TIMER)	; Current timer setting
   	sub 		(hl)			; Difference between the two
    	cp 		1			; # of frames elapsed
	jr 		nc,VWAITEND		; Exit
	;---------------------------		
	;  - EXTRA STUFF, MAYBE? -
	;---------------------------
    	jr 		VWAIT			; Repeat
	;---------------------------
VWAITEND:
	ld 		a,(CURRENT_TIMER)	; Current timer
    	ld 		(hl),a			; Store this setting
	;---------------------------	
	HALT
	;---------------------------
	call 		FLIPBUFFER		; LDI Screen Copy from $C000 to $4000
	;---------------------------
	di
"For the money, for the glory, and for the fun! Mostly for the money."
~ Bo "Bandit" Darville
User avatar
Stefan
Manic Miner
Posts: 809
Joined: Mon Nov 13, 2017 9:51 pm
Location: Belgium
Contact:

Re: Measuring frames/VSYNC

Post by Stefan »

If you do not mind your code not working on all models, search for “floating bus”.
User avatar
PROSM
Manic Miner
Posts: 476
Joined: Fri Nov 17, 2017 7:18 pm
Location: Sunderland, England
Contact:

Re: Measuring frames/VSYNC

Post by PROSM »

The common way to synchronise the Z80 with the raster beam is just to do a HALT - this stops the Z80 until the next interrupt is received, at which point it runs the interrupt service routine and then continues with the main program. The ULA (the Spectrum's central chip which handles video, sound, keyboard and tape I/O) generates an interrupt pulse at the start of the vertical sync period (i.e. just before the top border).

The BASIC ROM sets up the Z80 to run in interrupt mode 1, meaning that on every interrupt from the ULA, the Z80 will execute the routine at $0038. This routine increments your CURRENT_TIMER variable (which is called FRAMES in the Spectrum manual), and checks the keyboard.

Therefore, your waiting loop at the top is currently redundant, since when you do the HALT below it, the CURRENT_TIMER will get incremented, so your waiting loop is guaranteed to exit on the first iteration anyway. You would probably be better off using either the CURRENT_TIMER loop or the HALT, The difference is that if your code takes more than a frame to finish, with the timer loop you can just continue on without having to wait for the next vertical sync, as would happen with HALT. Whether this is an advantage or not depends on your use case.

There is an alternative, called interrupt mode 2, which makes the Z80 look up the address of the interrupt routine from a table you provide. This way, you can have your own interrupt routine and not have to use the one in ROM - this affords you some liberties, like being able to change the value of IY.
All software to-date
Working on something, as always.
AndyC
Dynamite Dan
Posts: 1409
Joined: Mon Nov 13, 2017 5:12 am

Re: Measuring frames/VSYNC

Post by AndyC »

One thing to bear in mind is that, unlike the Amiga, you're don't really have to worry about someone having a faster CPU. So a lot of the time HALT on it's own will suffice.

If you're execution time per frame is very variable, I'd personally probably use my own interrupt routine rather than the ROM one (which isn't terribly efficient) and just use that to increment a one byte counter indicating how many frames have passed.
User avatar
Prototron
Drutt
Posts: 11
Joined: Thu Mar 07, 2024 2:10 pm
Location: Glasgow, Scotland

Re: Measuring frames/VSYNC

Post by Prototron »

Thanks for the replies folks!
AndyC wrote: Sat Mar 09, 2024 10:48 am One thing to bear in mind is that, unlike the Amiga, you're don't really have to worry about someone having a faster CPU. So a lot of the time HALT on it's own will suffice.
Yeah that was a problem. I coded for the 68000 only, but folk were using all sorts of turbo-accelerated A1200s and complaining when things ran too fast.

I think I'll stick with HALT for now, but I'll keep that timer code just in case. At the moment I just want something to steady what's happening in the main loop without speeding up or slowing down when I add/remove elements with my game objects routine.

The IM2 handler sounds like something I'll end up with, so are there any code examples of that to have a look at?

Thanks.
"For the money, for the glory, and for the fun! Mostly for the money."
~ Bo "Bandit" Darville
XoRRoX
Manic Miner
Posts: 233
Joined: Wed Jul 11, 2018 6:34 am

Re: Measuring frames/VSYNC

Post by XoRRoX »

Here you can find a nice introduction to using IM2: http://www.breakintoprogram.co.uk/hardw ... interrupts
User avatar
ketmar
Manic Miner
Posts: 713
Joined: Tue Jun 16, 2020 5:25 pm
Location: Ukraine

Re: Measuring frames/VSYNC

Post by ketmar »

or simply don't bother at all. add 20 moving sprites, a lot of tile animations — and nobody will complain about tearing, because "wow! with all those updates it's a miracle that it is not 1 FPS!" ;-)
User avatar
ParadigmShifter
Manic Miner
Posts: 671
Joined: Sat Sep 09, 2023 4:55 am

Re: Measuring frames/VSYNC

Post by ParadigmShifter »

In Turbo Manic Miner I used a HALT to wait for a VBlank once I'd done all my processing and did the buffer copy in the ISR if I was ready to draw. I had a dirty buffer though and only copied cells which changed (so drawing sprites was more complicated).

So you can do in your ISR (requires IM2)

Code: Select all

; interrupt service routine
isr:	push af
	push bc
	push de
	push hl	; save registers

	IFDEF TIMING
	ld a, 0
	out (#fe), a
	ENDIF

isr_static:
	ld a, 0 ; this is deliberate, it is not supposed to be xor a, operand is modified outside the ISR
	or a

	jp z, .eoi	; if we are not ready to update screen, just play tune and update framecount
	; we are ready to update screen. clear the flag
	xor a
	ld (isr_static+1), a	; clear update flag
	
	; do your copy here
	
.eoi

        ; update frame counter here
        
        ; I also played the tune here
	
	pop hl
	pop de
	pop bc
	pop af
	; I turned interrupts back on here
	ei
	ret
And when I have finished all my drawing to the buffer code outside the isr I do this to tell the ISR to copy next time it goes off... it uses self modifying code to replace the 0 in this instruction

isr_static:
ld a, 0 ; this will be changed to ld a, 1 by the following code

Code: Select all

	; we are ready to update screen at next interrupt

	; disable interrupts while we set finished flag
	ld a, 1

	di
	ld (isr_static+1), a
	; enable interrupts again
	ei
I don't think the di and ei is necessary there since only 1 instruction is performed which modifies something the ISR might be looking at so an interrupt cannot go off while it is doing that. I used ei and di to be safe though since it is what I was used to doing on other platforms.

But I don't do that these days I draw direct to the screen. I still have a HALT but I have a clear list and a draw list I build during the frame and as soon as the HALT finishes I am ready to draw everything. That requires beating the raster when drawing though so if you are drawing a lot a double buffer is a lot easier to do (I know I won't be able to spare the memory for a framebuffer though since I am rewriting my code to be used in SJOE++ which has a 16K dictionary of 5638 4 letter words so I won't have memory to fit into 48K if I use a double buffer).

Copying with LDI is slower than using the stack to copy though which you should probably do as well if you go down this route.

You can also do the copy immediately after a HALT if you want though, I wouldn't do my copy in the ISR if I was rewriting it :) So that's similar to what you are doing really. EDIT: And you don't need to use IM2 then either.

You can force it to run at a capped frame rate by doing 2 HALTs instead of 1 if only 1 frame has passed and so on. Turbo Manic Miner was capped at 25fps max frame rate.
Last edited by ParadigmShifter on Sat Mar 09, 2024 5:46 pm, edited 1 time in total.
User avatar
ketmar
Manic Miner
Posts: 713
Joined: Tue Jun 16, 2020 5:25 pm
Location: Ukraine

Re: Measuring frames/VSYNC

Post by ketmar »

or you can let the ray go. ;-) if you'll start drawing after the ray is passed, you'll have the whole frame to draw everything. some games are using this tech, finishing everything when the ray is already drawing the start of the frame (which was already updated). the only problem is to make sure that the ray is in the scr$ area, so you need to waste ~14k tstates. it is possible to use them for some game logic, but then you'll have to keep logic code timing more or less stable. or use floating bus to estimate ray position.

on the other side, if you'll start rendering right in the interrupt handler, you'll have 14k contention-less tstates…
User avatar
ParadigmShifter
Manic Miner
Posts: 671
Joined: Sat Sep 09, 2023 4:55 am

Re: Measuring frames/VSYNC

Post by ParadigmShifter »

What I am doing currently, direct to screen with a clear list and a draw list

Code: Select all

mainloop:
	IF TIMING
	ld a, 7
	out (#fe), a
	ENDIF

	REPT FRAMEDELAY
	halt
	ENDR

        ; all my draw routines modify SP to do fast 16 bit reads/writes so I need to stash the SP and restore it after drawing is done
        ; it means I can't use any CALLs or PUSH/POP like you usually would in my drawing	
	di
	ld (savesp+1), sp
	IF TIMING
	xor a
	out (#FE), a
	ENDIF

	ld hl, (clrListPtr)

nextClear:
	ld a, (hl)
	or a
	jr z, doneClear
	inc hl
	ld (clrListPtr), hl ; first byte is pointer to a clear routine, which is aligned at an address multiple of 256 so I only need to store 1 byte
	ex de, hl ; routine I jump to can get back the pointer to their data in DE
	ld l, 0
	ld h, a
	jp (hl)

doneClear:

	ld hl, clrList
	ld (clrListPtr), hl

	IF TIMING
	ld a, 1
	out (#fe), a
	ENDIF

	ld hl, (drawListPtr)

nextSprite:
	ld a, (hl)
	or a
	jr z, doneDraw
	inc hl
	ld (drawListPtr), hl ; first byte is pointer to a draw routine, which is aligned at an address multiple of 256 so I only need to store 1 byte
	ex de, hl  ; routine I jump to can get back the pointer to their data in DE
	ld l, 0
	ld h, a
	jp (hl)

doneDraw:
	ld hl, drawList
	ld (drawListPtr), hl

savesp:
	ld sp, 0
	ei

	ld hl, (phase) ; this is a pointer to the routine that performs game logic, input etc.
	jp (hl)
Each command sits on a 256 byte boundary so I only need to store 1 byte to jump to it (so #93 code is at address #9300 and so on).

The routine that is jumped to has DE pointing at the rest of the data for the command which they unpack and then do stuff with, e.g.

Code: Select all

	ALIGN 256
clear16x8:
	; attribs
	;ld hl, (clrListPtr)
	ex de, hl
	ld e, (hl)
	inc hl		; 6T
	ld d, (hl)	; 7T
	ld a, d		; 4T
	xor ATTRIBS_MAGIC
	ld l, e
	ld h, a
	dec l ; attrib address, should be safe to not cross a boundary
	dec l
	ld sp, hl
	pop bc
	ex de, hl
	ld sp, hl
	push bc
	ld hl, (clrListPtr)
	inc hl
	inc hl
	;ld (clrListPtr), hl
	jp nextClear
parameters are the attribute address to clear (they do this by copying from a background attribs buffer instead of erasing any pixels, background attribs are at address #5800 which I can flip between normal attribs and background attribs via XORing the high byte with ATTRIBS_MAGIC (which has to be 1024 byte aligned for that to work).

Code: Select all

ATTRIBS_ADDR	EQU	#5800
ATTRIBS_MAGIC	EQU #58^(attribs/256)

; background attribs
	ALIGN 1024
attribs: BLOCK 768


Once the routine finishes they make sure HL is pointing to the next command in the list (it's terminated by a single byte of 0) and jp to the loop again.


So in my mainloop I do:

HALT
process clear and draw lists

jump to the game loop code for what phase we are currently in (playing, physics, scoring) which does all the logic and adds draw and clear commands to the lists for the next frame. So all the drawing for the previous frame is done before we even start deciding what to draw next frame.

repeat

Running screen... all drawing is done by the time the dark blue border is displayed. White border = waiting for VBlank. (EDIT: Except for drawing the collision minimap on right hand side of the screen... that's being drawn when the border is cyan. That's only used to debug though won't be in the game once everything works)

Image
User avatar
Prototron
Drutt
Posts: 11
Joined: Thu Mar 07, 2024 2:10 pm
Location: Glasgow, Scotland

Re: Measuring frames/VSYNC

Post by Prototron »

Apologies for getting back a little late but I was away at the weekend.

Thanks for all the great replies and examples! Plenty to chew on here. :)
"For the money, for the glory, and for the fun! Mostly for the money."
~ Bo "Bandit" Darville
Post Reply