Basic VSRAM Guide (Example for Sonic 1 Included w/ 2005 & SVN labels)

Discussion in 'Tutorials' started by Carlos Iagnecz, Jan 11, 2025.

  1. Carlos Iagnecz

    Carlos Iagnecz Newcomer Member

    Joined:
    Jun 16, 2023
    Messages:
    21
    Location:
    Brazil
    (All information is from plutiedev.com, visit there for more information)

    1. What is VSRAM
    VSRAM
    means Vertical Scroll Random Acess Memory and is used to scroll Plane A and B with two different configurations:
    Whole Screen Mode (abreviated to WSM for this guide) and 2 Collumn Row Mode (abreviated to 2CRM for this guide), pretty self explanatory. The format is $80 bytes with Plane A and B alternating values sized in words.

    2. VDP Commands
    To use such memory we first need to know how VDP commands work to write/read/DMA to memory and set the modes:
    VDP commands are written to the VDP control register ($C00004), the format varies depending on what we are doing.

    For setting modes simply write a word with the higher byte indicating the register and the lower indicating configuration, the one we are looking for is $8B:
    Bit #2 (counting from zero) is the VSRAM mode, it is normally clear as it's WSM and when set it's 2CRM.

    For writing,DMA operations and reading to any video memory, you need to write a long, however the target memory's bit arrangement is all over the place. As bits #13-10 are placed in the high word, bits #15-14 are placed in the first two lower bits.
    Here is a assembly expression to simplify the process:
    Code:
    (command+((location&$3FFF)<<16)+((location&$C000)>>14))
    Now the command depends on the memory and action:
    • Read from VSRAM: $00000010
    • Write to VSRAM: $40000010
    • DMA to VSRAM: $40000090*
      *(for DMA fill it will wait until the fill value is written to the VDP data register)
    To actually use these values move from/to the VDP data register ($C00000) when reading/writing respectively, the location will automatically increment by the size you used. (byte,word,long)

    3. Example in Sonic 1: Vertical Waves Underwater in Labyrinth
    First we need to enable the mode exclusively in LZ, so in 'Level_ClrVars3' (it's the same label for all public disassemblies) we can see that the VDP modes are set here for all levels and HBlank is enabled for LZ:
    Code:
        Level_ClrVars3:
            move.l    d0,(a1)+
            dbf    d1,Level_ClrVars3 ; clear object variables
    
            disable_ints
            bsr.w    ClearScreen
            lea    (vdp_control_port).l,a6
            move.w    #$8B03,(a6)    ; line scroll mode
            move.w    #$8200+(vram_fg>>10),(a6) ; set foreground nametable address
            move.w    #$8400+(vram_bg>>13),(a6) ; set background nametable address
            move.w    #$8500+(vram_sprites>>9),(a6) ; set sprite table address
            move.w    #$9001,(a6)        ; 64-cell hscroll size
            move.w    #$8004,(a6)        ; 8-colour mode
            move.w    #$8720,(a6)        ; set background colour (line 3; colour 0)
            move.w    #$8A00+223,(v_hbla_hreg).w ; set palette change position (for water)
            move.w    (v_hbla_hreg).w,(a6)
            cmpi.b    #id_LZ,(v_zone).w ; is level LZ?
            bne.s    Level_LoadPal    ; if not, branch
    
            move.w    #$8014,(a6)    ; enable H-interrupts
    
    The line commented as 'line scroll mode' contains the configuration for WSM so duplicate it to after the Labyrinth check and add #4 (bit 2^2=4) to its copy.
    Code:
            cmpi.b    #id_LZ,(v_zone).w ; is level LZ?
            bne.s    Level_LoadPal    ; if not, branch
    
            move.w    #$8014,(a6)    ; enable H-interrupts
            move.w    #$8B07,(a6)    ; enable row based y scroll
    
    However loading Labyrinth now causes only the first row to scroll:
    imagem_2025-01-11_172629338.png
    To fix that, head to VBlank (loc_B10 for Hivebrain 2005).
    Code:
            move.w    (vdp_control_port).l,d0
            move.l    #$40000010,(vdp_control_port).l
            move.l    (v_scrposy_vdp).w,(vdp_data_port).l ; send screen y-axis pos. to VSRAM
    
    It needs to be written ($80/4) times to the data port
    (the division is there because we are sending longs) now since we have way more entries, exclusively in LZ of course:
    Code:
            move.w    (vdp_control_port).l,d0
            move.l    #$40000010,(vdp_control_port).l ; go to $0 in VSRAM
    
            tst.b    (f_wtr_state).w    ; is water above top of screen?
            bne.s    @waterabove     ; if yes, branch
          
            move.w    #1,d1
            cmpi.b    #id_LZ,(v_zone).w ; is level LZ ?
            bne.s    @loop2
          
            move.w    #($80/4)-1,d1
        @loop2:
            move.l    d0,(vdp_data_port) ; send screen y-axis pos. to VSRAM
            dbf.w    d1,@loop2
          
        @waterabove:
    
    Now like horizontal scrolling, we need somewhere to store the y scroll data. Therefore allocate a place in RAM with $80 bytes, in this guide it is called 'v_vscrolltablebuffer'.

    We need to write to that buffer before transfering it to VSRAM when underwater so change 'Deform_LZ' to contain this instead of the rts (change the first to a bra.s LZ_Vertical)
    Code:
    LZ_Vertical:
        ; vertical scroll table
       
            lea    (v_vscrolltablebuffer).w,a1
            move.w    #($80/4)-1,d1
            move.w    (v_screenposy).w,d5
            move.w    (v_bgscreenposy).w,d6
        LZ_VerticalLoop:
            move.b    (a3,d3),d4    ; load deformation offset
            ext.w    d4
            add.w    d5,d4        ; add fg y-position
            move.w    d4,(a1)+    ; write to hscroll table
           
            move.b    (a2,d2),d4    ; load deformation offset
            ext.w    d4
            add.w    d6,d4        ; add bg y-position
            move.w    d4,(a1)+    ; write to hscroll table
           
            addq.b    #1,d2        ; advance fg & bg y positions
            addq.b    #1,d3        ; for deformation offset table
            dbf    d1,LZ_VerticalLoop
            rts
    
    Now we only need to transfer it on 'HBlank' (PalToCRAM in Hivebrain 2005), however adding code makes a branch invalid, therefore change all the move.l (a0)+,(a1) to a single:
    Code:
            move.l    d1,-(sp)
            move.w    #32-1,d1
        HB_loop1:
            move.l    (a0)+,(a1)    ; move palette to CRAM
            dbf.w    d1,HB_loop1
    
            move.l    (sp)+,d1
    
    And now to add the actual transfer code, right before the last line we just added:
    Code:
            move.l    #$40000010,4(a1)
            lea    (v_vscrolltablebuffer).w,a0 ; get buffer from RAM
            move.w    #($80/4)-1,d1
           
        HB_loop2:
            move.l    (a0)+,(a1) ; send screen y-axis pos. to VSRAM
            dbf.w    d1,HB_loop2
    
    That should be all, however...
    imagem_2025-01-11_182215804.png

    The sprites don't follow the distortion. To fix this head to 'BuildSpr_Draw' (sub_D750 for Hivebrain 2005) and add these lines after the label:
    Code:
            cmpi.b    #id_LZ,(v_zone).w
            bne.s    buildspr_no_ripple
           
            move.w    obY(a0),d0
            sub.w    (v_waterpos1).w,d0
            bmi.s    buildspr_no_ripple
            lea        (v_vscrolltablebuffer),a3
            move.w    d3,d0
            lsr.w    #2,d0 ; 4 * x / 16 = x / 4
            add.w    #96,d0
            andi.w    #$7F-3,d0
           
            sub.w    (a3,d0.w),d2
            add.w    (v_scrposy_vdp),d2
        buildspr_no_ripple:
            movea.w    obGfx(a0),a3
    
    

    (this won't affect the HUD as it uses obScreenY and not obY)
    imagem_2025-01-11_183414515.png
    Congratulations, you have added vertical waves to Sonic 1 :D
     
  2. Red2010 is now

    Red2010 is now A Normal RomHacker with occupations. Member

    Joined:
    Apr 24, 2023
    Messages:
    79
    Location:
    Somewhere in Spain
    In less than 24 hours this is already pinned on the forum

    Fun Fact: This guide was born out of some testing in Sonic Encore. Don't ask how Carlos got to this but I guess it's worth mentioning
     
    Carlos Iagnecz likes this.
  3. Malachi

    Malachi Bibblemaxxing Member

    Joined:
    Oct 16, 2022
    Messages:
    25
    Location:
    Australia
    To anyone experimenting with this, keep in mind that on hardware and some accurate emulators, that the leftmost row is prone to bugging out with vertical scrolling and horizontal scrolling both being used. Many games account for this with covering up that area for a sprite. You only need to cover the first 16 pixels, since it corrupts the first row of vscroll and each row is 16 pixels wide
    Due to blastem actually having a bug with vscroll emulation for a while, I've written an object that does just that for S3K. Maybe you guys can find some use out of it:
    Code:
    Obj_SSZ2Border:
    ; load 16 black tiles
    ; SIDENOTE: don't load tiles like this, especially for a water stage.
    ; Interrupts can easily get in the way
        lea    (VDP_data_port).l,a6
        locVRAM    tiles_to_bytes(ArtTile_SSZ2_Border)
        move.l    #$FFFFFFFF,d0
        moveq    #(16*(8/2))-1,d1
    -    move.l    d0,(a6)
        dbf    d1,-
    
        move.b    #ren_screenpos_mask,render_flags(a0)    ; base position on screen
        move.b    #256/2,height_pixels(a0)
        move.b    #256/2,width_pixels(a0)
        move.w    #make_priority(0),priority(a0)
        move.w    #make_art_tile(ArtTile_SSZ2_Border,0,1),art_tile(a0)
        move.l    #Map_SSZ2Border,mappings(a0)
        move.w    #(16/2)+128,x_pos(a0)
        move.w    #(224/2)+128,y_pos(a0)
        clr.b    (_unkFAA9).w        ; make sure this gets cleared
        move.l    #.main,address(a0)
    .main:
        tst.b    (_unkFAA9).w        ; has the island started falling? (Vscroll is disabled at this point)
        beq.s    .render            ; if not, render
        subi.w    #1,x_pos(a0)        ; move offscreen
        cmpi.w    #128-(16/2),x_pos(a0)
        ble.s    .delete            ; if offscreen, delete
    .render:
        jmp    (Draw_Sprite).w
    .delete:
        jmp    (Delete_Current_Sprite).w
    
     
  4. Carlos Iagnecz

    Carlos Iagnecz Newcomer Member

    Joined:
    Jun 16, 2023
    Messages:
    21
    Location:
    Brazil
    Looking back at this, there is a way to hide the leftmost row with a vdp command:
    $80xx has bit 5 dedicated to disabling the leftmost row. So just change the HBlank enable command in Level_ClrVars3 to:
    Code:
    move.w    #$8034,(a6)    ; enable H-interrupts
    
     
    PeanutNoceda likes this.
  5. Malachi

    Malachi Bibblemaxxing Member

    Joined:
    Oct 16, 2022
    Messages:
    25
    Location:
    Australia
    That vdp command hides the leftmost row of tiles (8 pixels), whereas vertical scroll effects two tiles (16 pixels, technically 15 due to the way the junk data appears). With some scrolling values it does properly mask the bugged scrolling, but not for others. On the bright side, inaccurate emulation that doesn't emulate the vscroll junk scrolling, is also unlikely to emulate the leftmost row mask, so it kinda enhances the experience on them in a way