Add Hill Top Zone to 2P VS Mode

Discussion in 'Engineering & Reverse Engineering' started by E-122-Psi, Jul 7, 2020.

  1. MoDule


    Tech Member
    Procrastinating from writing bug-fix guides
    Both the water and split-screen effects rely on horizontal interrupts, and the game isn't programmed to account for both effects at once. The interrupt handler itself decides which effect to use based on whether you're in two-player mode, and the game as a whole only ever prepares for a single horizontal interrupt.

    To get both effects working, you'd have to include some extra logic to calculate when each of the interrupts is supposed to happen (i.e. on which scan line), as well as what each interrupt is supposed to do. Ideally you'd do this outside the interrupt handler, since you don't want to waste time on anything other than data transfers there.

    This was actually a topic in the community a few years ago, whether it was even possible to do up to three full palette transfers each frame.
  2. E-122-Psi


    This is gonna be harder than I thought. :P

    Hill Top seems to be the 'easy' fix of the lot. I think to get the other levels running competently I'd need someone with scrolling and compression data knowledge apparent.

    I suppose there is another factor to consider adding to multiplayer though, and that's more characters:


    Making a 2 player friendly Knuckles that uses one palette and has correct tile mappings would likely be simple enough, but even then I'd need to figure out how to make a basic character select option for 2 player mode.

    The best ideas for projects I can think of otherwise are adding a 'Fixed Items' option like Mania did where the monitor generation is normal. Other than, THREE player split screen if people wanna get REALLY ambitious? (I think you'll be on your own for that one though.) :P
  3. MoDule


    Tech Member
    Procrastinating from writing bug-fix guides
    So, I've been working on the interrupt thing I mentioned earlier and it turns out it's a little more complicated than I outlined previously.
    If you look past the graphical garbage, you'll see splitscreen with water present both on the top and bottom. It's not perfect yet, though. For one, it flickers pretty bad whenever slowdown occurs. I haven't been able to pin down why that happens here, but not on waterless splitscreen (I left the original H-Int handlers intact and just switch to this special one for zones with water). The title cards are also a little wonky, but everything seems to be playable, excluding object loading glitches and slowdown.
  4. E-122-Psi


    You got it running? Bravo. :D

    (I may desire to pick your brain later :P)

    Ah well the garbage might be down to things I had to run through. Did you customise the SwScrl data for the level? If not switch it to one of the two player branches like I did here (this redirects to HTZ's 2 player scroll routine). It won't make it perfect background wise (I'll need to figure out how to make custom 2 player scroll routines) but it will make Tails' screen more coherent:

    Code (Text):
    1. SwScrl_ARZ:
    2.     tst.w    (Two_player_mode).w
    3.     bne.w    loc_CB10
    4.     move.w    ($FFFFEEB0).w,d4
    5.     ext.l    d4
    6.     muls.w    #$119,d4
    7. ........
    Also some of the badnik tiles interrupt the split screen data in the VRAM, which is around the $A000 area (this is also what corrupts the title card). Turn off or redirect Whisp, Chopchop and Grounder's art in the PLC entry for the level.
    Last edited: Jul 15, 2020
  5. MoDule


    Tech Member
    Procrastinating from writing bug-fix guides
    Maybe I could start looking into the scroll routines. I did fully document most of them a while back, so I might have an easier time making two-player versions of them.

    So, I was able to fix the flickering bug. Just forgot to reset my scroll registers and sprite tables in one place. There's still two more bugs that I want to iron out, though.
    • For some reason, when Tails is fully under water, then moves up enough to put the water surface on screen, for one frame his screen will fully use the above water palette. Not sure what's causing that...
    • On the top screen, the water palette will sometimes show up a little too late (i.e. a few lines below the water surface) if it is too close to the top of the screen. This is because of the Do_Updates routine taking too long and thus delaying the interrupt handler. I need to find a smarter way of scheduling that call, if it's possible.
    Or maybe I could just post my code changes here and see if someone else can find the fix :V
  6. E-122-Psi


    Can if you want. This is a tutorial thread after all (and I'd love all these VS fixes to be open source really).

    Worse comes to worse, I suppose we can just turn off the water palette for VS mode. Wouldn't be the first time a Sonic game didn't use them.
  7. MoDule


    Tech Member
    Procrastinating from writing bug-fix guides
    I have no intention of keeping it secret. I would just prefer to have it fully working before I make a fully fledged guide.

    Edit: I suppose I could post a preliminary version. That way people could look it over and tell me if I'm doing something terribly wrong
  8. E-122-Psi


    Our good friend Zeta_Null has still been busy tweaking CPZ's tileset to work in split screen by the way, and right now we almost have a working version:


    There's only a couple objects not reworked at this point (Grabber, the boost pads, and the tipping platforms). Obviously the level tiles themselves needed to be streamlined a good deal to make this work as well but at least it looks coherent and clean now. Zeta_Null fixed the background as well, but I need to fix the scrolling in 2 player to properly show that.

    The objects needed to be trimmed down in some areas due to being a vertical heavy level, but it is raceable:

    EDIT: Updated version which fixes some of the remaining object art:

    EDIT: ANOTHER refinement:

    EDIT: ANOTHER OTHER refinement. :P This one adds a couple more objects back and fixes Spiny's projectile:
    Last edited: Jul 17, 2020
  9. MoDule


    Tech Member
    Procrastinating from writing bug-fix guides
    OK, here's a quick rundown of the code changes I made with little commentary. I'll go into more detail and arrange everything better once I write the proper guide. Look out for ;<-- comments; those show where I changed something, or point out something important.

    First things first, we're going to need 23 bytes of extra RAM variables:
    Code (Text):
    1. ; I placed these two in the free space above Hint_flag, around DMA_data_thunk
    2. Hint_water:            ds.w    1    ; cleared after top screen's water effect
    3. Hint_split:            ds.w    1    ; cleared after the screen has been split
    5. H_int_routine:            ds.l    1    ; first H-Int routine to run after V-Int
    7. ;<-- these two need to be together
    8. H_int_schedule_split_copy:    ds.w    1    ; copied during V-Int
    9. H_int_schedule_water_copy:    ds.w    1    ; copied during V-Int
    11. ; I placed these at the end, between Music_to_play_2 and Demo_mode_flag
    12. Water_fullscreen_flag_P2:    ds.b    1
    13. H_int_schedule_split:        ds.w    1    ;<-- these also need to be together  
    14. H_int_schedule_water:        ds.w    1    ;<--
    15. H_int_jmp:            ds.w    1    ; JMP command
    16. H_int_addr:            ds.l    1    ; address to jump to
    Next, we'll look at the code changes. I'll just present them from top to bottom, in the order that they show up in the code.

    Edit the interrupt vector to point to one of our new RAM addresses:
    Code (Text):
    1. ;Vectors:
    2.     dc.l System_Stack, EntryPoint, ErrorTrap, ErrorTrap; 4
    3.     dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap; 8
    4.     dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap; 12
    5.     dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap; 16
    6.     dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap; 20
    7.     dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap; 24
    8.     dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap; 28
    9.     dc.l H_int_jmp, ErrorTrap, V_Int,     ErrorTrap; 32        ;<--
    10.     dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap; 36
    11.     dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap; 40
    12.     dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap; 44
    13.     dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap; 48
    14.     dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap; 52
    15.     dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap; 56
    16.     dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap; 60
    17.     dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap; 64
    Initialize some of our new variables after the checksum test:
    Code (Text):
    1. ;checksum_good:
    2.     lea    (System_Stack).w,a6
    3.     moveq    #0,d7
    5.     move.w    #bytesToLcnt($200),d6
    6. -    move.l    d7,(a6)+
    7.     dbf    d6,-
    9.     move.b    (HW_Version).l,d0
    10.     andi.b    #$C0,d0
    11.     move.b    d0,(Graphics_Flags).w
    12.     move.l    #'init',(Checksum_fourcc).w ; set flag so checksum won't be run again
    13.     move.w    #$4EF9,(H_int_jmp).w                    ;<--
    14.     move.l    #H_Int,(H_int_routine).w                ;<--
    15.     move.l    #$8ADF8ADF,(H_int_schedule_split).w    ; initialize    ;<--
    16. ; loc_370:
    17. GameInit:
    Reset some variables during V-Int:
    Code (Text):
    1. V_Int:
    2.     movem.l    d0-a6,-(sp)
    3.     tst.b    (Vint_routine).w
    4.     beq.w    Vint_Lag
    6. -    move.w    (VDP_control_port).l,d0
    7.     andi.w    #8,d0
    8.     beq.s    -
    10.     move.l    #vdpComm($0000,VSRAM,WRITE),(VDP_control_port).l
    11.     move.l    (Vscroll_Factor).w,(VDP_data_port).l
    12.     btst    #6,(Graphics_Flags).w
    13.     beq.s    +
    15.     move.w    #$700,d0
    16. -    dbf    d0,- ; wait here in a loop doing nothing for a while...
    17. +
    18.     move.b    (Vint_routine).w,d0
    19.     move.b    #VintID_Lag,(Vint_routine).w
    20.     move.w    #1,(Hint_flag).w
    21.     move.w    #1,(Hint_water).w                    ;<--
    22.     move.w    #1,(Hint_split).w                    ;<--
    23.     move.l    (H_int_schedule_split).w,(H_int_schedule_split_copy).w    ;<--
    24.     move.l    (H_int_routine).w,(H_int_addr).w            ;<--
    25.     andi.w    #$3E,d0
    26.     move.w    Vint_SwitchTbl(pc,d0.w),d0
    27.     jsr    Vint_SwitchTbl(pc,d0.w)
    29. VintRet:
    30.     addq.l    #1,(Vint_runcount).w
    31.     movem.l    (sp)+,d0-a6
    32.     rte
    More resetting and a branch to a new two player mode handler:
    Code (Text):
    1. loc_4C4:
    2.     tst.b    (Water_flag).w
    3.     beq.w    Vint0_noWater
    4.     tst.w    (Two_player_mode).w                    ;<--
    5.     bne.w    Vint0_water2P                        ;<--
    6.     move.w    (VDP_control_port).l,d0
    7.     btst    #6,(Graphics_Flags).w
    8.     beq.s    +
    10.     move.w    #$700,d0
    11. -    dbf    d0,- ; do nothing for a while...
    12. +
    13.     ; restore our H-Int schedule from previous frame
    14.     move.w    #1,(Hint_flag).w
    15.     move.w    #1,(Hint_water).w                    ;<--
    16.     move.w    #1,(Hint_split).w                    ;<--
    17.     move.l    (H_int_schedule_split).w,(H_int_schedule_split_copy).w    ;<--
    18.     move.l    (H_int_routine).w,(H_int_addr).w            ;<--
    20.     stopZ80
    More resetting:
    Code (Text):
    1. Vint0_noWater:
    2.     move.w    (VDP_control_port).l,d0
    3.     move.l    #vdpComm($0000,VSRAM,WRITE),(VDP_control_port).l
    4.     move.l    (Vscroll_Factor).w,(VDP_data_port).l
    5.     btst    #6,(Graphics_Flags).w
    6.     beq.s    +
    8.     move.w    #$700,d0
    9. -    dbf    d0,- ; do nothing for a while...
    10. +
    11.     ; restore our H-Int schedule from previous frame
    12.     move.w    #1,(Hint_flag).w
    13.     move.w    #1,(Hint_water).w                    ;<--
    14.     move.w    #1,(Hint_split).w                    ;<--
    15.     move.l    (H_int_schedule_split).w,(H_int_schedule_split_copy).w    ;<--
    16.     move.l    (H_int_routine).w,(H_int_addr).w            ;<--
    18.     move.w    (Hint_counter_reserve).w,(VDP_control_port).l
    This is new code that should be placed under Vint0_noWater:
    Code (Text):
    1. Vint0_water2P:
    2.     move.w    (VDP_control_port).l,d0
    3.     move.l    #vdpComm($0000,VSRAM,WRITE),(VDP_control_port).l
    4.     move.l    (Vscroll_Factor).w,(VDP_data_port).l
    5.     btst    #6,(Graphics_Flags).w
    6.     beq.s    .notPAL
    8.     move.w    #$700,d0
    10. .loopWatiPAL:
    11.     dbf    d0,.loopWatiPAL ; do nothing for a while...
    13. .notPAL:
    14.     ; restore our H-Int schedule from previous frame
    15.     move.w    #1,(Hint_flag).w
    16.     move.w    #1,(Hint_water).w
    17.     move.w    #1,(Hint_split).w
    18.     move.l    (H_int_schedule_split).w,(H_int_schedule_split_copy).w
    19.     move.l    (H_int_routine).w,(H_int_addr).w
    21.     stopZ80
    23.     tst.b    (Water_fullscreen_flag).w
    24.     bne.s    .waterPal
    26.     dma68kToVDP Normal_palette,$0000,palette_line_size*4,CRAM
    28.     bra.s    .resetScreen
    29. ; ---------------------------------------------------------------------------
    31. .waterPal:
    32.     dma68kToVDP Underwater_palette,$0000,palette_line_size*4,CRAM
    34. .resetScreen:
    35.     move.w    (Hint_counter_reserve).w,(a5)
    36.     move.w    #$8200|(VRAM_Plane_A_Name_Table/$400),(VDP_control_port).l    ; Set scroll A PNT base to $C000
    37.     move.l    (Vscroll_Factor_P2).w,(Vscroll_Factor_P2_HInt).w
    38.     dma68kToVDP Sprite_Table,VRAM_Sprite_Attribute_Table,VRAM_Sprite_Attribute_Table_Size,VRAM
    39.     bsr.w    sndDriverInput
    41.     startZ80
    43.     bra.w    VintRet
    Even more resetting:
    Code (Text):
    1. loc_748:
    2.     move.w    (Hint_counter_reserve).w,(a5)
    3.     move.w    #$8200|(VRAM_Plane_A_Name_Table/$400),(VDP_control_port).l    ; Set scroll A PNT base to $C000
    5.     dma68kToVDP Horiz_Scroll_Buf,VRAM_Horiz_Scroll_Table,VRAM_Horiz_Scroll_Table_Size,VRAM
    6.     dma68kToVDP Sprite_Table,VRAM_Sprite_Attribute_Table,VRAM_Sprite_Attribute_Table_Size,VRAM
    8.     bsr.w    ProcessDMAQueue
    9.     bsr.w    sndDriverInput
    11.     startZ80
    13.     movem.l    (Camera_RAM).w,d0-d7
    14.     movem.l    d0-d7,(Camera_RAM_copy).w
    15.     movem.l    (Camera_X_pos_P2).w,d0-d7
    16.     movem.l    d0-d7,(Camera_P2_copy).w
    17.     movem.l    (Scroll_flags).w,d0-d3
    18.     movem.l    d0-d3,(Scroll_flags_copy).w
    19.     move.l    (Vscroll_Factor_P2).w,(Vscroll_Factor_P2_HInt).w
    20.     move.w    #1,(Hint_water).w                    ;<--
    21.     move.w    #1,(Hint_split).w                    ;<--
    22.     move.l    (H_int_schedule_split).w,(H_int_schedule_split_copy).w    ;<--
    23.     move.l    (H_int_routine).w,(H_int_addr).w            ;<--
    24. ;    cmpi.b    #$5C,(Hint_counter_reserve+1).w                ;<--
    25.     cmpi.b    #$2F,(Hint_counter_reserve+1).w        ; move updates into H-Int a little earlier
    26.     bhs.s    Do_Updates
    27.     move.b    #1,(Do_Updates_in_H_int).w
    28.     rts
    Code (Text):
    1. loc_BD6:
    2.     move.w    (Hint_counter_reserve).w,(a5)
    4.     dma68kToVDP Horiz_Scroll_Buf,VRAM_Horiz_Scroll_Table,VRAM_Horiz_Scroll_Table_Size,VRAM
    5.     dma68kToVDP Sprite_Table,VRAM_Sprite_Attribute_Table,VRAM_Sprite_Attribute_Table_Size,VRAM
    7.     bsr.w    ProcessDMAQueue
    8.     jsr    (DrawLevelTitleCard).l
    9.     jsr    (sndDriverInput).l
    11.     startZ80
    13.     movem.l    (Camera_RAM).w,d0-d7
    14.     movem.l    d0-d7,(Camera_RAM_copy).w
    15.     movem.l    (Scroll_flags).w,d0-d1
    16.     movem.l    d0-d1,(Scroll_flags_copy).w
    17.     move.l    (Vscroll_Factor_P2).w,(Vscroll_Factor_P2_HInt).w
    18.     move.w    #1,(Hint_water).w                    ;<--
    19.     move.w    #1,(Hint_split).w                    ;<--
    20.     move.l    (H_int_schedule_split).w,(H_int_schedule_split_copy).w    ;<--
    21.     move.l    (H_int_routine).w,(H_int_addr).w            ;<--
    22.     bsr.w    ProcessDPLC
    23.     rts
    Small adjustment to regular H-Int handler:
    Code (Text):
    1. H_Int:
    2.     tst.w    (Hint_flag).w
    3.     beq.w    +
    4.     tst.w    (Two_player_mode).w
    5.     beq.w    PalToCRAM
    7. H_Int_splitscreen_nowater:
    8.     move.w    #0,(Hint_flag).w
    9.     move.w    #0,(Hint_split).w                    ;<--
    10.     move.l    a5,-(sp)
    11.     move.l    d0,-(sp)
    Add our custom H-Int handlers under PalToCRAM:
    Code (Text):
    1. ; |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
    2. ; Special two player mode H-Int handlers for water + splitscreen
    3. ;
    4. ; We need to add extra interrupts between our water and splitscreen effects,
    5. ; because the H-Int counter doesn't take on a new value until one interrupt
    6. ; after it was set.
    8. ; ===========================================================================
    9. ; Interrupt handler for halfway point to top screen's water level
    10. ;
    11. ; Produces no visual effect, but is needed to set up the interrupt
    12. ; counter for the splitscreen effect
    13. H_Int_water_halfway:
    14.     ; set interrupt counter for halfway between water and splitscreen
    15.     move.w    (H_int_schedule_split_copy).w,(VDP_control_port).l    ; applys after next H-Int
    17.     ; set next H-Int routine
    18.     move.l    #H_Int_water_top_screen,(H_int_addr).w
    20.     tst.b    (Do_Updates_in_H_int).w
    21.     beq.s    .return
    23.     ; only do updates if we have enough time before doing the water palette
    24.     cmpi.b    #$2F>>1,(Hint_counter_reserve+1).w
    25.     blo.s    .return
    26.     clr.b    (Do_Updates_in_H_int).w
    27.     movem.l    d0-a6,-(sp)
    28.     bsr.w    Do_Updates
    29.     movem.l    (sp)+,d0-a6
    31. .return:
    32.     rte
    34. ; ===========================================================================
    35. ; Interrupt handler for top screen's water effect
    36. H_Int_water_top_screen:
    37.     tst.w    (Hint_water).w
    38.     beq.w    .return
    40.     ; temporarily disable H-Int processing
    41.     move    #$2700,sr
    42.     move.w    #0,(Hint_water).w
    44.     ; set next H-Int routine
    45.     move.l    #H_Int_splitscreen_halfway,(H_int_addr).w
    47.     ; transfer water palette for top screen
    48.     movem.l    a0-a1,-(sp)
    49.     lea    (VDP_data_port).l,a1
    50.     lea    (Underwater_palette).w,a0     ; load palette from RAM
    51.     move.l    #vdpComm($0000,CRAM,WRITE),4(a1)    ; set VDP to write to CRAM address $00
    52.     rept 32
    53.     move.l    (a0)+,(a1)    ; move palette to CRAM (all 64 colors at once)
    54.     endm
    56.     movem.l    (sp)+,a0-a1
    57.     tst.b    (Do_Updates_in_H_int).w
    58.     beq.s    .return
    60.     clr.b    (Do_Updates_in_H_int).w
    61.     movem.l    d0-a6,-(sp)
    62.     bsr.w    Do_Updates
    63.     movem.l    (sp)+,d0-a6
    65. .return:
    66.     rte
    67. ; the H-Int counter value set in H_Int_water_halfway will now apply
    69. ; ===========================================================================
    70. ; Interrupt handler for halfway point to splitscreen effect
    71. ;
    72. ; Produces no visual effect, but is needed to set up the next interrupt
    73. ; counter for the bottom screen's water effect
    74. H_Int_splitscreen_halfway:
    75.     ; set interrupt counter for bottom screen's water
    76.     move.w    (H_int_schedule_water_copy).w,(VDP_control_port).l    ; applys after next H-Int
    78.     ; set next H-Int routine
    79.     move.l    #H_Int_splitscreen,(H_int_addr).w
    81.     tst.b    (Do_Updates_in_H_int).w
    82.     beq.s    .return
    84.     clr.b    (Do_Updates_in_H_int).w
    85.     movem.l    d0-a6,-(sp)
    86.     bsr.w    Do_Updates
    87.     movem.l    (sp)+,d0-a6
    89. .return:
    90.     rte
    92. ; ===========================================================================
    93. ; Interrupt handler for splitscreen effect (with water potentially present)
    94. ;
    95. ; Because we have to transfer the palette again, we lose one more scanline
    96. ; than we would in H_Int_splitscreen_normal
    97. H_Int_splitscreen:
    98.     tst.w    (Hint_split).w
    99.     beq.w    .return
    101.     ; temporarily disable H-Int processing
    102.     move.w    #0,(Hint_split).w
    104.     ; set next H-Int routine
    105.     move.l    #H_Int_water_bottom_screen,(H_int_addr).w
    107.     move.l    a5,-(sp)
    108.     move.l    d0,-(sp)
    110. .waitForVblank:
    111.     move.w    (VDP_control_port).l,d0    ; loop start: Make sure V_BLANK is over
    112.     andi.w    #4,d0
    113.     beq.s    .waitForVblank
    115.     ; Display disable
    116.     move.w    (VDP_Reg1_val).w,d0
    117.     andi.b    #$BF,d0
    118.     move.w    d0,(VDP_control_port).l
    120.     move.w    #$8200|(VRAM_Plane_A_Name_Table_2P/$400),(VDP_control_port).l    ; PNT A base: $A000
    121.     move.l    #vdpComm($0000,VSRAM,WRITE),(VDP_control_port).l
    122.     move.l    (Vscroll_Factor_P2_HInt).w,(VDP_data_port).l
    124.     stopZ80
    125.     dma68kToVDP Sprite_Table_2,VRAM_Sprite_Attribute_Table,VRAM_Sprite_Attribute_Table_Size,VRAM
    126.     tst.b    (Water_fullscreen_flag_P2).w
    127.     bne.s    .water
    128.     dma68kToVDP Normal_palette,$0000,palette_line_size*4,CRAM
    129.     bra.s    .restartZ80
    131. .water:  
    132.     dma68kToVDP Underwater_palette,$0000,palette_line_size*4,CRAM
    134. .restartZ80:
    135.     startZ80
    137. .waitForVblank2:
    138.     move.w    (VDP_control_port).l,d0
    139.     andi.w    #4,d0
    140.     beq.s    .waitForVblank2
    142.     ; Display enable
    143.     move.w    (VDP_Reg1_val).w,d0
    144.     ori.b    #$40,d0
    145.     move.w    d0,(VDP_control_port).l
    147.     move.l    (sp)+,d0
    148.     movea.l    (sp)+,a5
    150. .return:
    151.     rte
    152. ; the H-Int counter value set in H_Int_splitscreen_halfway will now apply
    154. ; ===========================================================================
    155. ; Interrupt handler for bottom screen's water effect
    156. ;
    157. ; Also disables processing of future interrupts for this frame
    158. H_Int_water_bottom_screen:
    159.     tst.w    (Hint_flag).w
    160.     beq.s    H_Int_skip
    162.     ; prevent future procesing of H-Ints for this frame
    163.     move    #$2700,sr
    164.     move.w    #0,(Hint_flag).w
    166.     ; set next H-Int routine
    167.     move.l    #H_Int_skip,(H_int_addr).w
    169.     ; set next interrupt to occur off screen (i.e. disabling H-Int)
    170.     move.w    #$8A6F+4,(VDP_control_port).l    ; next H-Int in 224 lines
    172.     ; transfer water palette for bottom screen
    173.     movem.l    a0-a1,-(sp)
    174.     lea    (VDP_data_port).l,a1
    175.     lea    (Underwater_palette).w,a0     ; load palette from RAM
    176.     move.l    #vdpComm($0000,CRAM,WRITE),4(a1)    ; set VDP to write to CRAM address $00
    177.     rept 32
    178.     move.l    (a0)+,(a1)    ; move palette to CRAM (all 64 colors at once)
    179.     endm
    181.     movem.l    (sp)+,a0-a1
    183. H_Int_skip:
    184.     rte
    Prevent water from disabling split-screen; initialization:
    Code (Text):
    1. Level_InitWater:
    2.     move.b    #1,(Water_flag).w
    3. ;    move.w    #0,(Two_player_mode).w                    ;<--
    4. +
    5.     lea    (VDP_control_port).l,a6
    7.     ;(...)
    9.     move.w    #$8ADF,(Hint_counter_reserve).w    ; H-INT every 223rd scanline
    10.     move.l    #H_Int,(H_int_routine).w    ; reset H-Int routine    ;<--
    11.     tst.w    (Two_player_mode).w
    12.     beq.s    +
    Add a two-player-mode version to MoveWater:
    Code (Text):
    1. MoveWater:
    2.     clr.b    (Water_fullscreen_flag).w
    3.     moveq    #0,d0
    4.     cmpi.b    #aquatic_ruin_zone,(Current_Zone).w    ; is level ARZ?
    5.     beq.s    +        ; if yes, branch
    6.     move.b    (Oscillating_Data).w,d0
    7.     lsr.w    #1,d0
    8. +
    9.     tst.w    (Two_player_mode).w                    ;<--
    10.     bne.s    MoveWater2P                        ;<--
    11.     add.w    (Water_Level_2).w,d0
    Add some new code after NonWaterEffects:
    Code (Text):
    1. MoveWater2P:
    2.     clr.b    (Water_fullscreen_flag_P2).w
    3.     moveq    #$6B,d2            ; scanline where second screen starts
    4.     add.w    (Water_Level_2).w,d0
    5.     move.w    d0,(Water_Level_1).w
    6.     move.w    (Water_Level_1).w,d0
    7.     move.w    d0,d1            ; save water position for later
    8.     sub.w    (Camera_Y_pos).w,d0    ; calculate distance between water surface and top of screen
    9.     asr.w    #1,d0            ; adjust to screen position
    11.     ; since interrupts take a while to occur and handle, let's not trigger
    12.     ; any too close to the top of the screen
    13.     cmpi.w    #3,d0            ; is water less than 4 lines below the top of the screen?
    14.     bge.s    .notFullScreen        ; if not, branch
    15.     move.b    #1,(Water_fullscreen_flag).w
    16.     bra.s    .setSplitscreenP1
    18. .notFullScreen:
    19.     ; same for the bottom of the first screen. We don't want to have a
    20.     ; palette change too close to the split
    21.     cmpi.w    #$6B-4,d0    ; is water within 4 pixels of the split?
    22.     blo.s    .setWaterP1    ; if not, branch
    24. .setSplitscreenP1:
    25.     ; prepare directly for splitscreen, since there is no water on the top screen
    26.     move.l    #H_Int_splitscreen_halfway,(H_int_routine).w    ; start with the split effect
    27.     move.b    #$6B>>1,(Hint_counter_reserve+1).w    ; do two H-INTs at 108/2 lines each
    28.     bra.s    .p2
    30. .setWaterP1:
    31.     ; prepare for top screen's water
    32.     lsr.w    #1,d0        ; adjust water lines for two interrupts
    33.     sub.w    d0,d2        ; lines between water and splitscreen
    34.     sub.w    d0,d2
    35.     lsr.w    #1,d2        ; adjust split lines for two interrupts
    36.     subq.w    #1,d0        ; H-Int counter is 0-based
    37.     move.l    #H_Int_water_halfway,(H_int_routine).w        ; start with the water effect
    38.     move.b    d0,(Hint_counter_reserve+1).w    ; do two H-INTs at d0/2 lines each
    39.     move.b    d2,(H_int_schedule_split+1).w    ; linecount for the two H-INTs leading up to splitscreen
    41. .p2:
    42.     move.w    d1,d0            ; restore water position
    43.     sub.w    (Camera_Y_pos_P2).w,d0    ; calculate distance between water surface and top of screen
    44.     asr.w    #1,d0            ; adjust to screen position
    46.     ; let's not have another interrupt too soon after the split
    47.     cmpi.w    #3,d0
    48.     bge.s    .notFullScreenP2
    49.     move.b    #1,(Water_fullscreen_flag_P2).w
    50.     bra.s    .setScreenBottomP2
    52. .notFullScreenP2:
    53.     cmpi.w    #$6F-4,d0    ; is water within 4 pixels of the bottom?
    54.     blo.s    .setWaterP2    ; if not, branch
    56. .setScreenBottomP2:
    57.     move.b    #$6F+4,(H_int_schedule_water+1).w    ; skip bottom screen water
    58.     bra.w    NonWaterEffects
    60. .setWaterP2:
    61.     ; prepare for bottom screen's water
    62.     addq.w    #4-1,d0        ; account for screen separator's height (4 px) and make counter
    63.     move.b    d0,(H_int_schedule_water+1).w    ; lines between split screen and bottom screen water
    64.     bra.w    NonWaterEffects
    65. ; End of function WaterEffects
    Switch out a RAM address in BuildSprites_P2:
    Code (Text):
    1. BuildSprites_P2:
    2. ;    tst.w    (Hint_flag).w    ; has H-int occured yet?        ;<--
    3.     tst.w    (Hint_split).w    ; has H-int occured yet?        ;<--
    4.     bne.s    BuildSprites_P2    ; if not, wait
    5.     lea    (Sprite_Table_2).w,a2
    To reiterate on the two bugs I'm aware of, Tails' screen will briefly use the above water palette if you move upwards from fully underwater to where the water surface is on screen, and Sonic's water palette change sometimes happens too late if the water surface is too close to the top of the screen.

    The latter happens, because Do_Updates is taking too long and delaying the H-Int that does the palette change. No idea what's causing the former. I suspect it's either in MoveWater2P or H_Int_splitscreen, but I can't find the problem.

    Anyway, here it is. Have fun.
    • Like Like x 1
    • Informative Informative x 1
    • List
  10. Overlord


    Aros gartref, diogelu'r GIG, achub bywydau Moderator
    Berkshire, England
    Learning Cymraeg
    With the obvious exceptions of Sky Chase and Death Egg, is it feasible to be at a point where every level in Sonic 2 is playable in 2P, now that the water problem has seemingly been worked around? That'd be really something.
  11. MarkeyJester


    ♡ ! Resident Jester
    If you can provide a source with your alterations above already applied, I will provide some assistance, the H-blank routines are way too unoptimise and I have a few tricks up my sleave.
  12. E-122-Psi


    It also depends on VRAM. As shown with the CPZ conversion, it is theoretically possible to convert the tiles to split screen, but to fit it all in, some sacrifices had to be made.

    It also depends on how well certain obstacles mesh with two screens. CPZ still has an issue where dying can send you into the auto spin tubes for some reason, while the oil in OOZ doesn't display.

    The water conversion is looking hopeful. kudos to MoDule, though I'll need to translate it to Xenowhirl (or move my ARZ edits to a GitHub disassembly) before I can use it myself. By the way, I keep hearing about freeing up RAM to add these variables, what are the usual hot spots you guys fiddle with to get more room to put them in? (keeping in mind I'm using a ROM that outside the level conversions is otherwise unaltered and has no new flags.)
    Last edited: Jul 17, 2020
  13. MoDule


    Tech Member
    Procrastinating from writing bug-fix guides
    I'd love to see those optimizations. How would I best go about sharing the source files?
  14. E-122-Psi


    Quick minor update, but I THINK that you can fix the issue with Rexon with the same trick as the airlifts, ie. set Long Distance and Remember State on. It should then only load once (though it won't respawn even after being destroyed now).

    Also advisory tip, add one blank tile to the end of Rexon's art (don't worry there's a blank area in the VRAM for it) so that his fireball renders properly in splitscreen.

    Last edited: Jul 20, 2020
  15. E-122-Psi


    Okay so confirmed you CAN just use the same trick as with the airlifts to fix Rexxon, activate the Remember State and Long Distance flags in SonLVL (weird, I was certain I tried that before).

    Rexxon can still make the level kinda laggy though so be warned.

    Also I've fixed a behaviour problem with Sol. Normally Sol only shoots his fireballs at Sonic, edit this line in Sol's routine to make him fire at Tails as well:

    Code (Text):
    1. loc_371DC:
    2.     move.w    (MainCharacter+x_pos).w,d0
    3.     sub.w    x_pos(a0),d0
    4.     bcc.s    loc_371E8
    5.     neg.w    d0
    7. loc_371E8:
    8.     cmpi.w    #$A0,d0
    9.     bcc.s    loc_371DC_Tails
    10.     move.w    (MainCharacter+y_pos).w,d0
    11.     sub.w    y_pos(a0),d0
    12.     bcc.s    loc_371FA
    13.     neg.w    d0
    15. loc_371FA:
    16.     cmpi.w    #$50,d0
    17.     bcc.s    loc_371DC_Tails
    18.     tst.w    (Debug_placement_mode).w
    19.     bne.s    loc_371DC_Tails
    20.     move.b    #1,anim(a0)
    22. loc_371DC_Tails:
    23.     tst.w    (Two_player_mode).w
    24.     beq.w    loc_3720C
    25.     move.w    (Sidekick+x_pos).w,d0
    26.     sub.w    x_pos(a0),d0
    27.     bcc.s    loc_371E8_Tails
    28.     neg.w    d0
    30. loc_371E8_Tails:
    31.     cmpi.w    #$A0,d0
    32.     bcc.s    loc_3720C
    33.     move.w    (Sidekick+y_pos).w,d0
    34.     sub.w    y_pos(a0),d0
    35.     bcc.s    loc_371FA_Tails
    36.     neg.w    d0
    38. loc_371FA_Tails:
    39.     cmpi.w    #$50,d0
    40.     bcc.s    loc_3720C
    41.     tst.w    (Debug_placement_mode).w
    42.     bne.s    loc_3720C
    43.     move.b    #1,anim(a0)
    45. loc_3720C:
    46.     bsr.w    JmpTo26_ObjectMove
    47.     lea    (off_372D2).l,a1
    48.     bsr.w    JmpTo25_AnimateSprite
    49.     andi.b    #3,mapping_frame(a0)
    50.     bra.w    JmpTo39_MarkObjGone
    51. ; ===========================================================================
    Here's a demonstration ROM with Rexxon and Sol fixed:

    Special thanx to D.A. Garden for bug finding.
    Last edited: Jul 20, 2020
  16. Black Squirrel

    Black Squirrel

    staying alert on the beach Wiki Sysop
    Northumberland, England
    quipu two metres apart
    Performance with Rexon on screen didn't seem too terrible to me, but maybe you can get better results by reducing its neck... balls from 4 to 3?
  17. E-122-Psi


    Well I'll leave it to people who use the tutorial to manage the objects. I suspect people who also ported the S3K priority/object managers might have a smoother experience.

    By the way, I think I've gotten CPZ as good as I can right now (I've even fixed some bugs like the autospin tubes sometimes cancelling out respawning and the Spiny's only aiming at Sonic).

    Besides the neccessary simplified art, the remaining issue right now is the background scrolling, which I can just not get running properly in split screen. I'm gonna have to ask if anyone here is experienced with editing the scroll routines. If I can get that working, I might try and manage this all into another tutorial.
  18. MoDule


    Tech Member
    Procrastinating from writing bug-fix guides
    For the backgrounds you're going to be limited to 512x512 pixels, due to the way the graphics and scrolling are set up. The background tiles are shared between the two player's screens, so we can't do any tile loading. This works for the 2P zones, because all their backgrounds fit within these constraints (MCZ is an exact fit). For the non-2P zones (except HTZ and OOZ) you're going to have to shrink the backgrounds down a little for them to fit. ARZ is going to be the biggest problem, since it's three times as tall as the limit.

    As for the scrolling itself, I've achieved some minor successes so far. CPZ and ARZ scroll correctly for the top screen. The bottom screens are still giving me trouble, but I'll get it eventually.

    In other news, MarkeyJester has been hard at work optimizing and fixing the H-Int routines. The bugs I mentioned earlier are now gone thanks to him. Currently he is working on optimizing the tile remapping for interlace mode compatibility.
  19. E-122-Psi


    Ah that makes sense, I noticed even with just the one screen active there's some cutoff with CPZ's background. And yeah, I'm still flummoxed how to make two player's screen load properly besides just linking to one of the existing routines.

    If you want, I can give you Zeta_Null's tile fixes for CPZ so you have a clearer view.

    And you guys are tile mapping ARZ as well? It sounds like you two are pretty much doing ARZ yourselves. :P
    Last edited: Jul 21, 2020
  20. MoDule


    Tech Member
    Procrastinating from writing bug-fix guides
    Roughly speaking, you need to run the zone's scrolling code twice, but with only half as many lines, each. First, you calculate your background camera position, then use that to determine how far each section of the background has moved. Then you need to take that information and turn it into values that you can send to the H-scroll table in the VDP.

    Most zones actually do this in a semi-standardized way, where they prepare an array of scroll values (TempArray_LayerDef), then run some code that takes a table of height values and compares them against the current camera position until the first visible background section is found. Then it just needs to fill in a screen's worth of scroll values that it takes from TempArray_LayerDef. MCZ does this, for instance, and it shows how to do it for two screens. CNZ also uses this system, but it messes up the bottom screen's scrolling. I'll post a fix for that some other time.

    As for which tileset Marky's working on right now, it is indeed ARZ, which I found out just now.