Not Rocket Science

Why does Altered Beast need the CD-ROM² System Card 1.0?

When it comes to PC Engine CD-ROM² System Card compatibility, the answer is usually "The Super System Card 3.0 will play every game... except for one". That exception is 1989's PC Engine CD-ROM² release of Altered Beast (Juuouki), which will hang once you transform into the beast and the game tries to load a new chunk of the level. The game is not frozen - music still plays and controls are still responsive, it just does not scroll any further on any System Card other than 1.0.

Altered Beast hang

This only happens if you transformed to a beast - you can be at zero, one, or two power ups without any problems, but once you get the third power up and turn into a beast, the next level chunk load will fail and the screen will not scroll anymore. This always happens - you can trigger this two minutes into level 1 by picking up the first three powerups, or you can miss one and keep playing until you pick up the third powerup later and then have the screen scrolling/level loading break later in the level.

Using Mesen2's movie recording and scripting features

The Mesen2 emulator has a Movie recording feature that is somewhat confusingly named: It doesn't record a movie, it records an initial Save State and the inputs. The .mmo file is actually a .zip file:

Mesen2 .mmo contents

I recorded a playthrough on both the System Card 1.0 and 2.0. The game is deterministic - replaying the movie always yielded the same results.

Mesen2 has a Scripting Window where I can create a LUA script which allowed me to look at calls to cd_read, which is the System Card BIOS function to load additional data from the CD (JSR E009).

local frame = 0

emu.addEventCallback(function() frame = frame + 1 end, emu.eventType.endFrame)

emu.addMemoryCallback(function(addr, value)
    local _AL = emu.read(0xF8, emu.memType.pceWorkRam, false)
    local _AH = emu.read(0xF9, emu.memType.pceWorkRam, false)
    local _BL = emu.read(0xFA, emu.memType.pceWorkRam, false)
    local _BH = emu.read(0xFB, emu.memType.pceWorkRam, false)
    local _CL = emu.read(0xFC, emu.memType.pceWorkRam, false)
    local _CH = emu.read(0xFD, emu.memType.pceWorkRam, false)
    local _DL = emu.read(0xFE, emu.memType.pceWorkRam, false)
    local _DH = emu.read(0xFF, emu.memType.pceWorkRam, false)

    local record = (_CL << 16) | (_CH << 8) | _DL
    local type_names = {
        [0x01] = "LOCAL",
        [0xFE] = "VRAM",
        [0xFF] = "VRAM",
    }
    local type_name = type_names[_DH] or string.format("MPR%d", _DH)

    emu.log(string.format("f=%d cd_read REC=$%06X TYPE=%s($%02X) _AX=$%02X%02X _BX=$%02X%02X", frame, record, type_name, _DH, _AH, _AL, _BH, _BL))
end, emu.callbackType.exec, 0xE009)

emu.log("Logging cd_read")

Using this, I could see that the System Card 2.0 just stops making the cd_read call for the next level chunk once you're transformed into a beast - it just never gets dispatched.

So there's the symptom, but now the question is: Why?

What causes the game to load the next level chunk?

Before answering "Why doesn't it load?", the question is actually "What causes the game to load the next chunk?". If you play the game regularly, you undoubtedly run into moments where the game just stops for a few seconds as the CD loads new data, and then continues. How does it decide to do that?

I knew from looking at the Mesen debugger that the game was stuck in the per-frame VBlank handler, and found code starting at $A7FF that checks for two things:

It's the second check that never succeeds on System Card 2.0. Before transforming to the beast, the value is $43 on both System Card 1.0 and 2.0, which is 0100 0011 in binary, so bit 6 is set.

At some point, the System Card 1.0 sets $2904 to $03, thus clearing bit 6 and making the check pass. This never happens on System Card 2.0 when you're a beast.

I've traced writes to memory address $2904 and both 1.0 and 2.0 change it the same way while you're untransformed, but as soon as you're the beast, the System Card 2.0 fails to clear bit 6, so the game never gets through this code:

LDA $2904    ; $A825
AND #$C0     ; $A828 - mask 11xxxxxx
BEQ $A848    ; $A82A - if $2904 & $C0 == 0, load next level chunk
RTS          ; $A82C - otherwise wait until the next frame

This also explains why the game isn't frozen and still accepts controls and plays music: The main game loop is still running, it's just not getting the "go ahead and load the next chunk!" signal.

What clears bit 6 in $2904?

Now that we know what is (not) happening, the next question becomes "Whose job is it to clear the bits?"

The answer is in $4019:

LDA $2904,X    ; Note the X register index!
AND #$BF       ; 1011 1111 - Clear bit 6
STA $2904,X

Normally, this starts with the X register being $00, therefore starting at memory address $2904. But on System Card 2.0, X started at $01 once I was a beast - so the clear loop starts at $2905 and misses $2904.

What calls $4019?

Phew, that is a lot of tracing but it boils down to "The game calls cd_read and later on uses X to index without ever setting X to $00".

What changed in cd_read in System Card 2.0?

I've tried to trace the cd_read method in the System Cards, and it looks like it was completely rewritten. Code differences between 2.0, 2.1, and 3.0 are pretty minor, but code differences between 1.0 and 2.0 are massive. For the moment, I've given up finding the exact place that causes the X register change because the method looks nothing like System Card 1.0's method and I'm already too deep in another rabbit hole.

Looking at the Mesen logs, I did notice that cd_read does set X to different values even on System Card 1.0. The _DH argument ("Data read address type") affects the result: When targeting VRAM, X=$FF in System Card 1.0 and X=$00 in 2.0. If targeting LOCAL or MPR, X=$00 on 1,0 and X=$01 on 2.0.

I did check the V1.00 BIOS Manual and cd_read makes no claims about the X register, only that it returns the status in A ($00 on success, and a sub-error code otherwise).

Why does it only happen after the beast transformation?

If X == $01 happens all the time on System Card 2.0, why is it only broken in beast mode? Because the code at $4019 is only called when you're a beast.

The main game loop calls code that branches based on whether bit 7 in zero page memory $3E is set or not:

LDA $3E       ; $3982
BPL $3989     ; $3984 - Is bit 7 set?
JMP $3A0F     ; $3986 - If set, jump to $3A0F - good path
ROL A         ; $3989 - If clear, we're here and in trouble
...   
JSR $3A75     ; $399D - Eventually calls cd_read, which changes X
...   
JSR $4000     ; $39AA - Gets to the infinite loop at $4019

Okay, so if bit 7 in $3E is clear, we're in trouble. What sets or clears bit 7 in $3E? The main game loop does, it calls JSR $4003, which eventually lands here:

LDA $2700,X   ; $66C9 - Observed either $01, $08 or $09
CMP #$01      ; $66CC - Not powered up or power up 1
BEQ $66D9     ; $66CE - Code at $66D9 sets bit 7
CMP #$08      ; $66D0 - After power up 2
BEQ $66D9     ; $66D2 - Code at $66D9 sets bit 7
LDA #$40      ; $66D4 - Beast, clear bit 7 - trouble!
STA $3E       ; $66D6
RTS           ; $66D8

I'm not 100% what $2700 is, could be the sprite index (frankly, at this point I wasn't overly interested in digging any deeper anymore), but I observed it to be $01 when I'm not powered up or after the first power up, $08 after the second power up, and finally $09 after the beast transformation.

In summary

Post Tags
PC Engine