Page 1 of 1

How To Remove Sprite Flicker

Posted: Sat Jan 06, 2024 12:51 am
by Manannan
Hello All,

I am hoping you guys can give me some advice on fixing flicking sprites for my AGI interpreter.

You can see from this video that sprites that don't move flicker occasionally.
https://www.youtube.com/watch?v=rIVZNwVOI7c

The code to make the sprites work is really complicated, so I can't display it all.

However here is basically how it works:

At the end of the game loop once per cycle:
1. Disable IRQ
For every sprite, which is visible:
a. If the sprite animation sequence isn't in memory allocate some unused VRAM with a manager, and blit the animation sequence into VRAM. I emphasise unused, as the segment of VRAM is unused it should be able to be updated outside of the IRQ handler without causing artifacts.
b. Fill a sequential buffer with the following things. This buffer will be read by an IRQ handler to create sprite attributes:
I. Vera Address Sprite Data Address Middle (Low will always be 0)
II. 1 Vera Address Sprite Data High
III. 2 x position low
IV. 3 x position high
V. 4 y position (high will always be zero)
VI. 5 Sprite Attr Size/Palette Offset

c. Increment bESpritesUpdatedBufferPointer once for every byte. This the pointer a position in the buffer. This pointer is useful for marking where to put the next byte
d. Add two zeros on the end of the buffer without incrementing the pointer, as an end of list marker. If there is another sprite these zeros will be overwritten.
4. Renable IRQ. There should now be 6 bytes for every visible sprite in the buffer plus two zeros on the end

5. In the IRQ Handler execute the bEHandleSpriteUpdates method:

Code: Select all


;Macro Definitions
.macro CLEAR_SPRITE_ATTRS NO_TO_CLEAR
.local @outerLoop
.local @outerLoopCheck
.local @innerLoop
.local @innerLoopCheck
.local @end

ldy NO_TO_CLEAR
beq @end

SET_VERA_START_SPRITE_ATTRS #$0, #$4 ;Set VERA channel 0 to the start of the sprites attributes table with a stride of 4
SET_VERA_START_SPRITE_ATTRS_HIGH #$1, #$4 ;Set VERA channel 1 to the start of the sprites attributes table + 1 with a stride of 4

@outerLoop:
stz VERA_data0
stz VERA_data1

@outerLoopCheck:
dey
bne @outerLoop

@end:
.endmacro

.macro GET_NEXT_FROM_SPRITE_UPDATE_BUFFER
.local @continue

lda _bESpritesUpdatedBuffer,y
iny
.endmacro

;Define Insertion Order:
;0 Vera Address Sprite Data Middle (Low will always be 0) (If both the first two bytes are zero that indicates the end of the buffer)
;1 Vera Address Sprite Data High
;2 x low
;3 x high
;4 y
;5 Sprite Attr Size/Palette Offset




ZP_LOW_BYTE = ZP_TMP_5


bEHandleSpriteUpdates: ; --------------Executed method is here

lda _bESpritesUpdatedBufferPointer ; If there is nothing in the buffer do nothing, leave the sprites as is
cmp #< _bESpritesUpdatedBuffer
bne @start
lda _bESpritesUpdatedBufferPointer + 1
cmp #> _bESpritesUpdatedBuffer
bne @start


jmp @end

@start:
CLEAR_SPRITE_ATTRS _maxViewTable

SET_VERA_START_SPRITE_ATTRS #$0, #$1 ; Sets VERA channel 0 to the start of the sprites attributes table with a stride of 1

@loop:
GET_NEXT_FROM_SPRITE_UPDATE_BUFFER ;Address 12:5 0 (buffer 0)
sta ZP_LOW_BYTE

GET_NEXT_FROM_SPRITE_UPDATE_BUFFER ;Address 16:13 1 (buffer 1)

ora ZP_LOW_BYTE
beq @addressReset

GET_NEXT_FROM_SPRITE_UPDATE_BUFFER ;X Low 2 (buffer 2)

GET_NEXT_FROM_SPRITE_UPDATE_BUFFER ;X High 3 (buffer 3)

GET_NEXT_FROM_SPRITE_UPDATE_BUFFER ;Y Low 4 (buffer 4)

stz VERA_data0 ;Y High 5 Always 0

lda #$8 ; Collision Z Lvl 2 and Flip 6 (8 means in front of bitmap but behind text layers and not flipped, with a zero collision mask)
sta VERA_data0

GET_NEXT_FROM_SPRITE_UPDATE_BUFFER ;Sprite Attr Size 7 (buffer 5)

bra @loop
@addressReset:
lda #< _bESpritesUpdatedBuffer
sta _bESpritesUpdatedBufferPointer
lda #> _bESpritesUpdatedBuffer
sta _bESpritesUpdatedBufferPointer + 1

@end:
rts
I have done all of the things I could think of to make sure there is no flicker:
1. Reduced cycles in the IRQ Handler routine
2. Moved the sprite handling code to the beginning of the IRQ handler, to rule out unrelated code eating the VBLANK time
3. Ensured that IRQ is disabled when it should be
I think we can rule out too many sprite per scanline, because even though the sprites are currently all 32 x 32 or 64 x 64 sprites (to be fixed) there are not enough sprites to cause that issue
I can’t see how I am running over IRQ time, there are only 6 sprites on screen currently

I also noticed that the flicker only affects static sprites, the door and Gwydion (boy with the blue pants). Notice how the chickens don’t flicker.

Re: How To Remove Sprite Flicker

Posted: Sat Jan 06, 2024 3:47 am
by DragWx
Are you acknowledging the vblank interrupt before you re-enable them? If not, your IRQ handler might be reacting to a "stale" vblank interrupt that happened while IRQs are disabled, which would cause your IRQ handler to run outside of vblank.

Re: How To Remove Sprite Flicker

Posted: Sat Jan 06, 2024 6:20 am
by Manannan
Hi DragWX,

Do you mean that before I execute a cli command in code that I need to reset any latches there too?

Can you point me to an example which explains what you mean?
Thanks for your time.

Regards,
Andrew Lay

Re: How To Remove Sprite Flicker

Posted: Sat Jan 06, 2024 7:43 am
by DragWx
That's what I mean, yes: putting an extra vblank acknowledgement (write #$01 to $9F27, the same as you do in your vblank IRQ routine already) right before the end of a SEI/CLI block.

The reason is, you're specifically looking for your vblank IRQ routine to run at the start of vblank each time. However, suppose vblank occurs during a SEI/CLI block. The vblank IRQ is not discarded, it just waits until interrupts are re-enabled, and then your code will immediately receive that IRQ, even though we're no longer at the start of vblank (and may even be out of vblank).

By putting a vblank acknowledgement right before the end of a SEI/CLI block, any "missed" vblank IRQs will be discarded, and the code will now correctly wait for the start of the next vblank, instead of trying to react to any it may have just missed.

Re: How To Remove Sprite Flicker

Posted: Sat Jan 06, 2024 7:50 am
by Manannan
DragWx wrote: Sat Jan 06, 2024 7:43 am That's what I mean, yes: putting an extra vblank acknowledgement (write #$01 to $9F27, the same as you do in your vblank IRQ routine already) right before the end of a SEI/CLI block.

The reason is, you're specifically looking for your vblank IRQ routine to run at the start of vblank each time. However, suppose vblank occurs during a SEI/CLI block. The vblank IRQ is not discarded, it just waits until interrupts are re-enabled, and then your code will immediately receive that IRQ, even though we're no longer at the start of vblank (and may even be out of vblank).

By putting a vblank acknowledgement right before the end of a SEI/CLI block, any "missed" vblank IRQs will be discarded, and the code will now correctly wait for the start of the next vblank, instead of trying to react to any it may have just missed.
Thanks,

I never would have thought of that.

I will let you know when I am done. . .

Re: How To Remove Sprite Flicker

Posted: Sat Jan 06, 2024 8:40 am
by Johan Kårlin
I think there is a simpler way of doing this, you don’t need a buffer and you don’t need to disable interrupts. Just wait for vblank, make your calculations and update all sprite attributes in VRAM right away. With a few sprites on the screen you should have plenty of time doing this.

To keep track of how much time is spent for differen parts of the program, I usually change one of the most common colors on the screen. I change color, make screen updates, change color, make calculations for the next frame, change to the original color. This is a variant of the old C64 trick of increasing the border color to measure time. I have often found that I have more time left than I thought.

Re: How To Remove Sprite Flicker

Posted: Sun Jan 07, 2024 2:55 am
by Manannan
DragWx wrote: Sat Jan 06, 2024 7:43 am That's what I mean, yes: putting an extra vblank acknowledgement (write #$01 to $9F27, the same as you do in your vblank IRQ routine already) right before the end of a SEI/CLI block.

The reason is, you're specifically looking for your vblank IRQ routine to run at the start of vblank each time. However, suppose vblank occurs during a SEI/CLI block. The vblank IRQ is not discarded, it just waits until interrupts are re-enabled, and then your code will immediately receive that IRQ, even though we're no longer at the start of vblank (and may even be out of vblank).

By putting a vblank acknowledgement right before the end of a SEI/CLI block, any "missed" vblank IRQs will be discarded, and the code will now correctly wait for the start of the next vblank, instead of trying to react to any it may have just missed.
Thanks DragWx,
Your suggestion solved the problem. I appreciate your time and effort.

Basically all I had to do was make these macros:

Code: Select all

.macro REENABLE_INTERRUPTS
 lda VSYNC_BIT ; Has a value of 1
 sta VERA_isr ;Defined in x16.inc
 cli
.endmacro
and

Code: Select all

#define REENABLE_INTERRUPTS() \
do { \
    asm("lda #%w", VSYNC_BIT); \
    asm("sta %w", VERA_ISR); \
    asm("cli"); \
  \
} while (0);
and call the macro in lieu of CLI.

There was also another bug as well which may have contributed. If you look at my code above in the 'CLEAR_SPRITE_ATTRS' macro you will notice that I am erroneously disabling sprites by zeroing the address of the sprite attribute not the ZDepth/ZOrder as the VERA manual states on page 10.


Here is an updated version of my code with the other bug fixed for the record.

Code: Select all


;Macro Definitions
.macro CLEAR_SPRITE_ATTRS NO_TO_CLEAR
.local @outerLoop
.local @outerLoopCheck
.local @innerLoop
.local @innerLoopCheck
.local @end

ldy NO_TO_CLEAR
beq @end

SET_VERA_START_SPRITE_ATTRS #$0, #$4, SA_VERA_ZORDER ;Set VERA channel 0 to the start of the sprites attributes table with a stride of 4 on the ZDepth byte. This is because as the VERA manual states sprites are disabled though this byte

@outerLoop:
stz VERA_data0
stz VERA_data1

@outerLoopCheck:
dey
bne @outerLoop

@end:
.endmacro

.macro GET_NEXT_FROM_SPRITE_UPDATE_BUFFER
.local @continue

lda _bESpritesUpdatedBuffer,y
iny
.endmacro

;Define Insertion Order:
;0 Vera Address Sprite Data Middle (Low will always be 0) (If both the first two bytes are zero that indicates the end of the buffer)
;1 Vera Address Sprite Data High
;2 x low
;3 x high
;4 y
;5 Sprite Attr Size/Palette Offset




ZP_LOW_BYTE = ZP_TMP_5


bEHandleSpriteUpdates: ; --------------Executed method is here

lda _bESpritesUpdatedBufferPointer ; If there is nothing in the buffer do nothing, leave the sprites as is
cmp #< _bESpritesUpdatedBuffer
bne @start
lda _bESpritesUpdatedBufferPointer + 1
cmp #> _bESpritesUpdatedBuffer
bne @start


jmp @end

@start:
CLEAR_SPRITE_ATTRS _maxViewTable

SET_VERA_START_SPRITE_ATTRS #$0, #$1 ; Sets VERA channel 0 to the start of the sprites attributes table with a stride of 1

@loop:
GET_NEXT_FROM_SPRITE_UPDATE_BUFFER ;Address 12:5 0 (buffer 0)
sta ZP_LOW_BYTE

GET_NEXT_FROM_SPRITE_UPDATE_BUFFER ;Address 16:13 1 (buffer 1)

ora ZP_LOW_BYTE
beq @addressReset

GET_NEXT_FROM_SPRITE_UPDATE_BUFFER ;X Low 2 (buffer 2)

GET_NEXT_FROM_SPRITE_UPDATE_BUFFER ;X High 3 (buffer 3)

GET_NEXT_FROM_SPRITE_UPDATE_BUFFER ;Y Low 4 (buffer 4)

stz VERA_data0 ;Y High 5 Always 0

lda #$8 ; Collision Z Lvl 2 and Flip 6 (8 means in front of bitmap but behind text layers and not flipped, with a zero collision mask)
sta VERA_data0

GET_NEXT_FROM_SPRITE_UPDATE_BUFFER ;Sprite Attr Size 7 (buffer 5)

bra @loop
@addressReset:
lda #< _bESpritesUpdatedBuffer
sta _bESpritesUpdatedBufferPointer
lda #> _bESpritesUpdatedBuffer
sta _bESpritesUpdatedBufferPointer + 1

@end:
rts

Re: How To Remove Sprite Flicker

Posted: Sun Jan 07, 2024 4:56 am
by Manannan
Johan Kårlin wrote: Sat Jan 06, 2024 8:40 am I think there is a simpler way of doing this, you don’t need a buffer and you don’t need to disable interrupts. Just wait for vblank, make your calculations and update all sprite attributes in VRAM right away. With a few sprites on the screen you should have plenty of time doing this.

To keep track of how much time is spent for differen parts of the program, I usually change one of the most common colors on the screen. I change color, make screen updates, change color, make calculations for the next frame, change to the original color. This is a variant of the old C64 trick of increasing the border color to measure time. I have often found that I have more time left than I thought.

Readers please note. The main reply is in the post above this reply is a courtesy to Johan Kårlin.

Hi Johan,
Thanks for the feedback. I am still learning and as such feedback is invaluable.
I am not sure that you are right on this one. I have hidden a lot of complexity from what I pasted above, but I will point out several issues which I can see with your suggestion.

Also sprites seem to be working now (although I need to tidy some loose ends), so I am reluctant to change things now.

Please note also, that I don’t wait for VBLANK after enabling interrupts; it happens at the end of the sprite interpreter cycle, whether there are sprite updates or not.

- My project is a hybrid C and assembler solution, because it is based on Meka which is C.
I believe that you cannot call C from the IRQ handler. In my experiments even calling empty C methods from IRQ causes a crash.
To display the sprites I need to read from the interpreter’s state, which is a complicated set of C structs with pointers that need to be followed. While possible in assembly this is a lot easier in C
- I also need to maintain my own state as well. To do this I need to access my banked memory allocator (banked equivalent of malloc) which is written in C and can’t be called from the IRQ.
This job is also easier in C.
- It’s also easier to debug if the IRQ is as simple as possible. If the complexity is kept out of the IRQ handler, then I can debug with my C based interpreter debugger and temporary printf’s
- Blitting outside of the IRQ allows me to blit whole loops averaging 7 frames each, at once. Some animation frames are too large to fit into a single sprite as well and there are some sequences that are up to 15 frames each in some games. I think I may have to do one at a time if I did this in IRQ, which seems inefficient.
- I can’t just wait for the IRQ handler to perform updates and keep the interpreter running. The AGI interpreter expects an interpreter cycle worth of sprite updates and then a wait for the next cycle.

This is a really complicated project requiring a thorough understanding of Sierra AGI, and hours spent in careful general analysis. I can’t cover all bases without writing a tome (super long book). But I hope I summarized some of the main reasons why I did things the way I did.

Thanks for your insight; I may need your assembly expertise one day 😊.

Re: How To Remove Sprite Flicker

Posted: Sun Jan 07, 2024 8:45 pm
by Johan Kårlin
Okay, I am sorry that I didn't understand the complexity of the task and that some things required multiple frames to complete. I was eager to help, but I acted too hastily. I assumed that it was comparable to problems I have faced before. I wish you the best of luck with your project, it seemed interesting in the brief video you provided. I am pleased to hear that you have resolved the issue.

Re: How To Remove Sprite Flicker

Posted: Sun Jan 07, 2024 11:18 pm
by Manannan
Johan Kårlin wrote: Sun Jan 07, 2024 8:45 pm Okay, I am sorry that I didn't understand the complexity of the task and that some things required multiple frames to complete. I was eager to help, but I acted too hastily. I assumed that it was comparable to problems I have faced before. I wish you the best of luck with your project, it seemed interesting in the brief video you provided. I am pleased to hear that you have resolved the issue.
No problem, I appreciate advice. I like your 'color change border' trick I could use that. :)