Monday, August 7, 2017

Garbage in, garbage out

New to Sonic 3 are various different kinds of objects which take over Sonic's movement and rotate him using a series of 3D rotation sprites. Rather than do this through animations, these objects modify Sonic's mapping frame directly so he's always in the correct pose, depending on his distance to the center of the object.


Should you enter debug mode while riding one of these objects however, everything catches on fire pretty quickly, with some emulators outputting garbage sprites to the screen, where as other emulators, as well as hardware, mysteriously reset back to the SEGA screen.


So what's going on? Well, right off the bat, when you enter object placement mode, the first thing that happens is that the player character's mappings and art pointer are backed up to somewhere else in RAM.
DebugMode:
    moveq   #0,d0
    move.b  (Debug_placement_mode).w,d0
    move.w  off_92A1C(pc,d0.w),d1
    jmp     off_92A1C(pc,d1.w)
; ---------------------------------------------------------------------------
off_92A1C:  dc.w loc_92A20-off_92A1C
            dc.w loc_92AB0-off_92A1C
; ---------------------------------------------------------------------------

loc_92A20:
    addq.b  #2,(Debug_placement_mode).w
    move.l  $C(a0),($FFFFFFCA).w        ; save mappings to $FFCA
    cmpi.b  #6,5(a0)
    bhs.s   loc_92A38
    move.w  $A(a0),($FFFFFFCE).w        ; save art tile to $FFCE

loc_92A38:
    ...
The reason for this is that, in order to display the preview sprite for the currently selected object, the player's mappings and art are temporarily replaced with those of the target object. The player's mapping frame is also changed in order to pick the appropriate sprite from the sprite mapping definitions.

If this isn't setting off alarm bells already, it should. Remember how the 3D rotation objects would rather mess with the player's mapping frame directly? Well, the Carnival Night Zone barrel above, as an example, animates Sonic by setting his mapping frame to a value between $55 and $5B. These correspond to the standing rotation sprites as seen below.


Ordinarily, what happens is that a mapping frame of $55 gets multiplied by two, and the result, $AA, is added to Sonic's base mappings address in order to retrieve the pointer to the actual mapping definition for mapping frame $55.

However, when we enter object placement mode, Sonic's mappings pointer is replaced with the pointer for the currently selected object, for instance, a ring. Now, a ring has nine whole frames of animation, so when we add $AA to the base mappings address for rings at $1A99A, we end up with the value $1AA44, which is well past all the mapping definitions and their pointers, and smack dab in the middle of the code for the rings from the slot machine bonus cage.
Map_Ring:   dc.w word_1A9AC-Map_Ring    ; 1A99A: 0012
            dc.w word_1A9B4-Map_Ring    ; 1A99C: 001A
            dc.w word_1A9BC-Map_Ring    ; 1A99E: 0022
            dc.w word_1A9C4-Map_Ring    ; 1A9A0: 002A
            dc.w word_1A9CC-Map_Ring    ; 1A9A2: 0032
            dc.w word_1A9D4-Map_Ring    ; 1A9A4: 003A
            dc.w word_1A9DC-Map_Ring    ; 1A9A6: 0042
            dc.w word_1A9E4-Map_Ring    ; 1A9A8: 004A
            dc.w word_1A9EC-Map_Ring    ; 1A9AA: 0052
word_1A9AC: dc.w 1
            dc.b $F8,   5,   0,   0, $FF, $F8
word_1A9B4: dc.w 1
            dc.b $F8,   5,   0,   4, $FF, $F8
word_1A9BC: dc.w 1
            dc.b $F8,   1,   0,   8, $FF, $FC
word_1A9C4: dc.w 1
            dc.b $F8,   5,   8,   4, $FF, $F8
word_1A9CC: dc.w 1
            dc.b $F8,   5,   0,  $A, $FF, $F8
word_1A9D4: dc.w 1
            dc.b $F8,   5, $18,  $A, $FF, $F8
word_1A9DC: dc.w 1
            dc.b $F8,   5,   8,  $A, $FF, $F8
word_1A9E4: dc.w 1
            dc.b $F8,   5, $10,  $A, $FF, $F8
word_1A9EC: dc.w 0
; ---------------------------------------------------------------------------

loc_1A9EE:
    moveq   #0,d0                       ; 1A9EE: 7000
    move.b  5(a0),d0                    ; 1A9F0: 1028 0005
    move.w  off_1A9FC(pc,d0.w),d1       ; 1A9F4: 323B 0006
    jmp     off_1A9FC(pc,d1.w)          ; 1A9F8: 4EFB 1002
; ---------------------------------------------------------------------------
off_1A9FC:  dc.w loc_1AA02-off_1A9FC    ; 1A9FC: 0006
            dc.w loc_1AA56-off_1A9FC    ; 1A9FE: 005A
            dc.w loc_1AA62-off_1A9FC    ; 1AA00: 0066
; ---------------------------------------------------------------------------

loc_1AA02:
    moveq   #0,d1                       ; 1AA02: 7200
    move.w  $3C(a0),d1                  ; 1AA04: 3228 003C
    swap    d1                          ; 1AA08: 4841
    move.l  $34(a0),d0                  ; 1AA0A: 2028 0034
    sub.l   d1,d0                       ; 1AA0E: 9081
    asr.l   #4,d0                       ; 1AA10: E880
    sub.l   d0,$34(a0)                  ; 1AA12: 91A8 0034
    move.w  $34(a0),$10(a0)             ; 1AA16: 3168 0034 0010
    moveq   #0,d1                       ; 1AA1C: 7200
    move.w  $3E(a0),d1                  ; 1AA1E: 3228 003E
    swap    d1                          ; 1AA22: 4841
    move.l  $38(a0),d0                  ; 1AA24: 2028 0038
    sub.l   d1,d0                       ; 1AA28: 9081
    asr.l   #4,d0                       ; 1AA2A: E880
    sub.l   d0,$38(a0)                  ; 1AA2C: 91A8 0038
    move.w  $38(a0),$14(a0)             ; 1AA30: 3168 0038 0014
    lea     Ani_Ring(pc),a1             ; 1AA36: 43FA 002E
    bsr.w   Animate_Sprite              ; 1AA3A: 6100 01AC
    subq.w  #1,$40(a0)                  ; 1AA3E: 5368 0040
    bne.w   Draw_Sprite                 ; 1AA42: 6600 0182      <---------
    movea.l $2E(a0),a1                  ; 1AA46: 2268 002E
    subq.w  #1,(a1)                     ; 1AA4A: 5351
    bsr.w   GiveRing                    ; 1AA4C: 6100 FB48
    ...
We're now trying to read garbage information as mappings data, which explains the digital vomit seen in emulators, but in reality, it's even worse. The word value at $1AA44 is 0001 which, when added to $1A99A, results in $1A99B, an odd address. Odd not as in peculiar, but indivisible by 2.
loc_1ADB2:
    ...
    movea.l mappings(a0),a1
    moveq   #0,d4
    btst    #5,d6                       ; is the static mappings flag set?
    bne.s   loc_1ADD8                   ; if it is, branch
    move.b  mapping_frame(a0),d4
    add.w   d4,d4
    adda.w  (a1,d4.w),a1
    move.w  (a1)+,d4                    ; <--------- <--------- <---------
    subq.w  #1,d4                       ; get number of pieces
    ...
The Render_Sprites function proceeds to read the sprite piece count from address $1A99B. This results in an attempt to read a word value from an odd address, which is an illegal operation on the 68000 processor. An exception is raised, which is caught by Sonic 3's exception handler, and the game resets in response to the unexpected catastrophe.

Emulators which ignore this hardware rule in favor of increased performance don't raise this exception, so the garbage information is successfully read, resulting in garbage sprites being displayed on screen.

6 comments:

  1. Hey. Do you think you could provide some guidance with this:

    http://forums.sonicretro.org/index.php?showtopic=34181&pid=901848&st=75&#entry901848

    ReplyDelete
    Replies
    1. Actually, I have zero experience with the PC port. I might end up having to take a look at it though, because there are so many things interesting with that screenshot. The checkerboard shadows! The color blending! The atrocious desync!

      Delete
  2. Somewhat related (but not really to Sonic 3), is the bad art pointer a similar reason to why placing an object while dead in Sonic 2 crashes the game?

    ReplyDelete
    Replies
    1. That's actually a different thing. When you die, the game stops processing object code so that every sprite other than Sonic freezes in place. Spawning new objects in this state clearly breaks something; Sonic 3 simply resumes processing objects as soon as you enter debug placement mode.

      Delete
  3. Isn't it actually supposed to "lock up" in the original Sonic 3 error handler, but re-init the game in SK/S3K?

    ReplyDelete
  4. I've experienced this too; isn't it _weird_ looking?

    ReplyDelete