don't click here

ASM Experimental Mega CD Error Handler (plus other Mode 1 tools)

Discussion in 'Engineering & Reverse Engineering' started by OrionNavattan, Aug 8, 2023.

  1. OrionNavattan


    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.

    Code (ASM):
    3. ; -------------------------------------------------------------------------
    4. ; Check if sub CPU has crashed
    5. ; -------------------------------------------------------------------------
    7. checksubCPU:   macro dest
    8.        cmpi.b   $FF,(mcd_sub_flag).l       ; has sub CPU crashed?
    9.        beq.w   \dest               ; branch if so
    10.        endm
    12. ; -------------------------------------------------------------------------
    13. ; Send a command to the sub CPU
    14. ; -------------------------------------------------------------------------
    16. SubCPUCommand:
    17.        checksubCPU SubCPUCrash
    18.        move.w   d0,(mcd_maincom_0).l           ; send the command
    20.    .wait_subCPU:
    21.        checksubCPU SubCPUCrash
    22.        move.w   (mcd_subcom_0).l,d0           ; has the sub CPU received the command?
    23.        beq.s   .wait_subCPU           ; if not, wait
    24.        cmp.w   (mcd_subcom_0).l,d0       ; is it processing the current command?
    25.        bne.s   .wait_subCPU           ; if not, wait
    27.        move.w   #0,(mcd_maincom_0).l           ; mark as ready to send commands again
    29.    .wait_subCPU2:
    30.        checksubCPU SubCPUCrash
    31.        move.w   (mcd_subcom_0).l,d0           ; is the sub CPU done processing the command?
    32.        bne.s   .wait_subCPU2           ; if not, wait
    33.        rts      
    35. ; -------------------------------------------------------------------------
    36. ; Wait for Word RAM access
    37. ; -------------------------------------------------------------------------
    39. WaitWordRAMAccess:
    40.        checksubCPU   SubCPUCrash
    41.        btst   #0,GAMEMMODE           ; do we have Word RAM access?
    42.        beq.s   WaitWordRAMAccess       ; if not, wait
    43.        rts
    45. ; -------------------------------------------------------------------------
    46. ; Give Sub CPU Word RAM access
    47. ; -------------------------------------------------------------------------
    49. GiveWordRAMAccess:
    50.        checksubCPU   SubCPUCrash
    51.        bset   #1,GAMEMMODE           ; give Sub CPU Word RAM access
    52.        beq.s   GiveWordRAMAccess       ; branch if it has not been given
    53.        rts      
    56. ; -------------------------------------------------------------------------
    57. ; If sub CPU has crashed
    58. ; -------------------------------------------------------------------------
    60. SubCPUCrash:
    61.        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.)

    Code (ASM):
    2. ; ---------------------------------------------------------------
    3. ; If the BIOS is used, the user init routine of the sub CPU
    4. ; program will need to set up the jump table entries for the
    5. ; exception vectors. The following is an example of how to do this.
    6. ; ---------------------------------------------------------------
    8. SPInit:      
    9.        lea ExceptionPointers(pc),a0 ; pointers to exception entry points
    10.        lea _AddressError(pc),a1   ; first error vector in jump table
    11.        moveq   #9-1,d0           ; 9 vectors total
    13.    .vectorloop:
    14.        addq.l   #2,a1       ; skip over instruction word
    15.        move.l   (a0)+,(a1)+   ; set table entry to point to exception entry point
    16.        dbf d0,.vectorloop   ; repeat for all vectors
    17.        rts
    19. ExceptionPointers:
    20.        dc.l AddressError
    21.        dc.l IllegalInstr
    22.        dc.l ZeroDivide
    23.        dc.l ChkInstr
    24.        dc.l TrapvInstr
    25.        dc.l PrivilegeViol
    26.        dc.l Trace
    27.        dc.l Line1010Emu
    28.        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.
    Screenshot 2023-08-07 at 20.41.10.png
    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.
  2. Devon


    Powered by a malfunctioning Motorola 68000 Tech Member
    your mom
    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.
    • Informative Informative x 1
    • List
  3. OrionNavattan


    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.
    • Informative Informative x 2
    • List