8

I was trying to implement multiple LYC IRQs to implement faux parallax effects (like in Game Gear Shinobi), and my method was to begin LYC interrupts at a particular scanline, say about halfway down the screen, and using lookup tables, scroll the screen every X frames and then set the LYC register to the next scanline in the lookup table. It worked as intended, except I noticed a bit of "tearing" where the scroll is not perfectly split across an 8-pixel boundary (I specifically chose LYC values that were spaced apart in multiples of 8), and there seems to be some "spilling" into the next scanline. Is this a direct result of the interrupt code being too slow?

The code:

ParallaxLayer1      equ &C021   ;scroll value for parallax layer 1
ParallaxLayer2      equ &C022   ;scroll value for parallax layer 2
ParallaxLayer3      equ &C022   ;scroll value for parallax layer 3

LYC_Lookup:     ;which scanline to out to LYC for the NEXT IRQ
    byte 32,48,64,0
ScrollSpeed_Lookup:
    byte 0,4-1,8-1,16-1 


doParallax:         ;the STAT interrupt jumps here. This is an LYC interrupt.
    push af
    push hl
    push de
        ld hl,ParallaxLayer1
        ld de,ScrollSpeed_Lookup
        
        ld a,(LYC_Pointer)
        and &03
        jr z,skipParallax
        
        add L
        ld L,a      ;offset to the correct parallax layer for this interrupt.

        ld a,(LYC_Pointer)
        and &03
        add e
        ld E,a          ;now both HL and DE are offset by A.
        
        ld a,(de)       ;get the bit mask for the current parallax layer 
                        ;(this represents scroll speed, bigger number = slower scrolling)
  
        ld e,a          ;we will AND the frametimer with E later.
        
        ld a,(hl)
        gb_out <scx     ;we're still going to out the scroll value to SCX regardless.
        
        ld a,(frametimer)
        and E
        jr nz,skipParallax
            inc (hl)    ;increment the parallax layer scroll value.
skipParallax:
    ;prep this interrupt to happen during the next LYC in the list.
    ld a,(LYC_Pointer)
    ld hl,LYC_Lookup
    and &03
    add L
    ld L,a
    ld a,(hl)
    gb_out <lyc           ;this code shall run again at the next specified scanline.

    ld a,(LYC_Pointer)    ;inc index for next time.
    inc a
    ld (LYC_Pointer),a
    
    pop de
    pop hl
    pop af
    reti

Scanline Tearing

1 Answer 1

12

It can be a direct result of that, yes. I've summed up the clock durations of your routine and it comes up to 344 (plus a little bit for entering the interrupt, and for your OUT macro), which is a pretty long routine. I don't think that's the problem though since the duration of a line is fixed at 456 clocks. (If you exceeded that, it would be really bad.)

I think your issue is that handling the LYC interrupt begins processing at the start of a line, and your SCX modification happens over 100 clocks after that. This puts it into Mode 3 of the GB's line drawing cycle, where it's already drawing things onto the screen. To make your effect more stable, you want to update any LCD registers as early as possible. Preferably immediately. The very first thing your handler should do is read the new SCX value from a dedicated 'new scx' byte in high ram and commit it, then do whatever index handling/cycling/reading/storing you want in preparation for the next line.

Maybe this would be the fastest way?

interrupt_begin:
       ldh SCRATCH,a  ; 12, store A in high ram temporarily
       ldh a,NEW_SCX  ; 12, get new SCX to store from high ram
       ldh SCX,a      ; 12, SCX is stored.
       ldh a,SCRATCH  ; A is back.
       ...            ; Do operations to prepare next NEW_SCX.
                      ; Don't forget to push/pop AF if you do anything extra.
       reti

Ideally you should be doing no calculations in a LYC or HBLANK handler. Calculate everything ahead of time in your VBLANK: make an array of scroll values, even on lines you don't intend to alter the scrolling on. You can then read and iterate from there in the HBLANK instead of LYC (you should be able to use STAT interrupts to check for the Mode 3 -> Mode 0 transition) using the knowledge that after VBLANK terminates the 0th line is displayed, then a HBLANK, then the 1st line. Something like that. LYC is more useful to detect the beginning or end of a region that has a common interrupt. HBLANK gives you more time to react and doesn't need to be re-primed as long as you want to do something every line / every xth line.

Your code with clock cycles shown:

16      push af
16      push hl
16      push de
12      ld hl,ParallaxLayer1
12      ld de,ScrollSpeed_Lookup
    
16      ld a,(LYC_Pointer)
 8      and &03
 8      jr z,skipParallax
        
 4      add L
 4      ld L,a      ;offset to the correct parallax layer for this interrupt.

16      ld a,(LYC_Pointer)
 8      and &03
 4      add e
 4      ld E,a          ;now both HL and DE are offset by A.
        
 8      ld a,(de)       ;get the bit mask for the current parallax layer 
                        ;(this represents scroll speed, bigger number = slower scrolling)
  
 4      ld e,a          ;we will AND the frametimer with E later.
    
 8      ld a,(hl)
[Total clocks since start of interrupt = 164 here.]
 ?      gb_out <scx     ;we're still going to out the scroll value to SCX regardless.
    
16      ld a,(frametimer)
 4      and E
 8      jr nz,skipParallax
12      inc (hl)    ;increment the parallax layer scroll value.
skipParallax:
        ;prep this interrupt to happen during the next LYC in the list.
16      ld a,(LYC_Pointer)
12      ld hl,LYC_Lookup
 8      and &03
 4      add L
 4      ld L,a
 8      ld a,(hl)
 ?      gb_out <lyc           ;this code shall run again at the next specified scanline.

16      ld a,(LYC_Pointer)    ;inc index for next time.
 4      inc a
16      ld (LYC_Pointer),a
    
12      pop de
12      pop hl
12      pop af
16      reti
5
  • I'm sorry, I got so used to the out macro that I forgot it was a macro! It's just LDH (#),a. I didn't know the syntax at the time for LDH so I just made a macro that inlines the bytecode Nov 21, 2021 at 0:32
  • No problem, I couldn't remember off the top of my head if the gb had a real OUT instruction for system registers or if they were memory mapped. I've been doing a lot of Sound Blaster coding in DOS which uses OUT. :)
    – knol
    Nov 21, 2021 at 0:34
  • I have to say, I never thought of using LDH to back up the accumulator faster than PUSH/POP AF - that's pretty clever! Nov 21, 2021 at 3:30
  • LHD is only very slightly faster, and it takes up a byte of high ram for SCRATCH for however long you're on the screen where the effect is displayed, and you still have to PUSH/POP AF later if you want to do any maths to read a new NEW_SCX since they'll destroy F. There's very little reason to do ldh instead of PUSH AF immediately entering the interrupt (which doesn't use SCRATCH), I just wanted to see what was the fastest way to get a var into SCX. Interrupts are tight but you can spare those four cycles! :D
    – knol
    Nov 21, 2021 at 6:15
  • I think I should start be restructuring my main document, it's an absolute mess. Nov 21, 2021 at 18:47

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .