don't click here

Dynamically Loading Ring Animation Frames into VRAM in Sonic 1

Discussion in 'Engineering & Reverse Engineering' started by Devon, Jul 18, 2023.

  1. Devon

    Devon

    A̸ ̴S̴ ̵C̵ ̷E̶ ̸N̸ ̴D̶ ̵E̶ ̸D̶ Tech Member
    1,262
    1,433
    93
    your mom
    In the original Genesis games, every ring and sparkle animation is loaded into VRAM at once. While it's serviceable enough, this does make it hard to implement a smoother animation with extra frames.

    However, you might realize that each ring is synchronized with each other. Each stage ring uses the same animation frame, and even the lost rings that use a separate animation handler are still synchronized with each other. So, how about we just allocated 2 spaces in VRAM, one space to load the animation frame for the stage rings, and the other for lost rings? That's what this guide is going to help you out with.

    You will need to have the DMA queue ported over, because that will make this whole thing a lot simpler. Part 3 of the spindash porting guide will have you covered.

    Step 1: Preparing the graphics

    For this to work, the ring graphics will need to be decompressed, because the DMA queue directly loads the graphics into VRAM. We also aren't going to do the treatment for the sparkle frames, because those are not synchronized.

    So, what is needed to be done is separate the ring frames from the sparkle frames, with the ring frames being decompressed, and the sparkles still compressed in Nemesis. The ring frames will also need to be reformatted in a way that will fit this new system. The idea will be to allocate 4 tiles, and have a single sprite frame display it, and just have each frame set up to fit in it.

    I went and ahead and did this. Put "Ring Frames.bin" in the artunc folder, put "Ring Sparkles.bin" in the artnem folder, and replace "Rings.asm" in the _maps folder. Delete "artnem/Rings.bin", since that's being replaced. Also delete "_maps/Rings (JP1).asm", because that will no longer be used.


    Step 2: Adding the graphics into your disassembly

    In sonic.asm, change
    Code (Text):
    1. Nem_Ring:    binclude    "artnem/Rings.bin"
    2.         even

    into
    Code (Text):
    1. Art_Ring:    binclude    "artunc/Ring Frames.bin"
    2.         even
    3. Nem_Sparkles:   binclude    "artnem/Ring Sparkles.bin"
    4.         even

    Then change
    Code (Text):
    1.         if Revision=0
    2. Map_Ring:    include    "_maps/Rings.asm"
    3.         else
    4. Map_Ring:        include    "_maps/Rings (JP1).asm"
    5.         endif

    into
    Code (Text):
    1. Map_Ring:    include    "_maps/Rings.asm"

    Step 3: Fixing the PLCs and frame IDs

    With the ring frames separated from the sparkles, we must now fix up the PLCs to offset the sparkles into the correct place. The original ring frames took up $140 bytes, so that's the amount we will add to the VRAM address in the PLCs.

    Go into "_inc/Pattern Load Cues.asm", and change
    Code (Text):
    1.                plcm    Nem_Ring, $F640     ; rings
    into
    Code (Text):
    1.                plcm    Nem_Sparkles, $F780     ; ring sparkles

    Now, because the mappings have been reduced to only have 1 frame that displays dynamically changing graphics, we'll need to fix up some frame IDs.

    In "_incObj/25 & 37 Rings.asm", remove this line:
    Code (Text):
    1.                move.b    (v_ani1_frame).w,obFrame(a0) ; set frame
    and this line:
    Code (Text):
    1.                move.b    (v_ani3_frame).w,obFrame(a0)

    Those lines set the frame ID for the rings. Again, since we are only using 1 frame to display a changing graphic, we won't be using that. We will use v_ani1_frame and v_ani3_frame for the ring frame loading later.

    In the same file, under .makerings in the RingLoss object, change
    Code (Text):
    1.                move.w    #$27B2,obGfx(a1)
    to
    Code (Text):
    1.                move.w    #$27B6,obGfx(a1)

    The lost rings will use a different area of VRAM, since they are not synced with the other stage rings, so the tile ID is pushed down 4 tiles. However, this will need to be set back when it turns into a sparkle, so under
    RLoss_Sparkle, add this line:
    Code (Text):
    1. move.w    #$27B2,obGfx(a0)

    In "_anim/Rings.asm", change
    Code (Text):
    1. .ring:        dc.b 5, 4, 5, 6, 7, afRoutine
    into
    Code (Text):
    1. .ring:        dc.b 5, 1, 2, 3, 4, afRoutine

    This changes the ring sparkle animation to use the updated frame IDs.

    In "_inc/Special Stage Mappings & VRAM Pointers.asm", change
    Code (Text):
    1.     dc.l Map_Ring+$4000000
    2.     dc.w $27B2
    3.     dc.l Map_Ring+$5000000
    4.     dc.w $27B2
    5.     dc.l Map_Ring+$6000000
    6.     dc.w $27B2
    7.     dc.l Map_Ring+$7000000
    8.     dc.w $27B2
    to
    Code (Text):
    1.     dc.l Map_Ring+$1000000
    2.     dc.w $27B2
    3.     dc.l Map_Ring+$2000000
    4.     dc.w $27B2
    5.     dc.l Map_Ring+$3000000
    6.     dc.w $27B2
    7.     dc.l Map_Ring+$4000000
    8.     dc.w $27B2

    This updates the ring sparkle frame IDs in the special stage.

    In sonic.asm, remove this line under loc_1B2C8:
    Code (Text):
    1.                move.b    (v_ani1_frame).w,$1D0(a1)

    This line set the frame ID for the rings, much like with the 2 removed lines in the regular ring objects. We will use v_ani1_frame for the ring frame loading later.

    Finally, if you are using REV01, in _inc/DebugList.asm, go to .Ending and change the 8 to a 5 in this line
    Code (Text):
    1. dbug    Map_Ring,    id_Rings,    0,    8,    $27B2

    This fixes the frame ID used in the ending debug object list in REV01.

    Step 4: Loading the ring frame graphics into VRAM

    Now comes the part where a ring frame is chosen to be loaded into VRAM. Add this function into your disassembly somewhere:
    Code (Text):
    1.  
    2. ; ---------------------------------------------------------------------------
    3. ; Queue ring frame graphics loading
    4. ; ---------------------------------------------------------------------------
    5.  
    6. LoadRingFrame:
    7.                 moveq   #0,d1                           ; Get ring frame offset for regular rings
    8.                 move.b  (v_ani1_frame).w,d1
    9.                 lsl.w   #7,d1                           ; Each ring frame takes $80 bytes, so multiply by $80
    10.                 add.l   #Art_Ring,d1                    ; Queue a DMA transfer for this ring frame
    11.                 move.w  #$F640,d2
    12.                 move.w  #$80/2,d3
    13.                 jsr     QueueDMATransfer
    14.  
    15.                 cmpi.b  #id_Special,(v_gamemode).w      ; Are we in a special stage?
    16.                 beq.s   .noringloss                     ; If so, branch
    17.          
    18.                 moveq   #0,d1                           ; Get ring frame offset for lost rings
    19.                 move.b  (v_ani3_frame).w,d1
    20.                 lsl.w   #7,d1                           ; Each ring frame takes $80 bytes, so multiply by $80
    21.                 add.l   #Art_Ring,d1                    ; Queue a DMA transfer for this ring frame
    22.                 move.w  #$F640+$80,d2
    23.                 move.w  #$80/2,d3
    24.                 jmp     QueueDMATransfer
    25.  
    26. .noringloss:
    27.                 rts
    28.  

    Now, go to SynchroAnimate. Under SyncEnd, replace the rts with
    Code (Text):
    1.         jmp    LoadRingFrame

    Then, go to Level_SkipTtlCard, and place this line under the label
    Code (Text):
    1.         jsr    LoadRingFrame

    Then, go to SS_WaitForDMA and place that same line under
    Code (Text):
    1.         moveq    #plcid_SpecialStage,d0
    2.         bsr.w    QuickPLC    ; load special stage patterns

    Finally, go to SS_MainLoop, place that same line under
    Code (Text):
    1.         jsr    (SS_ShowLayout).l

    And, tada! You now have implemented a more dynamic system for ring animations.

    Step 5: Fixing the giant rings

    Now, there's one last issue to address. The giant rings at the end of the stage when you have 50 or more rings is affected by these changes. That's because it uses the same animation as the regular rings. Luckily, there is an unused set of animation variables we can use to circumvent this.

    Go to Sync3 under SynchroAnimate and change
    Code (Text):
    1.         subq.b    #1,(v_ani2_time).w
    2.         bpl.s    Sync4
    3.         move.b    #7,(v_ani2_time).w
    4.         addq.b    #1,(v_ani2_frame).w
    5.         cmpi.b    #6,(v_ani2_frame).w
    6.         blo.s    Sync4
    7.         move.b    #0,(v_ani2_frame).w

    to
    Code (Text):
    1.         subq.b    #1,(v_ani2_time).w
    2.         bpl.s    Sync4
    3.         move.b    #7,(v_ani2_time).w
    4.         addq.b    #1,(v_ani2_frame).w
    5.         andi.b    #3,(v_ani2_frame).w

    Which is basically the original ring animation code, but applied to the unused variables, instead.

    Now, go to GRing_Animate in "_incObj/4B Giant Ring.asm" and change
    Code (Text):
    1.         move.b    (v_ani1_frame).w,obFrame(a0)

    to
    Code (Text):
    1.         move.b    (v_ani2_frame).w,obFrame(a0)

    This makes the giant ring object actually use the new frame variable.

    Step 6: (OPTIONAL) Implementing a smoother ring animation

    Now that we have this system, we can easily add in additional frames of animation, since only the shown frame of animation is loaded into VRAM at once, therefore completely eliminating the risk of eating up VRAM.

    First, download this new set of ring frames that include inbetween frames from the 2013 remake of Sonic 1 and replace the old set in the artunc folder.

    Now, we need to update the animation handlers to make use of the extra frames.

    First, let's go to SynchroAnimate. First, let's update the handler for regular rings. The code for that is right here:
    Code (Text):
    1. Sync2:
    2.         subq.b    #1,(v_ani1_time).w
    3.         bpl.s    Sync3
    4.         move.b    #7,(v_ani1_time).w
    5.         addq.b    #1,(v_ani1_frame).w
    6.         andi.b    #3,(v_ani1_frame).w

    First, let's change v_ani1_time to reset to 3 instead of 7. This will halve the duration that a frame is displayed, which is needed with the doubled set of frames. Then, change the value of the "andi" instruction from 3 to 7. This change will add an extra bit in the AND mask, limiting it to bits 0-2 instead of 0-1. Basically, it'll limit the bits to use values 0-7, which exactly fits with the new set of frames.

    For the lost rings, there's this:
    Code (Text):
    1. Sync4:
    2.         tst.b    (v_ani3_time).w
    3.         beq.s    SyncEnd
    4.         moveq    #0,d0
    5.         move.b    (v_ani3_time).w,d0
    6.         add.w    (v_ani3_buf).w,d0
    7.         move.w    d0,(v_ani3_buf).w
    8.         rol.w    #7,d0
    9.         andi.w    #3,d0
    10.         move.b    d0,(v_ani3_frame).w
    11.         subq.b    #1,(v_ani3_time).w

    This one is bit more complicated, since it's made to slow down the ring animation over time. Let's start with the simple change. There's another "andi" instruction. Like with the previous change, change the 3 to a 7. Now, to make it so that it doesn't animate as slowly, change the "rol" instruction value from 7 to 8.

    The math involved in slowing down the ring animation is that there's a counter (v_ani3_time) that ticks from 255 to 0. As it ticks down, it adds itself into another value (v_ani3_buf). The result of the addition is used to calculate the frame ID to display. The lower the counter is, the lesser the additions, and thus the slower the animation. Originally, it used bits 9 and 10 (which the rol instruction is used to rotate them to the bottom) as the ring frame ID. With the change, it basically adds bit 8 into the mix, which will change more often than the other 2 bits. This is exactly how it halves the frame duration.

    Now, the special stage rings. Head on over to SS_AniWallsRings and you'll see:
    Code (Text):
    1.         subq.b    #1,(v_ani1_time).w
    2.         bpl.s    loc_1B2C8
    3.         move.b    #7,(v_ani1_time).w
    4.         addq.b    #1,(v_ani1_frame).w
    5.         andi.b    #3,(v_ani1_frame).w
    6.  
    7. loc_1B2C8:

    Looks familiar? It's the same code as the animation handler for the regular stage rings. So, apply the same changes here.

    And, tada, you should have a smoother ring animation!
     
    Last edited: Jul 22, 2023
    • Like Like x 7
    • Useful Useful x 1
    • List
  2. Rrose80149

    Rrose80149

    Member
    90
    19
    8
    Will this work on Hivebrain 2005?
     
  3. Devon

    Devon

    A̸ ̴S̴ ̵C̵ ̷E̶ ̸N̸ ̴D̶ ̵E̶ ̸D̶ Tech Member
    1,262
    1,433
    93
    your mom
    Yes, if you can track the equivalent pieces of code and data.
     
  4. Brainulator

    Brainulator

    Regular garden-variety member Member
    Interesting. I know Sonic Delta did this, so seeing this is cool.

    I think frame 8 of the old frame definitions is used by debug mode in Sonic 1 REV01, so that should be corrected somehow. It's blank, so I think you could just use frame 0 of Sonic's mappings instead.
     
  5. Devon

    Devon

    A̸ ̴S̴ ̵C̵ ̷E̶ ̸N̸ ̴D̶ ̵E̶ ̸D̶ Tech Member
    1,262
    1,433
    93
    your mom
    Right, though it's used in the ending. Regardless, go to .Ending in _inc/DebugList.asm and change the 8 to a 5 in this line
    Code (Text):
    1. dbug    Map_Ring,    id_Rings,    0,    8,    $27B2
     
    Last edited: Jul 19, 2023
  6. Devon

    Devon

    A̸ ̴S̴ ̵C̵ ̷E̶ ̸N̸ ̴D̶ ̵E̶ ̸D̶ Tech Member
    1,262
    1,433
    93
    your mom
    Added an extra step, because I forgot to account for the giant rings using the same animation handler as the regular rings.