don't click here

Sonic CD Quirks (Sega CD Version)

Discussion in 'Engineering & Reverse Engineering' started by Devon, Jul 11, 2022.

  1. Devon

    Devon

    There's nothing left but faith Tech Member
    770
    451
    63
    Gonna make a quick point in here to say that the time warp cutscene sequence doesn't actually play while the next time zone is loading; the next time zone loads AFTER the cutscene is done playing (as soon as it fades to white, not when the sound is done playing; Z80 runs independently. This also explains why it still takes some time to transition after). I imagine that the cutscene was still made to make it appear less awkward, maybe. It also explains why the CDDA variant of the warp sound was able to play in prototype builds, since the CD drive cannot read other data and stream CD audio at the same time.

    Here's the supporting code:
    [​IMG]

    "RunMMD" loads and executes a Main CPU program file. It doesn't exit out until said program is finished running.

    Finally, a quick visualization of the memory, and you can clearly see it only start to load the next time zone after the cutscene fades to white:
     
    Last edited: Aug 21, 2022
    • Informative Informative x 4
    • List
  2. Xiao Hayes

    Xiao Hayes

    Classic Eggman art Member
  3. Devon

    Devon

    There's nothing left but faith Tech Member
    770
    451
    63
    Came across this little visual bug while combing through the special stage code. So, when you turn in the special stages, Sonic's sprite tilts towards the direction he's turning, and it's gradual. Well, the developers intended to make the untilting gradual as well, but a couple of bad branches make it so that he snaps back to the normal sprite instantly.

    Bugged:
    [​IMG]

    Intended behavior:
    [​IMG]

    This is the code that's responsible:
    Code (Text):
    1.  
    2. .Untilt:
    3.     ; BUG: This function is bugged. It intends to gradually untilt Sonic's sprite,
    4.     ; but with the way the branches are set up, it makes it instant instead
    5.     cmpi.b    #5,oPlayerTilt(a0)     ; Are we tilting left?
    6.     bcs.s     .UntiltLeft            ; If so, branch
    7.  
    8. .UntiltRight:
    9.     subq.b    #1,oPlayerTilt(a0)     ; Untilt from the right
    10.     cmpi.b    #5,oPlayerTilt(a0)     ; Are we in the center?
    11.     bcc.s     .UntiltLeft            ; If not, branch
    12.     move.b    #5,oPlayerTilt(a0)     ; Cap at center
    13.     rts
    14.  
    15. .UntiltLeft:
    16.     addq.b    #1,oPlayerTilt(a0)     ; Untilt from the left
    17.     cmpi.b    #5,oPlayerTilt(a0)     ; Are we in the center?
    18.     bls.s     .UntiltLeft            ; If not, branch
    19.     move.b    #5,oPlayerTilt(a0)     ; Cap at center
    20.     rts
    21.  

    As you can see, the branches for when Sonic hasn't untilted all the way back to the center go to .UntiltLeft. When untilting from the right, it then performs a check if Sonic is right of the center, and make him snap to the center in that case. When untilting from the left, it ends up in a loop until the tilt value is at the center. Changing both branches to go to an RTS instruction instead gets you the intended behavior.

    Another thing I noticed was that the water slows you down in time attack mode, and only in time attack mode:
    Code (Text):
    1. ObjSonic_Water:
    2.     tst.b    timeStopped                ; Is time stopped?
    3.     bne.s    .End                       ; If so, branch
    4.  
    5.     move.b   #8,splashObject+oID        ; Spawn splash object
    6.     btst     #1,specStageFlags.w        ; Are we in time attack mode?
    7.     beq.s    .End                       ; If not, branch
    8.     move.w   #$500,oPlayerTopSpeed(a0)  ; If so, slow Sonic down
    9.  
    10. .End:
    11.     rts

    Finally, there's this unreferenced mode for Sonic's object code that makes him sink down out of the special stage indefinitely.
    Code (Text):
    1. ; -------------------------------------------------------------------------
    2. ; Unknown mode
    3. ; -------------------------------------------------------------------------
    4.  
    5. ObjSonic_Unk:
    6.     addq.w    #4,oSprY(a0)               ; Move sprite down
    7.     cmpi.w    #320+128,oSprY(a0)         ; Has it moved offscreen?
    8.     bcs.s     .End                       ; If not, branch
    9.     move.b    #6,oRoutine(a0)            ; Advance routine
    10.  
    11. .End:
    12.     rts
    13.  
    14. ; -------------------------------------------------------------------------
    15.  
    16. ObjSonic_Unk2:
    17.     rts

    [​IMG]
     
    Last edited: Aug 27, 2022
    • Informative Informative x 3
    • Like Like x 1
    • List
  4. Xiao Hayes

    Xiao Hayes

    Classic Eggman art Member
    Sorry, I see the difference, but I don't get why the second one's better. What I see is Sonic moving his head to the opposite side before looking to the right direction, and he also does after when fixed. In both cases, that doesn't look right to me.
     
  5. Devon

    Devon

    There's nothing left but faith Tech Member
    770
    451
    63
    Well, I never claimed it was "better", I was saying that's what the developers wanted to do initially.
     
  6. Clownacy

    Clownacy

    Tech Member
    915
    224
    43
    I wonder if this has anything to do with those unused 'shrugging' sprites.
     
  7. Devon

    Devon

    There's nothing left but faith Tech Member
    770
    451
    63
    See this post I made just some moments after finding that code lol.
     
  8. Xiao Hayes

    Xiao Hayes

    Classic Eggman art Member
    I supposed so, my question would have gone to the team that made it. XD
     
  9. Devon

    Devon

    There's nothing left but faith Tech Member
    770
    451
    63
    This... is a new one to me. I don't think I've seen a button tap check for a basic menu ever be implemented like this until now.

    I found this in the Visual Mode menu code:
    Code (Text):
    1. ROM:00FF20E8 loc_FF20E8:
    2. ROM:00FF20E8                 btst    #0,($A1201E).l
    3. ROM:00FF20F0                 bne.s   loc_FF210E
    4. ROM:00FF20F2                 btst    #0,($FFFFF39E).w
    5. ROM:00FF20F8                 beq.s   loc_FF2114
    6. ROM:00FF20FA                 bclr    #0,($FFFFF39E).w
    7. ROM:00FF2100                 subq.w  #1,($FFFFF382).w
    8. ROM:00FF2104                 bge.s   loc_FF2114
    9. ROM:00FF2106                 move.w  #4,($FFFFF382).w
    10. ROM:00FF210C                 bra.s   loc_FF2114
    11. ROM:00FF210E ; ---------------------------------------------------------------------------
    12. ROM:00FF210E
    13. ROM:00FF210E loc_FF210E:
    14. ROM:00FF210E                 bset    #0,($FFFFF39E).w
    15. ROM:00FF2114
    16. ROM:00FF2114 loc_FF2114:

    What it's doing here is that it checks if up is being held (yes, the game uses MCD communication registers here for the controller read buffer), and if so, it sets a flag. If it's not being held, it checks if the flag is set, and if so, it gets cleared, and moves the menu selection up. Effectively, it moves the menu selection on button release instead of tap. What's weird about this is that the controller reading function is just the standard Genesis controller reading function, which includes checking which buttons are being tapped instead of held. Yet, this menu ignores that and does... that, instead for the D-Pad. They do use the tapped bits for A, B, C, and start. Why? I honestly couldn't tell you. Maybe they were implementing some kind of other thing with the button presses? Probably not, but who knows...
     
    Last edited: Sep 18, 2022
  10. Devon

    Devon

    There's nothing left but faith Tech Member
    770
    451
    63
    Actually, the entirety of Visual Mode's menu is overengineered. It uses an object system for the menu options. Each option is their own object, and they have a sprite assigned to them. They also use an animation handler, even if that's not really utilized. Makes me wonder if they were planning something much more complex for this, or if they were just hacking shit together in a rush...

    Also, side note, it upsets me that not even NemDec is consistent between files. Sometimes, certain branches will use bsr.w, other times it's jsr (pc). Sometimes, NOPs are added in, sometimes they aren't present.

    From a level MMD:
    Code (Text):
    1. .NormalMode:
    2.         lsl.w   #2,d2                           ; Get number of 8-pixel rows in the uncompressed data
    3.         movea.w d2,a5                           ; and store it in a5
    4.         moveq   #8,d3                           ; 8 pixels in a pattern row
    5.         moveq   #0,d2
    6.         moveq   #0,d4
    7.         bsr.w   NemDec_BuildCodeTable
    8.         move.b  (a0)+,d5                        ; Get first word of compressed data
    9.         asl.w   #8,d5
    10.         move.b  (a0)+,d5
    11.         move.w  #16,d6                          ; Set initial shift value
    12.         bsr.s   NemDec_ProcessCompressedData
    13.         movem.l (sp)+,d0-a1/a3-a5
    14.         rts

    From the title screen:
    Code (Text):
    1. .NormalMode:
    2.         lsl.w   #2,d2                           ; Get number of 8-pixel rows in the uncompressed data
    3.         movea.w d2,a5                           ; and store it in a5
    4.         moveq   #8,d3                           ; 8 pixels in a pattern row
    5.         moveq   #0,d2
    6.         moveq   #0,d4
    7.         jsr     NemDec_BuildCodeTable(pc)
    8.         move.b  (a0)+,d5                        ; Get first word of compressed data
    9.         asl.w   #8,d5
    10.         move.b  (a0)+,d5
    11.         move.w  #16,d6                          ; Set initial shift value
    12.         bsr.s   NemDec_ProcessCompressedData
    13.         movem.l (sp)+,d0-a1/a3-a5
    14.         rts

    From the Visual Mode menu:
    Code (Text):
    1. .NormalMode:
    2.         lsl.w   #2,d2                           ; Get number of 8-pixel rows in the uncompressed data
    3.         movea.w d2,a5                           ; and store it in a5
    4.         moveq   #8,d3                           ; 8 pixels in a pattern row
    5.         moveq   #0,d2
    6.         moveq   #0,d4
    7.         jsr     NemDec_BuildCodeTable(pc)
    8.         move.b  (a0)+,d5                        ; Get first word of compressed data
    9.         asl.w   #8,d5
    10.         move.b  (a0)+,d5
    11.         move.w  #16,d6                          ; Set initial shift value
    12.         bsr.s   NemDec_ProcessCompressedData
    13.         nop
    14.         nop
    15.         nop
    16.         nop
    17.         movem.l (sp)+,d0-a1/a3-a5
    18.         rts
     
  11. Devon

    Devon

    There's nothing left but faith Tech Member
    770
    451
    63
    So, small bug in the time warp cutscene, but very subtle. It allocates 28 object slots for the sparkles that trail behind Sonic. 3 of them are set to be rendered in front of Sonic, and the other 25 behind him, so the processing code for them is split into 2, so that Sonic can be sandwiched in between them. However, there's a bug in it. Instead of starting on the first sparkle slot that's for being rendered behind Sonic, it accidentally starts at the first sparkle slot that's for being rendered in front. This means that the front sparkles are updated and rendered TWICE in a frame, and the last 3 sparkle objects never get updated.

    Code (Text):
    1.  
    2.         lea     sparkleObjsF.w,a0       ; Run sparkle objects in front of Sonic
    3.         moveq   #SPARKLEOBJFCNT-1,d7
    4.  
    5. .FrontSparkles:
    6.         move.w  d7,-(sp)
    7.         bsr.s   RunSparkleObject
    8.         move.w  (sp)+,d7
    9.         adda.w  #oSize,a0
    10.         dbf     d7,.FrontSparkles
    11.      
    12.         lea     sonicObject.w,a0        ; Run Sonic object
    13.         bsr.w   RunSonicObject
    14.      
    15.         lea     sonicTrailObj1.w,a0     ; Run Sonic's trailing sprite objects
    16.         bsr.w   RunSonicTrailObj
    17.         lea     sonicTrailObj2.w,a0
    18.         bsr.w   RunSonicTrailObj
    19.         lea     sonicTrailObj3.w,a0
    20.         bsr.w   RunSonicTrailObj
    21.      
    22.         ; BUG: Should be sparkleObjsB. As a result of this, the sparkles
    23.         ; in front of Sonic are run and drawn twice, while 3 sparkles meant to
    24.         ; appear behind Sonic never show up.
    25.         lea     sparkleObjsF.w,a0       ; Run sparkle objects behind Sonic
    26.         moveq   #SPARKLEOBJBCNT-1,d7
    27.  
    28. .BackSparkles:
    29.         move.w  d7,-(sp)
    30.         bsr.s   RunSparkleObject
    31.         move.w  (sp)+,d7
    32.         adda.w  #oSize,a0
    33.         dbf     d7,.BackSparkles
    34.  

    Bugged:
    [​IMG]

    Fixed:
    [​IMG]

    It's subtle, but more sparkles appear when fixed. You can see it better towards the end when they trail off after Sonic moves up.
     
    Last edited: Sep 19, 2022
    • Informative Informative x 4
    • Like Like x 1
    • List
  12. Devon

    Devon

    There's nothing left but faith Tech Member
    770
    451
    63
    So, after looking at DA Garden's code and noticing that it appears to have been done by the same programmer, here's my theory on this:

    I think they programmed all of DA Garden without even knowing that those bits for checking if a button was just tapped and not held down even existed. They are mostly unused in DA Garden sans the start button check, and they do the same exact weird manual button release checks. Then, when it came time to do the Visual Mode menu, they used the same general code base, and did those weird checks on the D-Pad. Someone probably came along, or maybe they looked deeper into how the controller reading function worked, and it was shown that those tapped bits existed, was like "oooooh...", and then used that going forward.

    Could I be wrong? Probably. But, that seems like the most probable thing, in my opinion.
     
    Last edited: Sep 21, 2022
    • Like Like x 4
    • Informative Informative x 1
    • List
  13. Devon

    Devon

    There's nothing left but faith Tech Member
    770
    451
    63
    Okay, more DA Garden shenanigans. Let's start off with a slight correction: DA Garden does used the tapped bits once, just for checking if the start button was pressed. This detail will be important later.

    So, when DA Garden initializes, it renders the planet before it fades from black so that it doesn't appear out of nowhere. It does 2 renders into both buffers to keep things synchronized. The Sub CPU handles updating the planet's position, angle, and size, and it also checks if the start button gets pressed to exit out of DA Garden. The initial planet render routine, however, has a bug in its CPU communication loop:
    Code (Text):
    1.         bset    #6,GAMAINFLAG                   ; Mark as exiting
    2.  
    3. .WaitSubCPU:
    4.         btst    #6,GASUBFLAG                    ; Has the Sub CPU responded?
    5.         beq.s   .WaitSubCPU                     ; If not, wait (BUG: This should be a "bne.s")
    6.         bclr    #6,GAMAINFLAG                   ; Communication is done

    The way that it's supposed to go is that, when the Sub CPU detects that the start button has been pressed, it sets bit 6 in the sub flag. The Main CPU is to then respond to that by setting bit 6 in the main flag, and then the Sub CPU responds to that by clearing bit 6 in the sub flag. It keeps them both in sync. However, as you can see in the code, it uses the wrong branch instruction to check if the Sub CPU has responded to the Main CPU's acknowledgement. Once the Main CPU sets its bit 6, the Sub CPU pretty much clears its bit 6 at the same time (it's already in its side of the communication, because the Main CPU waits for the Sub CPU to finish its updates), and thus the loop will just last forever, effectively being a game crash.

    This sounds pretty bad, but what if I told you that they actually DID "fix" this? At around the very beginning of the DA Garden program is this line:
    Code (Text):
    1.         move.w    #$8000,ctrlData                 ; Force program to assume start button was being held

    What this does is force the program to assume that start was being held before it started. When it finishes the first render, it does VSync, which in turns causes controller input to be updated, as you can see in this code:
    Code (Text):
    1.         bsr.w   UpdateSubCPU                    ; Update Sub CPU
    2.         bsr.w   WaitWordRAMAccess               ; Wait for Word RAM access
    3.         bsr.w   GetPlanetImage                  ; Get planet image
    4.         bsr.w   AnimateVolcano                  ; Animate volcano
    5.         bsr.w   GiveWordRAMAccess               ; Give back Word RAM access
    6.         bsr.w   VSync                           ; VSync
    7.  
    8.         addq.w  #1,vintRoutine.w                ; Set next V-INT routine
    9.         bsr.w   VSync                           ; VSync
    10.         btst    #6,GASUBFLAG                    ; Is the Sub CPU exiting?
    11.         bne.s   .Exit                           ; If so, branch

    If the start button is found to be pressed, because of that added line, the program thinks that this is because the start button is still being held down, even if that's not really true, and thus doesn't set the tapped bit. Now, if you don't have it pressed, then it all gets reset back to 0, meaning that, yes, the tapped bit can be set after the second render... but, keyword: "after". Like I said, VSync is called after a render is finished, so even if you can get the tapped bit to be set in this sequence, the Sub CPU will not actually see that, because it will have already updated at this point. And, thus, because of all this, technically, this bug is "fixed"... well, more like avoided. Without that hotfix, you can get the game to crash by pressing start on the right frame before it fades from black.

    It might sound like it's a frame specific thing to pull off without the hotfix, but actually you'd just be able to hold down the start button to trigger the crash, because at the very beginning, the controller data buffer is cleared out. This means that when it checks for the start button, it'll think that it was just pressed, and thus trigger the Sub CPU to exit out, and thus cause the crash on initialization.

    And as a side note, the Visual Mode menu retains that hotfix, which I believe further shows that it was indeed built from DA Garden's code. Another quick side note: if what I said was true about the weird button checks, then I wonder if this bug was what caused them to find out that those tapped bits existed. (EDIT: This theory grows even more true. The 0.51 prototype uses the held bit for checking the start button, and the hotfix doesn't exist. This also means that if you can boot into DA Garden in that prototype, you can trigger the crash by merely holding down the start button when it starts. Not even Visual Mode uses the tapped bits, they used the held bits for the face buttons. The face button checks got converted to use the tapped bits after the "fix" was put into place.)

    What a doozy.

    ...man I could start a blog with all the weird stuff I come across as I disassemble the game, lmao

    >programmer didn't know that there are bits you can check for when a button is tapped and released
    >they go on to only use the regular held bits, and for tapping, they manually checked for button release
    >DA Garden has a bug in the initialization routine that causes a crash if start is pressed to exit immediately, due to a bad branch in the CPU communication protocol, causing the Main CPU to enter an infinite loop
    >can't figure out why it's happening
    >oh god oh fuck
    >bitch about it to another programmer
    >that other programmer points out that those tap/release bits existed
    >try that
    >doesn't work, because controller data is cleared on initialization, and the controller update will treat the start button being held as just tapped, and thus set the bit, and trigger the crash
    >fuck it, no start button 4 u on initialization
    >crash "fixed"
    >yay
    >use tap/release bits for the rest of the button checks that were implemented
    >...well at least for the basic button checks, too lazy to change the manual button release checks
     
    Last edited: Sep 21, 2022
    • Like Like x 4
    • Informative Informative x 3
    • Useful Useful x 1
    • List
  14. Nik Pi

    Nik Pi

    Member
    Sonic CD unlocked :p
    But seriously, I'm even afraid to imagine how much effort and nerves have been invested in these "excavations". It's so impressive.. I bet you're so tried with this, man)
    Although I'm not an ardent fan of Sonic CD, but to see how you open all these things.. it really impresses me!
    I'm afraid to imagine what a headache this is.. I'm sure you're tired, but know that you're not doing this in vain. I think many people here respect you and your work. ERAnPGWXYAYUB_q.jpeg
     
  15. Black Squirrel

    Black Squirrel

    shaving is boring Wiki Sysop
    7,168
    1,218
    93
    Northumberland, UK
    hey wiki you're so fine
    Question:

    Naoto Oshima supposedly wanted an instant time zone transition but they couldn't read the disc quick enough, hence why the warp transition exists - to mask loading.

    But how much loading does it actually mask? Does it need to be as long as it is, or can it be shortened (or removed)? The transition feels like a desperate afterthought (although given it exists in even the most early of prototypes, it obviously wasn't) and I've never much cared for the way it interrupts play - I'd be curious to know how close they got.


    (I ask because in the PC version, there's a loading screen after the warp... which suggests it might not be masking some loading at all)
     
  16. Devon

    Devon

    There's nothing left but faith Tech Member
    770
    451
    63
    I actually answered this a little while ago in this thread:
    I even developed this!


    The CD read speed actually isn't that bad, and a good chunk (like, maybe half of it?) of the loading time is actually due to Nemesis decompression being slow. With some optimizations on when data gets loaded (it starts reading as soon as it starts fading to white and plays the warp sound), reducing the amount of data to load (by segmenting level files so that I only have to switch out time zone/act specific stuff), and switching to Kosinski Moduled for compressed graphics, I was able to achieve what I did in the second video.

    I will say, the reason why it takes so long to boot to the title screen, is because it loads 7 different files before it starts up. SPX is loaded during the Sega screen, IPX (the main program file), then MDINIT, then BRAMINIT and BRAMSUB, and then finally TITLEM and TITLES.
     
    Last edited: Sep 21, 2022
  17. Mookey

    Mookey

    Member
    58
    25
    18
    It's interesting imo that the only part of the cutscene still running once the game's loading is the jingle, considering how in earlier prototypes it was a CD track. Always wondered why that aspect changed so much (relatively so), but if the data on the CD can't be accessed while also playing CD audio then I guess the final FM iteration was due to loading optimization.

    Still strange that they initially considered using CD audio for it though; seems like an obvious enough issue that I'm surprised the devs even considered it at all.
     
  18. Devon

    Devon

    There's nothing left but faith Tech Member
    770
    451
    63
    The FM sound driver runs on the Z80, so the sound updates totally independently from the Genesis 68000. The sound in v0.02 and 510 was also FM, just a different sound.
     
  19. Mookey

    Mookey

    Member
    58
    25
    18
    Strange, you're right, but for some reason 712 has a variation of the track in redbook audio form (https://tcrf.net/Proto:Sonic_the_Hedgehog_CD_(Sega_CD)/Ver0.70#Audio).

    I wonder how that fits into all this, if all known versions use FM audio anyway (or does that proto use the CD track?).
     
  20. Black Squirrel

    Black Squirrel

    shaving is boring Wiki Sysop
    7,168
    1,218
    93
    Northumberland, UK
    hey wiki you're so fine
    Yes you did, sorry - I didn't read. Fascinating stuff.

    You might be able to make savings by loading the foreground before the background... though you'd have to do something fancy to make the transition less jarring.