don't click here

Sonic CD Quirks/Deconstruction

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

  1. Brainulator

    Brainulator

    Regular garden-variety member Member
    It should be noted that frameout is the object deletion subroutine.
     
  2. BenoitRen

    BenoitRen

    Tech Member
    793
    389
    63
    Some sprite_pattern objects are noted to contain zero entries of sprite data, yet when you actually look at the array you see that there is an entry of data.

    The first time it appeared in BLOCK.C as the first pattern:
    Code (Text):
    1.  
    2. sprite_pattern block0 =
    3. {
    4.   0,
    5.   { { -12, -12, 0, 403 } }
    6. };
    Then as the last pattern of Metal Sonic's appearance in Collision Chaos (MSNCCG.C):
    Code (Text):
    1.  
    2. sprite_pattern msnc3_patF = {
    3.   0,
    4.   { { -28, -24, 0, 433 } }
    5. };
    And now again as the last pattern of one of Sonic's friends in Tidal Tempest:
    Code (Text):
    1.  
    2. sprite_pattern spr_friend4_99 = {
    3.   0,
    4.   { { -8, -12, 0, 497 } }
    5. };
    In the cases of BLOCK and FRIEND4, they share an index (the last number) with a neighbouring entry. Metal Sonic's doesn't.

    Could this be a typo made during conversion? I've noticed that data structures in Sonic & Knuckles Collection have zero-based numbers to denote how many entries they have. The loop that treats them counts downwards until the counter is negative.
     
  3. BenoitRen

    BenoitRen

    Tech Member
    793
    389
    63
    I think I found another bug.

    In file TAGAMEB4.C, the function a_stop starts as follows:
    Code (Text):
    1.  
    2. static void a_stop(sprite_status* pActwk) {
    3.   short subact;
    4.  
    5.   --((short*)pActwk)[23];
    6.   if (((short*)pActwk)[23] < 0)
    7.   {
    8.     if (pActwk->actflg < 0)
    9.       soundset(179);
    What's wrong with it? actflg is an unsigned char, and here it's being tested for a negative value. As it's not signed, it can never be negative. But it's not a serious issue, because the only consequence is that a certain sound isn't played when the Taga-Taga enemy stops.

    How to fix it? Usually, when this check is done, actflg is casted to a (signed) char, like this:
    Code (Text):
    1. if ((char)pActwk->actflg < 0)
    If this inconsistency isn't present in the Mega CD version, then this is an undocumented difference. :)
     
    • Like Like x 2
    • Agree Agree x 1
    • List
  4. OrionNavattan

    OrionNavattan

    Tech Member
    175
    177
    43
    Oregon

    I believe this is the corresponding code in the Mega CD version (taken from TTZ 1 Present's MMD):
    Code (ASM):
    1. ObjTagaTaga_0_Routine4:
    2.         addi.w  #-1,ost_var_2A(a0)
    3.         bpl.w   locret_20DD80       ; exit if ost_var_2A is still positive
    4.         tst.b   ost_render(a0)
    5.         bpl.s   loc_20DCF6          ; skip playing sound if Taga-Taga is offscreen
    6.         move.w  #$B3,d0
    7.         jsr (PlayFMSound).l
    The tst.b/bpl pair is pretty much the standard method of checking if an object's sprite was displayed on the previous frame. There's no issues I can see with this instance of the check in the Mega CD version (nor with any of the others in TTZ 1 Present's MMD), so I think you've found a bug unique to the Sonic Gems port.
     
    • Informative Informative x 2
    • List
  5. Chimes

    Chimes

    The One SSG-EG Maniac Member
    928
    641
    93
    Can somebody confirm what in the world is going on with Stardust Speedway's spotlights?
    Ostensibly, it's supposed to strobe two dithered spotlights one at a time at 60fps to simulate transparency. Then it switches to 2 frames to show them alternating.
    In practice, the two spotlights even at 1-frame seem to have great amounts of frameskipping that leaves one spotlight more visible than the other.
    I've tried to find hardware videos of the spotlights. However, either:
    1. It's recorded from a emulator, where thanks to recording lag or improper framerates the frameskipping is exaggerated
    2. It's recorded either in 30fps or deinterlaced, which just shows one spotlight that warps into another since the frameskipping is still there.
    This video shows the spotlights at its most clearest.
    I've recorded the spotlights where even on BlastEm the spotlights display frequent frameskipping. I wonder if it's my monitor at fault...? It's hard to figure out if the frameskipping is a refresh rate issue or CD being weird.
     
    Last edited: Mar 1, 2024
  6. Devon

    Devon

    La mer va embrassé moi et délivré moi lakay. Tech Member
    1,423
    1,740
    93
    your mom
    The searchlights' secondary subtype value determine a few properties, which is set in the object layout. Bit 3 determines if it should be behind everything or in front, which isn't relevant to this specific thing. However, the rest are.

    Bit 2 determines how "bright" the searchlight is. The way that works is that it basically changes how often its sprite will display. If it's meant to be more dim, then it will only display the sprite every 4 frames. If it's meant to be brighter, it will display the sprite every other frame.

    Bits 0-1 determines which frame within the 2/4 frame window in which it will display. For example, for searchlights that are dimmer, within a 4 frame window, searchlights with this set to 0 will display first, and then the next frame will have searchlights with this set to 1 displayed, and then the next frame displays those with this set to 2, and finally the last frame displays those with this set to 3. This is the culprit to the weird timeskipping that you are noticing. My best guess for why it's set up this way is to help prevent too many sprites being drawn in a frame at once if there's multiple searchlights on screen. Kind of like how old NES games have that sprite flickering mechanism to get more sprites drawn.

    Here's the relevant code:
    Code (ASM):
    1. ObjSearchlight_Init:
    2.         addq.b  #2,oRoutine(a0)                                 ; Advance routine
    3.         ori.b   #4,oSprFlags(a0)                                ; Set sprite flags
    4.         move.l  #Spr_Searchlight,oSprites(a0)                   ; Set sprite data
    5.         move.b  #$7F,oWidth(a0)                                 ; Set width
    6.         move.b  #$7F,oYRadius(a0)                               ; Set height
    7.         move.w  oX(a0),oLightX(a0)                              ; Save X position
    8.  
    9.         move.b  oSubtype2(a0),d0                                ; Get which frame to display at within window
    10.         andi.b  #3,d0
    11.         move.b  d0,oLightFrame(a0)
    12.  
    13.         move.b  #1,oPriority(a0)                                ; Appear in front of everything
    14.         move.w  #$A4AF,oTile(a0)
    15.  
    16.         btst    #3,oSubtype2(a0)                                ; Should we be behind everything?
    17.         beq.s   ObjSearchlight_Main                             ; If not, branch
    18.  
    19.         move.b  #3,oPriority(a0)                                ; Appear behind everything
    20.         move.w  #$24AF,oTile(a0)
    21.         cmpi.b  #1,timeZone                                     ; Are we in the present?
    22.         beq.s   ObjSearchlight_Main                             ; If so, branch
    23.         addi.w  #$2000,oTile(a0)                                ; If not, also change to a different color
    24.  
    25. ; ---------------------------------------------------------------------------
    26.  
    27. ObjSearchlight_Main:
    28.         move.w  cameraY,d0                                      ; Align Y position with camera's
    29.         addi.w  #$70,d0
    30.         move.w  d0,oY(a0)
    31.  
    32.         moveq   #0,d0                                           ; Set sprite frame and position
    33.         move.b  oSubtype(a0),d0
    34.         add.w   d0,d0
    35.         move.w  .SpriteSetters(pc,d0.w),d0
    36.         jsr     .SpriteSetters(pc,d0.w)
    37.  
    38.         move.w  stageFrames,d0                                  ; Get frame in current 4-frame window
    39.         andi.b  #3,d0
    40.         btst    #2,oSubtype2(a0)                                ; Should we be bright (draw sprite more often with 2-frame window)?
    41.         beq.s   .Bright                                         ; If so, branch
    42.  
    43.         cmp.b   oLightFrame(a0),d0                              ; Should we draw our sprite on this frame?
    44.         bne.s   .End                                            ; If not, branch
    45.         jsr     DrawObject                                      ; If so, draw sprite
    46.  
    47. .End:
    48.         rts
    49.  
    50. .Bright:
    51.         andi.b  #1,d0                                           ; Get frame in current 2-frame window
    52.  
    53.         cmp.b   oLightFrame(a0),d0                              ; Should we draw our sprite on this frame?
    54.         bne.s   .End2                                           ; If not, branch
    55.         jsr     DrawObject                                      ; If so, draw sprite
    56.  
    57. .End2:
    58.         rts

    Here's an illustration of how this works:
     
    Last edited: Mar 3, 2024
    • Like Like x 3
    • Informative Informative x 3
    • List
  7. Devon

    Devon

    La mer va embrassé moi et délivré moi lakay. Tech Member
    1,423
    1,740
    93
    your mom
    The reason the sprite is messed up is because another object's graphics overwrite it in VRAM. However, this is not the case in act 3. However, due to a bug, this object in the 510 prototype uses the wrong tile ID for the sprite.

    This is the bugged code in question, where it checks the zone ID instead of the act ID:
    [​IMG]

    Here it is fixed:
    [​IMG]
     
  8. Kilo

    Kilo

    Deathly afraid of the YM2612 Tech Member
    1,039
    1,038
    93
    Canada
    Sonic 1 Source Code Recreation + Source Code Wiki Page
    I wonder what kind of practical use this could've had in level design... Probably not much since it was scrapped.
     
  9. Mookey

    Mookey

    Member
    165
    98
    28
    To me it looks like it was supposed to carry Sonic during the duration of the flashing, and then maybe pop him out of it once it stopped? There's a couple of objects in CCZ that do something similar so I can see it fitting in, but I don't know if there's anywhere in the current layout where it might have literally fit in (like the Metallic Madness divots where the trapdoor object originally was).
     
  10. Devon

    Devon

    La mer va embrassé moi et délivré moi lakay. Tech Member
    1,423
    1,740
    93
    your mom
    It is meant to carry Sonic around during the flashing, but it also teleports Sonic to another one once it stops flashing.
     
  11. Mookey

    Mookey

    Member
    165
    98
    28
    Oh I missed that at the end. So it looks like the only thing broken is the platform carrying him then.
     
  12. Devon

    Devon

    La mer va embrassé moi et délivré moi lakay. Tech Member
    1,423
    1,740
    93
    your mom
    Pretty much. In the end, though, it wasn't finished. In fact, its code was outright removed in the final, leaving just the graphics behind.
     
  13. OrionNavattan

    OrionNavattan

    Tech Member
    175
    177
    43
    Oregon
    Spent a good chunk of this last week disassembling the Time Attack menu (ATTACK.MMD). I still have a ways to go before it's fully labeled and documented, but in keeping with the game's well-documented lack of consistency, the menu has a couple of gems and oddities that leave me wondering just how many different teams of programmers worked on this game:

    • The menu seems to be built entirely out of tilemaps, with what seems to be a not insignificant amount of code dedicated to drawing and rendering them on the foreground plane.
    • The menu code runs from workram (not unexpected since the menu does perform BURAM reads and writes, which use the wordram to pass the data back and forth). However, the fact that the file is $20000 bytes in size should be a hint that there's something else going on. The first $D100 bytes are a otherwise normal workram MMD, but the remaining $12F00 bytes contain $F0E4 bytes (nearly 61 KB!) of Nemesis and Enigma-compressed tiles and tilemaps for the menu's level thumbnails, sandwiched between a balance of padding. These are left in the wordram after RunMMD copies the code, and are directly referenced and accessed by the workram program when the thumbnail on the screen needs to change. Since the BURAM reads/writes and the loading of new thumbnails are entirely self-contained, there is no issue with using the wordram concurrently for these tasks.
    • The joypad read function is... different.
      Code (ASM):
      1. ReadJoypad:
      2.        bsr.w   StopZ80
      3.        bsr.w   ReadJoypad_Do
      4.        bsr.w   StartZ80
      5.        move.b   (v_joypad_hold_actual).l,d1           ; d1 = previous joypad state
      6.        move.b   d0,(v_joypad_hold_actual).l           ; v_joypad_hold_actual = SACBRLDU
      7.        move.b   d0,d2
      8.        eor.b   d1,d2
      9.        and.b   d0,d2                               ; d2 = new joypad inputs only
      10.        move.b   d2,(v_joypad_press_actual).l       ; v_joypad_press_actual = SACBRLDU (new only)
      11.        rts
      12. ; =============================
      13.  
      14. ; nearly 400 lines farther down in the code
      15. ReadJoypad_Do:
      16.        movem.l d1,-(sp)
      17.        move.b   #0,(port_1_data).l       ; set port to read 00SA00DU
      18.        nop
      19.        nop
      20.        move.b   (port_1_data).l,d0       ; d0 = 00SA00DU
      21.        move.b   #$40,(port_1_data).l               ; set port to read 00CBRLDU
      22.        lsl.b   #2,d0
      23.        move.b   (port_1_data).l,d1                   ; d1 = 00CBRLDU
      24.        andi.b   #btnStart|btnA,d0
      25.        andi.b   #btnDir|btnB|btnC,d1
      26.        or.b   d1,d0                   ; d0 = SACBRLDU
      27.        not.b   d0                   ; invert bits, so that 1 = pressed
      28.        movem.l (sp)+,d1
      29.        rts
      Why the code is split like this is unknown, as is the non-usage of address registers within it. It also uses the same reduced FadeToBlack/FadeFromBlack routines as the Special Stages and sound test.
    • Perhaps the strangest one of all. The following is how the menu loads some of its tilemaps:
      Code (ASM):
      1.        move.l   #18-1,-(sp)                       ; a0 -> d0 (height-1)
      2.        move.l   #sizeof_vram_row_64/2,-(sp)       ; d3 (size of row)
      3.        move.l   #16-1,-(sp)                       ; d2 (width-1)
      4.        vdp_comm.l   move,(vram_timeatk_fg+$1AA),vram,write,-(sp)   ; d1 (destination)
      5.        pea   (v_enidec_buffer).l               ; d0 -> a0 (source)
      6.        bsr.w   TilemapToVRAM
      7.        lea 4*5(sp),sp
      8.        ; ...
      9. ; =============================
      10.  
      11. ; around 2200 lines farther down in the code
      12. TilemapToVRAM:
      13.        movem.l d0-d4/a0-a2,-(sp)
      14.        movem.l (8*4)+4(sp),d0-d3/a0   ; 8 backed up registers + return address
      15.        exg       d0,a0                   ; swap row count and source address to correct registers
      16.        lea (vdp_control_port).l,a1
      17.        lea (vdp_data_port).l,a2
      18.        add.w   d3,d3
      19.        swap   d3         ; calculate VRAM address delta
      20.  
      21.    .loop_row:
      22.        move.l   d1,(a1)       ; set destination
      23.        move.w   d2,d4       ; set loop counter for cells in row
      24.  
      25.    .loop_cell:
      26.        move.w   (a0)+,(a2)       ; write value to nametable
      27.        dbf d4,.loop_cell   ; repeat for all tiles in row
      28.        add.l   d3,d1           ; next row
      29.        dbf d0,.loop_row   ; repeat for all rows
      30.        movem.l (sp)+,d0-d4/a0-a2
      31.        rts
      This isn't too dissimmilar from the tllemap copy routine used in the levels other than the different register usage and allowing the user to specify the plane width as an argument, expect that for some reason, they seem to have used a version of the routine meant for use with compiled C code! All of the input arguments are pushed to the stack before the call, and then loaded into registers from the stack. This does seem to be necessary for C (which I know nothing about other than 68K C compilers have some limitations on how arguments are passed via registers), but it makes absolutely no sense to do this with hand-written assembly. And it's not the only one: there are a total of FIVE routines like this in the time attack menu, the others being a routine that sets the VDP registers, a routine that waits for a certain number of VBlanks, and two more routines that copy data to the VDP.

      Since all of them except for the VBlank delay are right next to each other in the code, I guess they could have come from some sort of library meant for use in C code. But why? Was the team that worked on the menu pressed for time and just using whatever code they happened to had handy?
     
    Last edited: Mar 13, 2024
    • Like Like x 3
    • Informative Informative x 1
    • List
  14. BenoitRen

    BenoitRen

    Tech Member
    793
    389
    63
    Neat to see someone continuing the disassembly of Sonic CD!

    Is the 68000 version of the Time Attack menu at all similar to the C version? The menus in the latter contain a lot of references to "hmx", which I suspect is a graphics library that isn't documented anywhere.
     
  15. OrionNavattan

    OrionNavattan

    Tech Member
    175
    177
    43
    Oregon
    It's unfortunately way too early for me to draw any conclusions, given that I've haven't looked too deep into the code itself yet.

    FWIW, there is some evidence suggesting that the time attack menu (and the BURAM manager for that matter) might have been planned to use GFX operations at one point: I also disassembled the unused ATTACK.BIN file (which I'm assuming is the remnants of a sub CPU program for the menu). It's basically just a subset of the BURAM manager's sub CPU program (BRAMSUB.BIN), and just like that one, it has an unused GFXInt (IRQ1) handler.
     
  16. Kilo

    Kilo

    Deathly afraid of the YM2612 Tech Member
    1,039
    1,038
    93
    Canada
    Sonic 1 Source Code Recreation + Source Code Wiki Page
    By GFX operations you mean things like rotation and scaling? Seems excessive for a time attack menu. But I guess if you have a bunch of CPU time to do it might as well flex some visual effects I suppose.
     
  17. Devon

    Devon

    La mer va embrassé moi et délivré moi lakay. Tech Member
    1,423
    1,740
    93
    your mom
    I went ahead and checked for myself, and basically, what's going on is that ATTACK.BIN sets its own graphics operation interrupt handler (which sets $41A0 to 0... which is some random address in the Sub CPU BIOS range???) and enables it.

    Code (ASM):
    1. ROM:00010000                 move.l  #TimeAttackIRQ1,_LEVEL1+2
    Code (ASM):
    1. ROM:00010040                 bset    #1,$FF8033
    Code (ASM):
    1. ROM:000100B6 TimeAttackIRQ1:
    2. ROM:000100B6                 move.b  #0,$41A0        ; wtf?
    3. ROM:000100BC                 rte

    I did check some BIOSes, and no, address $41A0 has no real significance, and refers to a different thing depending on the BIOS. Also, out of curiosity, I checked the prototypes, and this code was added very late into development (sometime between the July 21st build and August 1st build). Earlier builds don't have this code at all, and instead just leave the interrupt as disabled, with no handler set up at all.
     
    Last edited: Mar 13, 2024
    • Informative Informative x 4
    • Like Like x 2
    • List
  18. BenoitRen

    BenoitRen

    Tech Member
    793
    389
    63
    In the second and third acts, Wacky Workbench has these strings of spiked balls rotating around a sphere. There's a version of this object for each era, where the difference is the number of spiked balls. In the past it's four, in the present it's five, and in the future it's six.

    Looking at the code, however, the past version creates five spiked balls, while the other eras create six of them. The reason not all of them show up is because both the past and present versions create one spiked ball whose ID is put into the same slot of the parent object as the previously created one:
    Code (C):
    1. ((unsigned short*)a1)[33] = actionwk - actwk;
    2. ((unsigned short*)actionwk)[25] = a1 - actwk;
    3.  
    4. /* snip */
    5.  
    6. ((unsigned short*)a1)[33] = actionwk - actwk;
    7. ((unsigned short*)actionwk)[26] = a1 - actwk;
    8.  
    9. /* snip */
    10.  
    11. ((unsigned short*)a1)[33] = actionwk - actwk;
    12. ((unsigned short*)actionwk)[27] = a1 - actwk;
    13.  
    14. /* snip */
    15.  
    16. ((unsigned short*)a1)[33] = actionwk - actwk;
    17. ((unsigned short*)actionwk)[27] = a1 - actwk;
    18.  
    19. /* snip */
    20.  
    21. ((unsigned short*)a1)[33] = actionwk - actwk;
    22. ((unsigned short*)actionwk)[28] = a1 - actwk;
    23.  
    24. /* snip */
    25.  
    26. ((unsigned short*)a1)[33] = actionwk - actwk;
    27. ((unsigned short*)actionwk)[29] = a1 - actwk;
    But that's not all. The future version of this object has a bug depending on the platform.

    Each spiked ball contains the ID of its parent object in the form of a two-byte integer, saved in slots 66 (0x42) and 67 (0x43) of its sprite status. When it wants to retrieve this ID to check if its parent object has been deleted, it reads only the byte in slot 66.

    As the maximum value of an ID is 127, one byte is enough, but whether the byte you want is in the expected slot depends on the endianness of your system.

    As both x86 (PC) and the PS2 are little endian, the lower part of the integer will be stored first, which means it will find the ID in the expected slot on those platforms. PowerPC, however, is big endian, so I expect these strings of spiked balls to be missing on the GameCube:
    Code (C):
    1. ano = actionwk->actfree[20];
    2. if (actwk[ano].actno != 45)
    3. {
    4.   frameout(actionwk);
    5.   return;
    6. }
     
    • Informative Informative x 3
    • Like Like x 2
    • List
  19. PimpUigi

    PimpUigi

    ------- Route Magician ------- Sonic CD Technician Banned
    I thought you guys might find this object fun to talk about:
     
  20. BenoitRen

    BenoitRen

    Tech Member
    793
    389
    63
    It's not clear to me what you're trying to show us.