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: "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:
Actually, we talked about that before, and Hachelle-Bee said the same, but without providing the code: https://forums.sonicretro.org/index.php?threads/sonic-cd-warped.39548/#post-966183
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: Intended behavior: This is the code that's responsible: Code (Text): .Untilt: ; BUG: This function is bugged. It intends to gradually untilt Sonic's sprite, ; but with the way the branches are set up, it makes it instant instead cmpi.b #5,oPlayerTilt(a0) ; Are we tilting left? bcs.s .UntiltLeft ; If so, branch .UntiltRight: subq.b #1,oPlayerTilt(a0) ; Untilt from the right cmpi.b #5,oPlayerTilt(a0) ; Are we in the center? bcc.s .UntiltLeft ; If not, branch move.b #5,oPlayerTilt(a0) ; Cap at center rts .UntiltLeft: addq.b #1,oPlayerTilt(a0) ; Untilt from the left cmpi.b #5,oPlayerTilt(a0) ; Are we in the center? bls.s .UntiltLeft ; If not, branch move.b #5,oPlayerTilt(a0) ; Cap at center rts 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): ObjSonic_Water: tst.b timeStopped ; Is time stopped? bne.s .End ; If so, branch move.b #8,splashObject+oID ; Spawn splash object btst #1,specStageFlags.w ; Are we in time attack mode? beq.s .End ; If not, branch move.w #$500,oPlayerTopSpeed(a0) ; If so, slow Sonic down .End: 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): ; ------------------------------------------------------------------------- ; Unknown mode ; ------------------------------------------------------------------------- ObjSonic_Unk: addq.w #4,oSprY(a0) ; Move sprite down cmpi.w #320+128,oSprY(a0) ; Has it moved offscreen? bcs.s .End ; If not, branch move.b #6,oRoutine(a0) ; Advance routine .End: rts ; ------------------------------------------------------------------------- ObjSonic_Unk2: rts
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.
Well, I never claimed it was "better", I was saying that's what the developers wanted to do initially.
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): ROM:00FF20E8 loc_FF20E8: ROM:00FF20E8 btst #0,($A1201E).l ROM:00FF20F0 bne.s loc_FF210E ROM:00FF20F2 btst #0,($FFFFF39E).w ROM:00FF20F8 beq.s loc_FF2114 ROM:00FF20FA bclr #0,($FFFFF39E).w ROM:00FF2100 subq.w #1,($FFFFF382).w ROM:00FF2104 bge.s loc_FF2114 ROM:00FF2106 move.w #4,($FFFFF382).w ROM:00FF210C bra.s loc_FF2114 ROM:00FF210E ; --------------------------------------------------------------------------- ROM:00FF210E ROM:00FF210E loc_FF210E: ROM:00FF210E bset #0,($FFFFF39E).w ROM:00FF2114 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...
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): .NormalMode: lsl.w #2,d2 ; Get number of 8-pixel rows in the uncompressed data movea.w d2,a5 ; and store it in a5 moveq #8,d3 ; 8 pixels in a pattern row moveq #0,d2 moveq #0,d4 bsr.w NemDec_BuildCodeTable move.b (a0)+,d5 ; Get first word of compressed data asl.w #8,d5 move.b (a0)+,d5 move.w #16,d6 ; Set initial shift value bsr.s NemDec_ProcessCompressedData movem.l (sp)+,d0-a1/a3-a5 rts From the title screen: Code (Text): .NormalMode: lsl.w #2,d2 ; Get number of 8-pixel rows in the uncompressed data movea.w d2,a5 ; and store it in a5 moveq #8,d3 ; 8 pixels in a pattern row moveq #0,d2 moveq #0,d4 jsr NemDec_BuildCodeTable(pc) move.b (a0)+,d5 ; Get first word of compressed data asl.w #8,d5 move.b (a0)+,d5 move.w #16,d6 ; Set initial shift value bsr.s NemDec_ProcessCompressedData movem.l (sp)+,d0-a1/a3-a5 rts From the Visual Mode menu: Code (Text): .NormalMode: lsl.w #2,d2 ; Get number of 8-pixel rows in the uncompressed data movea.w d2,a5 ; and store it in a5 moveq #8,d3 ; 8 pixels in a pattern row moveq #0,d2 moveq #0,d4 jsr NemDec_BuildCodeTable(pc) move.b (a0)+,d5 ; Get first word of compressed data asl.w #8,d5 move.b (a0)+,d5 move.w #16,d6 ; Set initial shift value bsr.s NemDec_ProcessCompressedData nop nop nop nop movem.l (sp)+,d0-a1/a3-a5 rts
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): lea sparkleObjsF.w,a0 ; Run sparkle objects in front of Sonic moveq #SPARKLEOBJFCNT-1,d7 .FrontSparkles: move.w d7,-(sp) bsr.s RunSparkleObject move.w (sp)+,d7 adda.w #oSize,a0 dbf d7,.FrontSparkles lea sonicObject.w,a0 ; Run Sonic object bsr.w RunSonicObject lea sonicTrailObj1.w,a0 ; Run Sonic's trailing sprite objects bsr.w RunSonicTrailObj lea sonicTrailObj2.w,a0 bsr.w RunSonicTrailObj lea sonicTrailObj3.w,a0 bsr.w RunSonicTrailObj ; BUG: Should be sparkleObjsB. As a result of this, the sparkles ; in front of Sonic are run and drawn twice, while 3 sparkles meant to ; appear behind Sonic never show up. lea sparkleObjsF.w,a0 ; Run sparkle objects behind Sonic moveq #SPARKLEOBJBCNT-1,d7 .BackSparkles: move.w d7,-(sp) bsr.s RunSparkleObject move.w (sp)+,d7 adda.w #oSize,a0 dbf d7,.BackSparkles Bugged: Fixed: 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.
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.
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): bset #6,GAMAINFLAG ; Mark as exiting .WaitSubCPU: btst #6,GASUBFLAG ; Has the Sub CPU responded? beq.s .WaitSubCPU ; If not, wait (BUG: This should be a "bne.s") 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): 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): bsr.w UpdateSubCPU ; Update Sub CPU bsr.w WaitWordRAMAccess ; Wait for Word RAM access bsr.w GetPlanetImage ; Get planet image bsr.w AnimateVolcano ; Animate volcano bsr.w GiveWordRAMAccess ; Give back Word RAM access bsr.w VSync ; VSync addq.w #1,vintRoutine.w ; Set next V-INT routine bsr.w VSync ; VSync btst #6,GASUBFLAG ; Is the Sub CPU exiting? 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 Spoiler: Updated theory on the weird button checks >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
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.
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)
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.
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.
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.
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?).
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.