Dynamically Loading Ring Animation Frames into VRAM in Sonic 1

Discussion in 'Tutorials' started by Devon, Jul 18, 2023.

  1. Devon

    Devon DROWN, DROWN, DROWN MYSELF! Member

    Joined:
    Aug 26, 2013
    Messages:
    1,401
    Location:
    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:
    Nem_Ring:    binclude    "artnem/Rings.bin"
            even
    into
    Code:
    Art_Ring:    binclude    "artunc/Ring Frames.bin"
            even
    Nem_Sparkles:   binclude    "artnem/Ring Sparkles.bin"
            even
    Then change
    Code:
            if Revision=0
    Map_Ring:    include    "_maps/Rings.asm"
            else
    Map_Ring:        include    "_maps/Rings (JP1).asm"
            endif
    into
    Code:
    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:
                   plcm    Nem_Ring, $F640     ; rings
    into
    Code:
                   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:
                   move.b    (v_ani1_frame).w,obFrame(a0) ; set frame
    and this line:
    Code:
                   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:
                   move.w    #$27B2,obGfx(a1)
    to
    Code:
                   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:
    move.w    #$27B2,obGfx(a0)
    In "_anim/Rings.asm", change
    Code:
    .ring:        dc.b 5, 4, 5, 6, 7, afRoutine
    into
    Code:
    .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:
        dc.l Map_Ring+$4000000
        dc.w $27B2
        dc.l Map_Ring+$5000000
        dc.w $27B2
        dc.l Map_Ring+$6000000
        dc.w $27B2
        dc.l Map_Ring+$7000000
        dc.w $27B2
    to
    Code:
        dc.l Map_Ring+$1000000
        dc.w $27B2
        dc.l Map_Ring+$2000000
        dc.w $27B2
        dc.l Map_Ring+$3000000
        dc.w $27B2
        dc.l Map_Ring+$4000000
        dc.w $27B2
    This updates the ring sparkle frame IDs in the special stage.

    In sonic.asm, remove this line under loc_1B2C8:
    Code:
                   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:
    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:
    ; ---------------------------------------------------------------------------
    ; Queue ring frame graphics loading
    ; ---------------------------------------------------------------------------
    
    LoadRingFrame:
                    moveq   #0,d1                           ; Get ring frame offset for regular rings
                    move.b  (v_ani1_frame).w,d1
                    lsl.w   #7,d1                           ; Each ring frame takes $80 bytes, so multiply by $80
                    add.l   #Art_Ring,d1                    ; Queue a DMA transfer for this ring frame
                    move.w  #$F640,d2
                    move.w  #$80/2,d3
                    jsr     QueueDMATransfer
    
                    cmpi.b  #id_Special,(v_gamemode).w      ; Are we in a special stage?
                    beq.s   .noringloss                     ; If so, branch
            
                    moveq   #0,d1                           ; Get ring frame offset for lost rings
                    move.b  (v_ani3_frame).w,d1
                    lsl.w   #7,d1                           ; Each ring frame takes $80 bytes, so multiply by $80
                    add.l   #Art_Ring,d1                    ; Queue a DMA transfer for this ring frame
                    move.w  #$F640+$80,d2
                    move.w  #$80/2,d3
                    jmp     QueueDMATransfer
    
    .noringloss:
                    rts
    
    Now, go to SynchroAnimate. Under SyncEnd, replace the rts with
    Code:
            jmp    LoadRingFrame
    Then, go to Level_SkipTtlCard, and place this line under the label
    Code:
            jsr    LoadRingFrame
    Then, go to SS_WaitForDMA and place that same line under
    Code:
            moveq    #plcid_SpecialStage,d0
            bsr.w    QuickPLC    ; load special stage patterns
    Finally, go to SS_MainLoop, place that same line under
    Code:
            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:
            subq.b    #1,(v_ani2_time).w
            bpl.s    Sync4
            move.b    #7,(v_ani2_time).w
            addq.b    #1,(v_ani2_frame).w
            cmpi.b    #6,(v_ani2_frame).w
            blo.s    Sync4
            move.b    #0,(v_ani2_frame).w
    to
    Code:
            subq.b    #1,(v_ani2_time).w
            bpl.s    Sync4
            move.b    #7,(v_ani2_time).w
            addq.b    #1,(v_ani2_frame).w
            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:
            move.b    (v_ani1_frame).w,obFrame(a0)
    to
    Code:
            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:
    Sync2:
            subq.b    #1,(v_ani1_time).w
            bpl.s    Sync3
            move.b    #7,(v_ani1_time).w
            addq.b    #1,(v_ani1_frame).w
            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:
    Sync4:
            tst.b    (v_ani3_time).w
            beq.s    SyncEnd
            moveq    #0,d0
            move.b    (v_ani3_time).w,d0
            add.w    (v_ani3_buf).w,d0
            move.w    d0,(v_ani3_buf).w
            rol.w    #7,d0
            andi.w    #3,d0
            move.b    d0,(v_ani3_frame).w
            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:
            subq.b    #1,(v_ani1_time).w
            bpl.s    loc_1B2C8
            move.b    #7,(v_ani1_time).w
            addq.b    #1,(v_ani1_frame).w
            andi.b    #3,(v_ani1_frame).w
    
    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
    Stdh, Hame, giovanni.gen and 5 others like this.
  2. FerotheZeroFlag

    FerotheZeroFlag Just da motherfucking Great Teacher Onizuka! Member

    Joined:
    Jul 10, 2023
    Messages:
    36
    Thanks a lot, man! Finally I can put it on my hack. But another question is, where's the smooth giant ring?
     
  3. Devon

    Devon DROWN, DROWN, DROWN MYSELF! Member

    Joined:
    Aug 26, 2013
    Messages:
    1,401
    Location:
    your mom
    This guide isn't made for the giant ring, just the regular small rings.
     
  4. Speems

    Speems Well-Known Member Member

    Joined:
    Mar 14, 2017
    Messages:
    86
    Location:
    Rochester Hills, MI
    If imported, keep in mind that it'll break the ring's appearance in SonLVL as it calls for the nemesis file. I recommend editing the SonLVL Ring.cs file to refer to the uncompressed frame file with citing "uncompressed" in the compression type like so:
    [​IMG]
    And yes this in the S1 2005 files since it was the disassembly used for porting, but still works and should work for Github INI's.
     
  5. Dark Shamil Khan

    Dark Shamil Khan TASer lol Member

    Joined:
    Nov 7, 2021
    Messages:
    95
    Location:
    Pakistan
    This is interesting.. I did the tutorial right (with the optional part) and... The special stage ring is somehow now corrupted in its Flipped X frames. And gets back to normal afterwards.
    I am not sure what occurs or it has been mentioned in this tutorial or not. But that kind of felt strange as the special stage ring shouldn't be corrupted. By the way I'm using Hivebrain 2005. So uhh Yeah.
     
  6. Devon

    Devon DROWN, DROWN, DROWN MYSELF! Member

    Joined:
    Aug 26, 2013
    Messages:
    1,401
    Location:
    your mom
    I forgot to take that into account. My apologies. I added an extra step in the guide.
     
  7. RobiWanKenobi

    RobiWanKenobi Python Developer and ASM enthusiast Member

    Joined:
    Sep 10, 2022
    Messages:
    88
    Location:
    United States
    I don't hack Sonic 1, and that is why I am going to ask this question.

    What do I need to do to port dynamically loaded rings and smooth rings to sonic 2. I am pretty sure it'll be similar to sonic 1, but there are a lot of engine changes from sonic 1 to sonic 2.

    EDIT: I forgot to mention I have the s3k ring manager.
     
    Last edited: Jul 22, 2023
  8. Devon

    Devon DROWN, DROWN, DROWN MYSELF! Member

    Joined:
    Aug 26, 2013
    Messages:
    1,401
    Location:
    your mom
    The biggest thing I can think of is that the animation handler for the ring sparkle (when collecting a ring handled by the ring manager) is actually right at the first "-" temp symbol in RingsManager_Main. The animation starts in Touch_Rings when a value of $604 is set. The high byte is the animation timer and the low byte is the initial frame. If you set the mappings like you do in this guide, then you'll want to change the 4 to a 1, and then go to the animation handler, and change the frame ID check to check for 5 instead of 8. Then, in BuildRings, change the line that sets d1 to the contents of Rings_anim_frame to just set d1 to 0.

    Other than that, the changes are pretty much the same, minus the special stage and giant ring stuff. The VRAM location for the ring graphics IS different, so you're gonna have to take that into account when applying related fixes. You won't have to port over the DMA queue, because Sonic 2 already has it.
     
  9. RobiWanKenobi

    RobiWanKenobi Python Developer and ASM enthusiast Member

    Joined:
    Sep 10, 2022
    Messages:
    88
    Location:
    United States
    Thanks, I was also wondering what i would need to do to convert the files you included to sonic 2 format.

    For the art, I don't care if the colors are wonky since I have art to replace them with anyways.

    Finally, should I add aligns around the LoadRingFrame code to prevent address errors?

    EDIT: NVM, I got it working
     
    Last edited: Jul 23, 2023
  10. Devon

    Devon DROWN, DROWN, DROWN MYSELF! Member

    Joined:
    Aug 26, 2013
    Messages:
    1,401
    Location:
    your mom
    You shouldn't need to add any alignments around code, because every 68000 instruction's size is even. It's only when you have data that potentially can have an odd-numbered size that you'll have to worry about that.
     
  11. RobiWanKenobi

    RobiWanKenobi Python Developer and ASM enthusiast Member

    Joined:
    Sep 10, 2022
    Messages:
    88
    Location:
    United States
    I guess i set up mine differently because $604 worked just as intended.
     
  12. RobiWanKenobi

    RobiWanKenobi Python Developer and ASM enthusiast Member

    Joined:
    Sep 10, 2022
    Messages:
    88
    Location:
    United States
    Delete this post please, it was a double post because my internet lagged!
     
  13. Devon

    Devon DROWN, DROWN, DROWN MYSELF! Member

    Joined:
    Aug 26, 2013
    Messages:
    1,401
    Location:
    your mom
    I assume you didn't change the ring sprite mappings to my version, in which I made it only contain 1 ring frame instead of 4, since the other 3 are no longer needed. It's not really a requirement to have this system in place, per se, it's really just a small little optimization.
     
  14. RobiWanKenobi

    RobiWanKenobi Python Developer and ASM enthusiast Member

    Joined:
    Sep 10, 2022
    Messages:
    88
    Location:
    United States
    The reason I didn't use it is because in Sonic 2, it didn't actually work at all, so I just used the old s2 one.
     
  15. Devon

    Devon DROWN, DROWN, DROWN MYSELF! Member

    Joined:
    Aug 26, 2013
    Messages:
    1,401
    Location:
    your mom
    You have to convert them to Sonic 2's mappings format. SonMapEd or MappingsConverter can do that for you.
     
  16. Devon

    Devon DROWN, DROWN, DROWN MYSELF! Member

    Joined:
    Aug 26, 2013
    Messages:
    1,401
    Location:
    your mom
    EDIT: website broek
     
  17. RobiWanKenobi

    RobiWanKenobi Python Developer and ASM enthusiast Member

    Joined:
    Sep 10, 2022
    Messages:
    88
    Location:
    United States
    They actually dont open in sonmaped, even with s1 format on.
     
  18. Devon

    Devon DROWN, DROWN, DROWN MYSELF! Member

    Joined:
    Aug 26, 2013
    Messages:
    1,401
    Location:
    your mom
    Download them again. Turned out the ring frame label didn't have a colon after it, which threw off SonMapEd's mappings parser.
     
  19. RobiWanKenobi

    RobiWanKenobi Python Developer and ASM enthusiast Member

    Joined:
    Sep 10, 2022
    Messages:
    88
    Location:
    United States
    For anyone porting this to Sonic 2, to avoid VRAM issues with tails, follow these steps

    in s2.constants.asm, replace ArtTile_ArtNem_Ring with
    Code:
    ArtTile_ArtUnc_Ring              =    $06BC
    ArtTile_ArtNem_Ring             = ArtTile_ArtUnc_Ring ;For not having to edit tons of code
    ArtTile_ArtNem_RingSparkle            = $06C6
    
    for LoadRingFrame
    Code:
        align 4
    ; ---------------------------------------------------------------------------
    ; Queue ring frame graphics loading
    ; ---------------------------------------------------------------------------
    
    LoadRingFrame:
                    moveq   #0,d1                           ; Get ring frame offset for regular rings
                    move.b  (Rings_anim_frame).w,d1
                    lsl.w   #7,d1                           ; Each ring frame takes $80 bytes, so multiply by $80
                    add.l   #Art_Ring,d1                    ; Queue a DMA transfer for this ring frame
                    move.w  #$D780,d2
                    move.w  #$80/2,d3
                    jsr     QueueDMATransfer
    
                    moveq   #0,d1                           ; Get ring frame offset for lost rings
                    move.b  (Ring_spill_anim_frame).w,d1
                    lsl.w   #7,d1                           ; Each ring frame takes $80 bytes, so multiply by $80
                    add.l   #Art_Ring,d1                    ; Queue a DMA transfer for this ring frame
                    move.w  #$D780+$80,d2
                    move.w  #$80/2,d3
                    jmp     QueueDMATransfer
    
    .noringloss:
                    rts
       align 4
    
    And finally in PlrList_Std1
    replace
    Code:
        plreq ArtTile_ArtNem_Ring, ArtNem_Ring
    
    
    with
    Code:
        plreq ArtTile_ArtNem_RingSparkle, ArtNem_Sparkle
    
     
  20. BenjaminTheRaccoon

    BenjaminTheRaccoon I'm a Raccoon! The Raccooniest Raccoon to Raccoon! Member

    Joined:
    Jun 20, 2020
    Messages:
    15
    Location:
    The Dumpster Outside the Maccas
    Hello, I'm having an issue with this guide and would love knowing how I would fix the following bugs.
    Starting with... This twofer.
    None of this is right.png
    The sparkles show broken when I collect Rings, but are fine in VRAM, and the Rings aren't updating when they should.
    (Edit: The Rings only spin when the player is hurt, for some reason?)

    What's even weirder is that when I'm leaving the title screen, the audio jolts up in volume for until the fadeout is finished... What's going on?