When I started my research for a Mode 1 port of Sonic CD, one of the first things I thought about was some way to deal with exceptions on the Mega CD’s sub CPU (a necessity given the large amount of code that game runs on the sub CPU). I quickly discovered that in the nearly 12 years since the existence of Mode 1 was publicized, no one has ever bothered to write an exception handler for the Mega CD's sub CPU, so I did what any driven and motivated hacker does, and did the research on what would be necessary to implement one based on Vladikcomper’s excellent error handler. Exception handling on the sub CPU is somewhat more difficult than on the main CPU, to the point where there is unfortunately no universal, simple and easy drop-in solution. The sub CPU does not have any access to the main CPU’s address space, let alone the VDP; consequently, any exception handler for the sub CPU will inevitably require the involvement of the main CPU to actually output the exception information to the screen. A conversation with Vladikcomper about how to process the exception led to a couple of ideas (one of which may well be worth exploring later on given the limitations of what I came up with), but ultimately I decided on a fairly straightforward solution. Since processing an exception ultimately amounts to reading dumped registers and the exception frame on the stack, and because the main CPU can directly access the sub CPU’s stack, why not just let the main CPU process the exception? In the system I’ve devised, the error handler module on the sub CPU simply dumps the registers and gives the stack pointer to the main CPU via one of the communication registers, then the main CPU processes the exception by reading directly from sub CPU’s stack (with the console subsystem still running on the main CPU stack). Triggering the main CPU component of the handler brings up another problem: there is no way for the sub CPU to interrupt or otherwise actively signal the main CPU. The main CPU will need to actively monitor the status of the sub CPU and enter the handler on its own if the sub CPU crashes. There is unfortunately no easy, universal solution here; the best we can do is have the sub CPU place a sentinel value in one of the communication registers and wait for the main CPU to notice it. The main CPU will need to check for this sentinel value anytime it needs to interact with or wait on the sub CPU, including before sending a command, giving the word RAM, and at each VBlank before triggering the MD interrupt (aka. Level 2). Any wait loops where the main CPU waits on the sub CPU to finish (waiting on commands to process and waiting for the word RAM) will also need to incorporate checks for the crash sentinel. In any case, if the crash sentinel is found, the main CPU should enter the error handler. Spoiler: Examples of how to check for a crash sentinel Code (ASM): ; ------------------------------------------------------------------------- ; Check if sub CPU has crashed ; ------------------------------------------------------------------------- checksubCPU: macro dest cmpi.b $FF,(mcd_sub_flag).l ; has sub CPU crashed? beq.w \dest ; branch if so endm ; ------------------------------------------------------------------------- ; Send a command to the sub CPU ; ------------------------------------------------------------------------- SubCPUCommand: checksubCPU SubCPUCrash move.w d0,(mcd_maincom_0).l ; send the command .wait_subCPU: checksubCPU SubCPUCrash move.w (mcd_subcom_0).l,d0 ; has the sub CPU received the command? beq.s .wait_subCPU ; if not, wait cmp.w (mcd_subcom_0).l,d0 ; is it processing the current command? bne.s .wait_subCPU ; if not, wait move.w #0,(mcd_maincom_0).l ; mark as ready to send commands again .wait_subCPU2: checksubCPU SubCPUCrash move.w (mcd_subcom_0).l,d0 ; is the sub CPU done processing the command? bne.s .wait_subCPU2 ; if not, wait rts ; ------------------------------------------------------------------------- ; Wait for Word RAM access ; ------------------------------------------------------------------------- WaitWordRAMAccess: checksubCPU SubCPUCrash btst #0,GAMEMMODE ; do we have Word RAM access? beq.s WaitWordRAMAccess ; if not, wait rts ; ------------------------------------------------------------------------- ; Give Sub CPU Word RAM access ; ------------------------------------------------------------------------- GiveWordRAMAccess: checksubCPU SubCPUCrash bset #1,GAMEMMODE ; give Sub CPU Word RAM access beq.s GiveWordRAMAccess ; branch if it has not been given rts ; ------------------------------------------------------------------------- ; If sub CPU has crashed ; ------------------------------------------------------------------------- SubCPUCrash: trap #0 ; enter the sub CPU error handler Finally, if the BIOS is in use, the sub CPU will need to do a bit of setup work during its initialization to enable the error handler. The BIOS, and with it the sub CPU’s vector table, are (normally) write protected, so error handling support is provided by having the error vectors point to entries in the jump table (specifically, nine consecutive entries starting at $5F40). By default these are set so that the sub CPU reboots if an exception occurs, but the user program can modify them to instead point to an exception handler. This thankfully is fairly simple to do, requiring only six instructions if a dbf loop is used. (If you’re being daring and making a sub CPU program without the BIOS, then you can simply place the exception pointers in the vector table as you would on the main CPU.) Spoiler: Setting up exception vectors Code (ASM): ; --------------------------------------------------------------- ; If the BIOS is used, the user init routine of the sub CPU ; program will need to set up the jump table entries for the ; exception vectors. The following is an example of how to do this. ; --------------------------------------------------------------- SPInit: lea ExceptionPointers(pc),a0 ; pointers to exception entry points lea _AddressError(pc),a1 ; first error vector in jump table moveq #9-1,d0 ; 9 vectors total .vectorloop: addq.l #2,a1 ; skip over instruction word move.l (a0)+,(a1)+ ; set table entry to point to exception entry point dbf d0,.vectorloop ; repeat for all vectors rts ExceptionPointers: dc.l AddressError dc.l IllegalInstr dc.l ZeroDivide dc.l ChkInstr dc.l TrapvInstr dc.l PrivilegeViol dc.l Trace dc.l Line1010Emu dc.l Line1111Emu Experimental Mega CD Error Handler: Today, building on the above research, I present an experimental modification of Vladikcomper’s Error Handler 2.0 that adds basic support handling exceptions on the Mega CD sub CPU. I will be upfront in that is not the most elegant or flexible solution; since I wrote it around the BIOS, it requires that the sub CPU’s stack be in the first 128 KB of the program RAM and that the stack has not been moved from its default location. It also does not support symbol tables for the sub CPU program (though they, along with console programs, should still be usable on the main CPU side). It is also assembled entirely from source, rather than preassembled as is the case with VladikComper’s bundles. I’ve done what I can to make it as easy to install and use as possible, but ultimately it will be up to the user to ensure their interprocessor communication incorporates a crash sentinel value and checks for it, as well as setting up the exception vectors in the jump table. At the end of the day though, it appears to work for what I wrote it for, and I'm sure that others will find it useful. Included in the repository are four test ROMs that trigger address and illegal instruction exceptions on the main and sub CPUs. These has been tested and confirmed working correctly in Blastem and on real hardware (North American Model 1 VA2 + North American Sony Model 2), but I would deeply appreciate it people could test them on other Mega CD hardware configs and compatibles, and report if something doesn't work. (The ROMs will end on a red screen if no Mega CD is found, or on a blue screen if the BIOS is not recognized.) The handler does not work quite right in Genesis Plus GX due to an accuracy issue (namely, GX doesn’t emulate address exceptions on the sub CPU, allowing them to succeed as if nothing happened). I do plan to rebase the entire thing on the new Error Handler 2.5 at some point down the line, but this will work for now. Bonuses - Compressed Sub CPU Code and Mega CD Initialization: The test ROMs included with the handler also test two other things: my attempt at writing unified initialization code for Mode 1, derived from Devon’s excellent library, and, based on a suggestion from @MarkeyJester, a custom variant of Kosinski Moduled compression I devised to allow the sub CPU code to be compressed in ROM, such that program RAM bankswitches always occur between modules. (Thank you to @Clownacy for the modified ClownLZSS compressor.) The source for the test ROMs, including both of the aforementioned bonuses, can be found here.
I actually raised an issue back in 2021 when I was toying around with making my own error handler. The functionality is there, but it was disabled by default, because the emulator is primarily designed to run with good performance on Gamecube/Wii. After discussion, it's been enabled for the libretro and SDL versions. The version of GPGX used on BizHawk is unfortunately ancient, though.
Did a bit of further testing, and it seems this is a bug specific to the libretro version; the OpenEmu version of GPGX (which is only a little bit behind the libretro version) handles sub CPU address errors correctly. I've reported the issue on the LibRetro repo. Amusingly enough, mistakes I made when developing the handler revealed an upstream inaccuracy in GPGX. I initially used "stop #$2700" to halt the sub CPU instead of trapping in an infinite loop, which as it turns out is a huge no-no as that prevents the CPU from responding to bus requests. On real hardware, this causes the main CPU to hang waiting for a response, but GPGX continues as if the bus request succeeded. I've reported that as well.