Skip to content

Internal ROM

Javier de Silóniz Sandino edited this page Mar 13, 2022 · 19 revisions

SVP Internal ROM

The SVP chip has an internal ROM containing around 2KB of SSP1601 code (and some static data), as well as some interrupt vectors. A small part of the code contained here is run at bootup, matching the RESET interrupt vector.

Accessing the internal ROM data

This internal ROM data can be accessed from the SVP code (on real hardware, this behavior isn't emulated as of now) by reading from addresses 0xFC00 to 0xFFFF (that usually should be mapped to the final part of the ROM containing SVP code) in the internal PRAM view. i.e.:

ldi a, 0xFC08   # initial address for boot-up code
ld x, (a)       # reading first word and store it in register X

The actual code that allowed me to find this internal ROM can be found here.

Reads from the external memory space in that area should yield the actual contents in the ROM chip from the cartridge (Note: I still need to verify this myself).

Interrupt vectors

The SSP1601 can handle four interrupt vectors. Each one is mapped to the following addresses (which are read by the DSP at addresses 0xFFF8, 0xFFFA, 0xFFFC and 0xFFFE):

Interrupt Description Vector
RESET executed at startup 0xFC08
INT0 User Interrupt 0 0x03FA
INT1 User Interrupt 1 0x03FC
INT2 User Interrupt 2 0x03FE

Notice that the latter three are stored in IRAM (Instruction RAM) space. The opcodes for these are written to IRAM during the boot-up process that starts at 0xFC08.

Disassembly

This section contains a full description of the internal ROM inside the SVP, and tries to explain with some detail what each part of the code does. If you want to access the complete disassembly you can find it here.

Labels have been added to important memory locations (i.e.: start of routines, areas referenced by other code, etc...).

The Internal ROM contains multiple sections. They can be described as follows:

Interrupt handling and boot-up process

The first part of the internal ROM handles interruptions and the boot-up process (started by the RESET interrupt vector). Interesting points:

  • All user interrupts end up pointing to an interrupt handling routine at address 0xFC04. This routine reestablishes interrupt handling and basically seems to do nothing. This matches previous understanding that Virtua Racing doesn't use interrupts. But keeping these in IRAM gives option to others to handle interrupts in any way they want.

  • There's a security mechanism similar to TMSS, which I think is mainly intended to disallow unlicensed games (or avoid issues with a potential SVP passthrough cartridge holding "normal" games) to be run on the SVP. As previously suspected, this checks the notes field in the ROM header and checks for the "SV" string (as in SEGA Virtua) among other small checks. Failing to fulfill this and the SVP enters an infinite loop at address 0xFC00. Another small check is performed but seems to be less strict and seems intended to allow for multiple valid values in the ROM header.

  • Another question that was raised in the past is how the SVP knows where the game code starts. In Virtua Racing the entry point address can be found following the "SV" security string in the ROM header. This is actually checked by the Internal ROM and this is the code that's responsible for this. This means that the entry point is arbitrary (even though it doesn't make much sense to use other values, 0x0400 allows for the longest chunk of ROM space accessible by the internal program view (as 0x0000-0x03FF are mapped to IRAM).

  • Multiple external registers seem to be initialized here, others aren't. The (supposed) initialization process varies from one external register to another.

  • Also external register EXT5 is used during this part of the code, while totally unused (and undocumented so far) in Virtua Racing. If you know what EXT5 please let me know!

  • There are two words, right after what I call the infinite loop of death (at 0xFC02) that doesn't seem like meaningful code, and so far I haven't found any reference to them in what I've disassembled so far.

The whole process happens as follows:

infinite_loop_FC00:
            ld pc, 0xFC00              # infinite loop to get stuck after this ROM isn't identified as a valid SVP one.

loc_FC02:   # Not sure if the following two are even real instructions... haven't found a reference call to these anywhere yet.
            # Possibly a ROM version number and checksum?
            dw 0001                     
            dw 95EF

loc_FC04:   # This is the default interrupt handling routine for INT0, INT1 and INT2,
            # referenced in IRAM 0x3FA, 0x3FC and 0x3FE after bootup.
            # Which seems to not be doing that much, interrupts seem totally ignored.
            # Good thing about user interrupts living in IRAM is that game code can 
            # patch these to adapt a better interrupt strategy if required.

            dw 0000                     # NOP
            dw 0000                     # NOP
            mod f, setie                # activate interruptions
            ret                         # return from interrupt

loc_FC08:   # Reset interrupt handler - SVP boot-up sequence.
            ld st, 0000                 # Clean ST
            
            # External register initialization.
            # It's interesting how they all seem to be accessed in different ways,
            # also how not all of them are initialized at all.
            # (EXT6 is used right away after this with no initialization whatsoever.)

            ld -, ext5                  # Initializing the mysterious EXT5?
            ld a, ext0                  # Initializing EXT0/XST_State?
            ld ext0, a
            ld ext0, 0000
            ld -, ext7                  # The lower word of the accumulator register is external to the DSP
            ld -, ext7                  # I guess these dummy reads are required to initialize it?
            ld a, 0000
            ld ext7, 0000               # Initial value for accumulator

            # Writing interrupt vector handlers for INT0, INT1, and INT2.
            # They just put a "bra always, 0xFC04" in 0x3FA, 0x3FC and 0x3FE.
            # These addresses are referenced in the interrupt vectors for these
            # interrupts in IROM at addresses 0xFFFD, 0xFFFE and 0xFFFF.

            ld ext6, 83FA
            ld ext6, 081C               # Programming PM to access 0x1C83FA with autoincrement (0x3FA in IRAM).
            ld ext4, -                  # PM4 active for writes
            ld x, 0860                  # bra always,...
            ld y, FC04                  # ... 0xFC04
            ld ext4, x
            ld ext4, y                  # Write for 0x3FA
            ld ext4, x
            ld ext4, y                  # Write for 0x3FC
            ld ext4, x
            ld ext4, y                  # Write for 0x3FE
            
            # Security: a basic version of TMSS, which seems to be in place to avoid the release of unlicensed
            # games for the SVP (or perhaps thinking about a "regular" games running on a passthrough cartridge?)

            ld ext6, 00E4
            ld ext6, 0800                   # Accessing ROM header (address 00E4 with autoincrement)
            ld -, ext4                      # PM4 active for reads
            ld a, ext4
            cmpi a, 5356                    # comparison with "SV" - as in "SEGA Virtua"? - offset 1C8 in header: "SV" in the notes field
            bra z=0, @infinite_loop_FC00    # What are you trying? to sell your own SVP games? HA!
            ld a, ext4                      # Let's keep reading the header
            ld x, a
            andi a, 03FF                    # mask 10 LSB
            bra z=1, 0xFC38                 # is this discriminating among different possible bootup sequences/chip versions?
            eori a, 0001
            bra z=0, @infinite_loop_FC00    # Another check

loc_FC38: 
            ld a, x                     # They kept the value of the word after "SV" in the header area in X.
            andi a, 1C00                # mask bits 11, 12, 13
            ori a, E000                 
            ld ext0, a                  # value E000 seems to end up written in XST_state register, weird 
                                        # - didn't know you could write into it - is this part of its initialization?
            ld a, ext4                  # read next word from the header - but it doesn't get used after this?
  
            # Initial values for the registers before jumping to the main code in the software:
            ld a, 0000
            ld x, a
            ld y, a
            ld st, a
            ld r0, a
            ld r1, a
            ld r2, a
            ld r4, a
            ld r5, a
            ld r6, FC                   # Pointer for "stacked" calls to subroutines - see next section.

            # Game code entry point.
            # Next word from the header in the ROM is the entry point address (0x0400 in Virtua Racing). We just jump to it.
            ld pc, ext4

Functions library

A collection of small routines that can be called from other parts of the code. These include:

  • Data fill routines, some targetting IRAM and others allowing more flexibility to write to DRAM too.
  • Math subroutines, mainly used from larger math routines. Seemingly intented for fixed-point numbers.
  • Math routines, intended to be called from the game code. These rely heavily on fixed-point number addition/multiplication as well as sine/cosine calculations.

These routines have really funny details. For instance, instead of relying on CALL and RET instructions and be used as regular subroutines, these expect the return address to be introduced in r6 and jump to the function call directly with a simple BRA ALWAYS instruction. I suspect that's part of the reason on why r6 is initialized in such a funny way in the boot-up process.

Note: I'm in no way an expert programmer with assembly, especially when it comes to math. I've made an effort to document the disassembled code as best as possible but some of the high-level intention of these routines has escaped me. If you have a better understanding than what's there in the comments please reach out to me so we can improve the quality of these. In any case, I'll try to run these in real hardware one of these days and try to confirm/improve my findings.

Data fill routines disassembly

loc_FC4B:   # IRAM fill routine. Takes a word from ROM - ((r7||00)) as an offset to IRAM. This offset also serves
            # as a counter during the data write process. It's used to copy data from the internal program memory.
            # Possibly intended for use to load dynamic code, but Virtua Racing provides its own routines for that.
            # Not used by other routines, meant to be called from game code.

            ld a, ((r7|00))
            ori a, 8000
            ld ext6, a
            ld ext6, 081C      
            ld ext4, -              # Program writes against IRAM with autoincrement (offset in a)
            ld a, ((r7|00))         # First accessed word is considered the size of the area to copy.
loc_FC53:   ld ext4, ((r7|00))      # After that, the pointer provided by r7||00 will autoincrement to
            subi 0x01               # write the whole routine back to IRAM.
            bra n=0, 0xFC53         # Write data until we reach 0.
            ld pc, (r6+!)           # RET!
            
            
loc_FC58:   # This routine also writes to IRAM but seems a bit more convoluted than the one in 0xFC4B.
            # Location of the offset for the IRAM fills is in external memory, and its location can
            # be specified by two values in (r7||00) and (r7||01). This gives a bit more flexibility,
            # being able to read the offset value from ROM space, IRAM or DRAM.
            #
            # Not used by other routines, meant to be called from game code.

            ld ext6, (r7|01)
            ld a, (r7|00)
            ori a, 0800         # Set autoincrement - not sure why, it doesn't need it (only reads one value).
            ld ext6, a          # Programming PM4 for reads, address is a composite from (r7||00) and (r7||01)
            ld -, ext4
            ld a, ext4          # Read offset
            ori a, 8000         # Formatting the offset we got to match IRAM address space
            ld ext6, a
            ld ext6, 081c
            ld ext4, -          # Programming writes to IRAM...
            ld a, ext4          # Tasco Deluxe's docs don't specify what happens if you read from PM4 if it's programmed to write.
                                # Need to research this.
loc_FC66:
            ld x, ext4          # Again - unknown at this point what reading again from EXT4 would do here.
            ld ext4, x          # But we want to write whatever came from up into IRAM!
            subi 0x01           
            bra n=0, 0xFC66     # Loop until we reach offset 0.

            ld pc, (r6+!)       # RET!


            # Now there are some different data fill routines. They assume that EXT4 as been programmed
            # to access a certain address. A counter is setup in either r0 or r7, and source/data is
            # taken from the other memory bank (i.e.: counter in r7, source in r0).
            # As the rest of subroutines, r6 is used to contain the address to jump back to after finishing.

loc_FC6C:   # Data fill (r0) -> ext4
            ld a, (r7|00)                   # counter for the data writes            
loc_FC6D:   ld ext4, (r0+!)                 # Writing to wherever EXT4 is pointing to.
            subi 0x01
            bra n=0, 0xFC6D                 # Are we done or we keep filling?
            ld pc, (r6+!)                   # RET!

loc_FC72:   # Data fill (r4) -> ext4
            ld a, (r3|00)                   # Counter for this data fill
loc_FC73:   ld ext4, (r4+!)                 # Write in EXT4
            subi 0x01
            bra n=0, 0xFC73                 # Are we done or we keep filling?
            ld pc, (r6+!)                   # RET!
            
loc_FC78:   # Data fill ext4 -> (r0)
            ld a, (r7|00)                   # Counter for reads
loc_FC79:   ld (r0+!), ext4                 # Reading from EXT4
            subi 0x01
            bra n=0, 0xFC79                 # Are we done reading?

            ld pc, (r6+!)                   # RET!

loc_FC7E:   # Data fill ext4 -> (r4)
            ld a, (r3|00)                   # Counter for reads
loc_FC7F:   ld (r4+!), ext4                 # Reading from EXT4
            subi 0x01
            bra n=0, 0xFC7F                 # Are we done reading?
            ld pc, (r6+!)                   # RET!

loc_FC84:   # Data fill: value contained in (r7|00) -> ext4
            ld a, (r7|01)                   # Counter for writes
loc_FC85:   ld ext4, (r7|00)                # Writing to EXT4 from whatever is in the stack
            subi 0x01
            bra n=0, 0xFC85                 # Do we keep writing?
            ld pc, (r6+!)                   # RET!

Math basic subroutines disassembly

Intended to be called by the trigonometry-related routines in the following section. The overall approach for these seems to assume fixed-point numbers, possibly 8.24 signed.

loc_FC8A:   # (Presumably) Addition routine of two fixed-point numbers (arranged from r7|00 - r7|11)
            # Called from 0xFC8A works as a substraction routine. Called from 0xFC8F works as addition.
            # Stores the result in (r3|00, r3|01)
            ld a, (r7|10)
            ld ext7, (r7|11)
            mod always, neg
            ld (r7|10), a
            ld (r7|11), ext7
loc_FC8F:   ld a, (r7|01)
            add a, (r7|11)
            ld (r3|01), a
            ld a, 0000
            bra l=0, 0xFC97         # Handling carry
            addi 0x01
loc_FC97:   add a, (r7|00)
            add a, (r7|10)
            ld (r3|00), a           # Store result
            ld ext7, (r3|01)
            ld pc, (r6+!)           # RET!

loc_FC9C:   # Possibly this is a routine to perform 8.24 fixed-point number multiplication, accounting for overflow in negative cases,
            # See more details at the end of it. (still speculation from my side). Operands are in R7 stack (r7|00-01 / r7|10-11)
            # HEAVILY used in sine calculations using the sine table at the end of this code.
            andi 0x00
            ld A[0x04], a
            ld a, (r7|00)
            and a, -
            bra n=0, 0xFCA9     # Check if negative, otherwise skip following lines for ABS
            ld ext7, (r7|01)
            mod always, abs
            ld (r7|00), a
            ld (r7|01), ext7    # All these past lines seem to apply abs to the number in (r7|00-01) - only if number is negative
            ld a, 0001
            ld A[0x04], a       # Stores high word for operand "A" in RAM bank 0x04
loc_FCA9:   ld a, (r7|10)       # Work with operand "B" now
            and a, -
            bra n=0, 0xFCB4
            ld ext7, (r7|11)
            mod always, abs     # Same stuff as above
            ld (r7|10), a
            ld (r7|11), ext7    # All these past lines seem to apply abs to the number in (r7|10-11)
            ld a, A[0x04]       # Recover high part of operand "A"
            addi 0x01           # sum 1 (2's complement?)
            ld A[0x04], a       # store it back to RAM bank A 0x04
loc_FCB4:   ld a, (r7|00)
            ld ext7, (r7|01)    # get number in (r7|00-01) in AH/AL
            mod always, shl     # multiply acc by 2
            ld (r7|00), a       # store high word of result in (r7|00)                  
            ld a, 0x0000
            mod always, shr     # divide low word by 2 to get it as it was
            ld (r7|01), ext7    # store result in r7|01
            ld a, (r7|10)       # start working with the other operand
            ld ext7, (r7|11)
            mod always, shl     # shift everything to multiply by 2
            ld (r7|10), a       # store high word in (r7|10)
            ld a, 0x0000
            mod always, shr     # bring back lower word
            ld (r7|11), ext7    # store lower word in (r7|11)
            ld x, (r7|10)       # This seems to confirm that MPYA, MPYS... instructions
            ld y, (r7|01)       # aren't really needed to multiply. Loading data in reg_y
            ld a, p             # leads to x*y to be performed.
            ld x, (r7|11)
            ld y, (r7|00)
            add a, p            # A = (opAL * opBH) + (opAH * opBL). Let's call this RH/RL
            mod always, shr     # compensating multiplication result

            ld (r3|00), ext7    # RL goes into (r3|00)
            ld x, (r7|01)       
            ld y, (r7|11)
            ld a, p             # A = (opAL * opBL) - let's call this TH/TL
            ld x, a             # x = TH
            ld a, 0000
            mod always, shr     # compensate multiplication efect on TL

            ld (r3|01), ext7    # TL goes to (r3|01)
            ld a, x             # A = TH
            add a, (r3|00)      # A = TH + RL
            ld (r3|00), a       # store this into r3|00
            andi 0x01
            bra z=1, 0xFCDE

            ld a, (r3|01)       # if the TL is odd, 
            ori a, 0x8000       # make it negative (don't understand the reasoning here)
            ld (r3|01), a       # and bring it back to (r3|01)
loc_FCDE:   andi 0x00
            ld ext7, (r3|00)    # bring TH + RL to AL
            mod always, shr     # and shift it again right
            ld (r3|00), ext7    # and store it again
            ld a, A[0x04]       # bring back opAH
            andi 0x01           # + 1
            bra z=1, 0xFCEC     # if it was previously #FFFF, jump to routine in FCEC to load data into accumulators (and avoid further neg operation)
            ld a, (r3|00)
            ld ext7, (r3|01)
            mod always, neg
            ld (r3|00), a       # negate result of (TH + RL) and TL
            ld (r3|01), ext7
                # So at the end of all this, if I didn't lost myself here:
                # r3|00 = ((opAL * opBL) >> 16) + (((opAL * opBH) + (opAH * opBL)) << 16) - we keep the lower 16 bits 
                # r3|01 = ((opAL * opBL)) << 16 - we keep the higher 16 bits
                #
                # In my mind this could be a 8.24 fixed-point number multiplication routine, taking into account
                # possible overflows from signed numbers.
            ld pc, (r6+!)       # RET!

loc_FCEC:   # This subroutine just loads a composed number into both accumulators and returns.
            ld a, (r3|00)
            ld ext7, (r3|01)
            ld pc, (r6+!)       # RET!

loc_FCEF:   # I don't see references to this in other routines, and it's hard for me to tell
            # what's the high-level intention of what's it doing. Could you help making sense of it? :D
            andi 0x00
            ld (r3|10), a
            ld (r3|11), a           # empty r3|10 and r3|11
            ld y, a                 # empty y - y = 0 is a flag used later
            ld (r3|00), 0x001F
            ld a, (r7|00)
            cmpi 0x00               # is r7|00 positive?
            bra n=0, 0xFCFF
            ld ext7, (r7|01)
            mod always, neg
            ld (r7|00), a
            ld (r7|01), ext7        # if not, abs it.
            ld y, 0x0001
loc_FCFF:   ld a, (r7|10)
            cmpi 0x00
            bra n=0, 0xFD08         # is what's in r7|10 positive?
            ld a, y
            addi 0x02
            ld y, a                 # condition flagged with y = 3 (checked later)
            bra always, 0xFD0D
loc_FD08:   ld a, (r7|10)           # r7|10 is positive: neg it.
            ld ext7, (r7|11)        
            mod always, neg         
            ld (r7|10), a           # Store neg value back to r7|10-11
            ld (r7|11), ext7
loc_FD0D:   ld a, (r3|10)           # Bring r3|10-11
            ld ext7, (r3|11)
            mod always, shl         # multiply by 2
            ld (r3|10), a           
            ld (r3|11), ext7        # store result in r3 stack
            ld a, (r7|00)
            ld ext7, (r7|01)        # get values from r7|00-01
            mod always, shl         # multiply by 2
            ld (r7|00), a
            ld (r7|01), ext7        # store them back multiplied in their original locations
            bra l=0, 0xFD1C
            ld a, (r3|11)           # avoid carry handling on next line if it's the case
            addi 0x01
            ld (r3|11), a
loc_FD1C:   ld a, (r3|11)
            add a, (r7|11)          # A = r3|11 + r7|11
            ld x, a
            ld a, 0x0000
            bra l=0, 0xFD24         # is there carry?
            addi 0x01               # if it's the case, handle it
loc_FD24:   add a, (r3|10)
            add a, (r7|10)          # A = (r3|10) + (r7|10), x = r3|11 + r7|11
            bra n=1, 0xFD2D         # is A negative? skip next part
            ld (r3|10), a
            ld (r3|11), x           # store a and x intermediate results in r3 stack
            ld a, (r7|01)
            addi 0x01
            ld (r7|01), a           # 2's complement stored in r7|01?
loc_FD2D:   ld a, (r3|00)
            subi 0x01
            ld (r3|00), a
            bra n=0, 0xFD0D         # Is A negative after substracting 1? Go back and repeat with the newly stored values
            ld a, (r7|00)
            ld (r3|00), a
            ld a, (r7|01)
            ld (r3|01), a           # copy values from r7 stack to r3 stack
            andi 0x00
            ld a, y
            cmpi 0x00
            bra z=1, 0xFD43         # is y = 0? (r7|10 isn't positive)
            cmpi 0x03
            bra z=1, 0xFD43         # is y = 3? (r7|10 is positive)
            ld a, (r3|00)
            ld ext7, (r3|01)
            mod always, neg
            ld (r3|00), a
            ld (r3|01), ext7        # if not, negate r3|00-01
loc_FD43:   ld a, y
            andi 0x01
            bra z=1, 0xFD4C         # is y = 0? (r7|10 isn't positive)
            ld a, (r3|10)
            ld ext7, (r3|11)
            mod always, neg
            ld (r3|10), a
            ld (r3|11), ext7        # if not, negate r3|10-11
loc_FD4C:   ld a, (r3|00)
            ld ext7, (r3|01)
            ld x, (r3|10)
            ld y, (r3|11)           # P = r3|10 * r3|11 / AH = r3|00, AL = r3|01
            ld pc, (r6+!)           # RET!

loc_FD51:   # This routine is used to load a value from the sine LUT at 0xFEE3 using an offset in (r7|00).
            # Called from 0xFD51 returns a cosine, called from 0xFD55 returns a sine.
            # Value is stored in stack for RAM bank A: r3|00
            #
            # Use of the data coming from this routine seems weird so far. When calling the sine routine,
            # the value of the multiplication of the sine value and whatever other operand used is just
            # assumed to be in P. When calling the cosine, y is assigned 0001 and then the product register
            # is loaded in A.
            #
            # Would seem like storing data both in X or Y trigger a multiplication in the pipeline, that'd contradict
            # what CD2450 (a DSP derived from SSP1601) does (multiplication pipeline is triggered by writes to reg_y)
            # in that case. Virtua Racing seems to use both MLx instructions and writes to register X/Y to trigger
            # multiplications, so it's hard to tell. Will research with actual tests at some point to double check this.
            ld a, (r7|00)
            addi 0x40
            bra always, 0xFD56
loc_FD55:   ld a, (r7|00)
loc_FD56:   andi 0xFF           # We don't want to overflow here.
            addi a, 0xFEE3      # Add offset to the sine table initial address.
            ld x, (a)
            ld (r3|00), x       # Get value in stack.
            ld pc, (r6+!)       # RET! 

Math/trigonometric calculation routines disassembly

Most of these seem intended to be called by game code.

Note: considering the values of the sine/cosine table at the end of the internal ROM, the number system used for these functions would seem to be signed 8.24 fixed-point numbers (this is still unverified).

The main point of the code in this section is three functions that perform rotations of a point over one of the three axis.

loc_FD5C:   # Performs multiple addition as well as a multiplication on operands in addresses
            # pointed by r0 and r1. Not sure of what's it's intended use though.
            # Definitely not referenced in another part of the boot ROM, so it must've meant
            # to be called by game code.
            ld r0, 0x11
            ld a, r1
            addi 0x12
            ld r1, a                # Seems like operands from this are in r0=0x11, r1=0x23
            ld (r3|11), 0x0002      # counter?
            ld x, 0x0004
loc_FD64:   ld y, (r0+!)
            ld a, p                 # A = (r0) * 8?
            ld (r7|00), a
            ld (r7|01), ext7        # store result in r7|00-01 stack - operand A of addition
            ld a, (r1+!)
            ld (r7|10), a
            ld a, (r1-)
            ld (r7|11), a           # store following two values in r1 in r7|10-11 stack, but bring r1 to its initial value (operand B)
            ld -, (r6-)
            ld (r6), 0xFD71
            bra always, 0xFC8F      # perform addition
loc_FD71:   ld (r1+!), a
            ld (r1+!), ext7         # Store result of addition in R1 and increment
            ld a, (r3|11)
            subi 0x01
            ld (r3|11), a           # decrement counter
            bra n=0, 0xFD64         # keep doing this until it's 0 (3 times)
            ld pc, (r6+!)           # RET!

loc_FD79:   # Trigonometric calculations seem to be happening here. (r7|00) should contain an angle for the sine table.
            # r1 is used as a pointer for results.
            # Heavily used by other routines.

            # This routine takes a list of four vertices (as pairs of three coords each: A, B, C),
            # pointed by r1. Still not sure which axis corresponds to each of these coords.

            # The routine works by first placing into memory the following values:
            # A[0x08]: cos(a) - high word
            # A[0x09]: cos(a) - low word
            # A[0x0A]: sin(a) - high word
            # A[0x0B]: sin(a) - low word
            # A[0x0C]: -sin(a) - high word
            # A[0x0D]: -sin(a) - low word
            # A[0x0E]: cos(a) - high word
            # A[0x0F]: cos(a) - low word
            # (notice how sin/cos values are two word each, while the table only contains one per angle. References to the multiplier
            # in the routine adapt these values to the expected two-word ones).

            # The "inner" loop (0xFDA9) performs two multiplications of these values in successive order with the provided coordinates,
            # and later the two results are added, returning:
            #
            # b' = (b * cos(ang)) + (c * sin(ang)) >> 8
            # c' = (c * cos(ang)) - (b * sin(ang)) >> 8
            #
            # These results are stored in the position pointed by r1. Then the code jumps to go through the "outer loop", which basically
            # does this same stuff 4 times. As was suggested by reddit user @tyfighter, this is a 2D rotation matrix for 4-vertex polygons over edge A.
            
            ld -, (r6-)
            ld (r6), 0xFD7E
            bra always, 0xFD55  # Get sine value for the current angle, store in (r3|00)
loc_FD7E:   ld y, 0x0001        # It seems multiplication is triggered on sine values to adapt them to the current two-word fixed-point number format.
            ld a, p             
            mod always, shr
            ld r0, 0x0A
            ld (r0+!), a
            ld (r0+!), ext7
            mod always, neg
            ld (r0+!), a
            ld (r0+!), ext7     # A[0x0A]/A[0x0B] get the sine value, A[0x0C]/A[0x0D] get the negative sine.
            ld -, (r6-)
            ld (r6), 0xFD8D
            bra always, 0xFD51  # Get cosine value for the current angle, store in (r3|00) / x
loc_FD8D:   ld a, p
            mod always, shr
            ld r0, 0x08
            ld (r0+!), a
            ld (r0+!), ext7     # Stores cosine in two places: A[0x08], A[0x09]
            ld r0, 0x0E
            ld (r0+!), a
            ld (r0+!), ext7     # Also storing cosine in A[0x0E], A[0x0F]
            ld -, (r1+!)
            ld -, (r1+!)        # Skip first set of coordinates
            ld a, 0x0003
            ld B[0x08], a       # outer counter = 3
loc_FD9A:   ld a, r1            # r1 is also used as a pointer to store results later.
            ld r2, a            # r2 will be used to iterate over the point coordinates
            ld r4, 0x0A
            ld a, (r2+!)        # Copy operands to soft stack, first the trigonometric value
            ld (r4+!), a
            ld a, (r2+!)
            ld (r4+!), a        # Then the point coordinate
            ld a, (r2+!)
            ld (r4+!), a
            ld a, (r2+!)
            ld (r4+!), a
            ld r2, 0x08
            ld a, 0x0001
            ld B[0x09], a       # Storing "inner" counter in the data fill part of this routine
loc_FDA9:   
            # Multiplications between coordinate and trigonometric values:
            ld r4, 0x0A
            ld a, (r2+!)
            ld (r7|00), a           # Copy operands to soft stack, first the trigonometric value
            ld a, (r2+!)
            ld (r7|01), a
            ld a, (r4+!)
            ld (r7|10), a           # Then the point coordinate
            ld a, (r4+!)
            ld (r7|11), a
            ld -, (r6-)
            ld (r6), 0xFDB7
            bra always, 0xFC9C      # Multiply!
loc_FDB7:   ld (r3|10), a
            ld (r3|11), ext7        # Store results from first multiplication to r3 stack
            ld a, (r2+!)            # Same data copy as before.
            ld (r7|00), a           # Trigonometric value
            ld a, (r2+!)
            ld (r7|01), a
            ld a, (r4+!)
            ld (r7|10), a           # Point coordinate
            ld a, (r4+!)
            ld (r7|11), a
            ld -, (r6-)
            ld (r6), 0xFDC6
            bra always, 0xFC9C      # Multiply!
loc_FDC6:   ld (r7|00), a           # Store results in r7 stack (both results in A and r3 stack)
            ld (r7|01), ext7
            ld a, (r3|10)           
            ld (r7|10), a
            ld a, (r3|11)
            ld (r7|11), a           # Now R7 stack contains both multiplication results
            ld -, (r6-)
            ld (r6), 0xFDD1
            bra always, 0xFC8F      # Perform addition of both multiplications.
loc_FDD1:   mod always, shr
            mod always, shr
            mod always, shr
            mod always, shr
            mod always, shr
            mod always, shr
            mod always, shr
            mod always, shr         # compensate from overflow during multiplication process? (this would confirm 8 is the size for the real part)
            ld (r1+!), a
            ld (r1+!), ext7         # store results in address pointed by r1
            ld a, B[0x09]           # load counter from B[0x09].
            subi 0x01
            ld B[0x09], a           # and substract 1
            bra n=0, 0xFDA9         # Keep copying data if result < 0 (this happens 2 times each inner loop)
            ld -, (r1+!)
            ld -, (r1+!)
            ld a, B[0x08]           # Same thing with the "outer" counter in RAM B 0x08
            subi 0x01
            ld B[0x08], a
            bra n=0, 0xFD9A         # Keep copying data until it's 0 (four times) - the whole thing takes 8 round trips
            ld pc, (r6+!)           # RET!

loc_FDE8:   # Rotation of a quadrilateral over Y-axis.
            #
            # This routine follows the same approach than 0xFD79, but there are some differences:
            # first, it takes a different set of coordinates while doing the calculations (keeps X and Z, skips Y),
            # also the order of the signs of the sines is different.
            # The results seems to be:
            #
            # Z' = z * cos(angle) - x * sin(angle)
            # X' = z * sin(angle) + x * cos(angle)
            #
            # In a similar way, the user provides a pointer to the list of 4 points with 3 coordinates each (in r1),
            # and the rotated point is overwritten there.

            ld -, (r6-)
            ld (r6), 0xFDED
            bra always, 0xFD55          # Get sine value for the current angle, store in (r3|00) and x
loc_FDED:   ld y, 0x0001                # See notes in loc_FD51 - supposedly triggering multiplier with sine value
            ld a, p
            mod always, shr
            mod always, neg             # Result is negated, this is a difference from FDA9.
            ld r0, 0x0A
            ld (r0+!), a
            ld (r0+!), ext7         
            mod always, neg
            ld (r0+!), a
            ld (r0+!), ext7
            ld -, (r6-)
            ld (r6), 0xFDFD
            bra always, 0xFD51          # Get cosine value for the current angle, store in (r3|00)
loc_FDFD:   ld a, p
            mod always, shr             # Get multiplier result and compensate
            ld r0, 0x08                 # Another difference with FD79, address 0x08 is used, also
            ld (r0+!), a                # this section is simpler, not updating r1 pointer position.
            ld (r0+!), ext7
            ld r0, 0x0E
            ld (r0+!), a
            ld (r0+!), ext7
            ld a, 0x0003                # Outer counter seems the same as in FD79.
            ld B[0x08], a
loc_FE08:   ld a, r1
            ld r2, a
            ld r4, 0x0A                 # This is the data load sequence, similar as in 0xFD79,
            ld a, (r2+!)                # but some areas differ...
            ld (r4+!), a
            ld a, (r2+!)
            ld (r4+!), a
            ld -, (r2+!)                # ...r2 pointer is advanced two positions here, where
            ld -, (r2+!)                # in FD79 this is totally skipped.
            ld a, (r2+!)
            ld (r4+!), a
            ld a, (r2+!)
            ld (r4+!), a
            ld r2, 0x08
            ld a, 0x0001
            ld B[0x09], a               # inner counter is kept here, same as in 0xFD79.
loc_FE19:   ld r4, 0x0A                 # Prepping numbers for fixed-point multiplication, it's identical
            ld a, (r2+!)                # to the equivalent from FD79.
            ld (r7|00), a
            ld a, (r2+!)
            ld (r7|01), a
            ld a, (r4+!)
            ld (r7|10), a
            ld a, (r4+!)
            ld (r7|11), a
            ld -, (r6-)
            ld (r6), 0xFE27
            bra always, 0xFC9C          # Perform fixed-point multiplication
loc_FE27:   ld (r3|10), a               # Same here, this is the same behavior seen in 0xFD79.
            ld (r3|11), ext7
            ld a, (r2+!)
            ld (r7|00), a
            ld a, (r2+!)
            ld (r7|01), a
            ld a, (r4+!)
            ld (r7|10), a
            ld a, (r4+!)
            ld (r7|11), a
            ld -, (r6-)
            ld (r6), 0xFE36
            bra always, 0xFC9C          # Perform fixed-point multiplication
loc_FE36:   ld (r7|00), a               # Results from fixed-point multiplication are added here,
            ld (r7|01), ext7            # again same behavior as in FD79.
            ld a, (r3|10)
            ld (r7|10), a
            ld a, (r3|11)
            ld (r7|11), a
            ld -, (r6-)
            ld (r6), 0xFE41
            bra always, 0xFC8F
loc_FE41:   mod always, shr             # Operations here are basically the same as in the final
            mod always, shr             # section of 0xFD79, except for...
            mod always, shr
            mod always, shr
            mod always, shr
            mod always, shr
            mod always, shr
            mod always, shr
            ld (r1+!), a
            ld (r1+!), ext7
            ld a, B[0x09]
            subi 0x01
            ld B[0x09], a
            bra n=1, 0xFE54             # ... the way this looping scheme works. It's using the opposite
            ld -, (r1+!)                # condition, but otherwise seems to be doing the same.
            ld -, (r1+!)
            bra always, 0xFE19
loc_FE54:   ld a, B[0x08]
            subi 0x01
            ld B[0x08], a
            bra n=0, 0xFE08
            ld pc, (r6+!)               # RET!

loc_FE5A:   # Rotation of a quadrilateral over X-axis.
            #
            # Following the same approach of the previous two routines, the rotation is performed over the X axis.
            # The resulting calculation is as follows:
            #
            # Z' = z * cos(ang) + y * sin(ang)
            # Y' = y * cos(ang) - z * sin(ang)

            ld -, (r6-)
            ld (r6), 0xFE5F
            bra always, 0xFD55      # Get sine value for the current angle, store in (r3|00)
loc_FE5F:   ld y, 0x0001            # This first section works the same as the first one in FD79. See more details in there.
            ld a, p                 
            mod always, shr         
            ld r0, 0x0A
            ld (r0+!), a
            ld (r0+!), ext7
            mod always, neg
            ld (r0+!), a
            ld (r0+!), ext7
            ld -, (r6-)
            ld (r6), 0xFE6E
            bra always, 0xFD51      # Get cosine value for the current angle, store in (r3|00)
loc_FE6E:   ld a, p                 # This section is identical to the alternative one found in routine 0xFDE8
            mod always, shr
            ld r0, 0x08
            ld (r0+!), a
            ld (r0+!), ext7
            ld r0, 0x0E
            ld (r0+!), a
            ld (r0+!), ext7
            ld a, 0x0003
            ld B[0x08], a
loc_FE79:       # The data loading loop for operands in the following fixed-point multiplication call,
                # is identical to the one found for routine 0xFD79 (in 0xFD9A):
            ld a, r1
            ld r2, a
            ld r4, 0x0A
            ld a, (r2+!)
            ld (r4+!), a
            ld a, (r2+!)
            ld (r4+!), a
            ld a, (r2+!)
            ld (r4+!), a
            ld a, (r2+!)
            ld (r4+!), a
            ld r2, 0x08
            ld a, 0x0001
            ld B[0x09], a
loc_FE88:       # Preparation of operands for fixed-point multiplication. Again, identical to the equivalent
                # section for routine 0xFD7A (found in 0xFDA9). The only difference is that we don't skip
                # any coordinates now, instead focus on the first two (a and b).
            ld r4, 0x0A
            ld a, (r2+!)
            ld (r7|00), a
            ld a, (r2+!)
            ld (r7|01), a
            ld a, (r4+!)
            ld (r7|10), a
            ld a, (r4+!)
            ld (r7|11), a
            ld -, (r6-)
            ld (r6), 0xFE96
            bra always, 0xFC9C      # Fixed-point multiplication call
loc_FE96:       # Same here, equivalent to load section in 0xFD9A except that we don't skip the first coordinate and
                # focus on A and B.
            ld (r3|10), a
            ld (r3|11), ext7
            ld a, (r2+!)
            ld (r7|00), a
            ld a, (r2+!)
            ld (r7|01), a
            ld a, (r4+!)
            ld (r7|10), a
            ld a, (r4+!)
            ld (r7|11), a
            ld -, (r6-)
            ld (r6), 0xFEA5
            bra always, 0xFC9C      # Fixed-point multiplication call
loc_FEA5:       # Addition of the two fixed-point multiplication results. Yeah, you guessed it:
                # identical to the one in routine 0xFD7A.
            ld (r7|00), a
            ld (r7|01), ext7
            ld a, (r3|10)
            ld (r7|10), a
            ld a, (r3|11)
            ld (r7|11), a
            ld -, (r6-)
            ld (r6), 0xFEB0
            bra always, 0xFC8F
loc_FEB0:       # Same thing here. Final preparations of the result, and handling of the
                # counter scheme is the exact same as the final section for routine 0xFD7A.
            mod always, shr
            mod always, shr
            mod always, shr
            mod always, shr
            mod always, shr
            mod always, shr
            mod always, shr
            mod always, shr
            ld (r1+!), a
            ld (r1+!), ext7
            ld a, B[0x09]
            subi 0x01
            ld B[0x09], a
            bra n=0, 0xFE88
            ld -, (r1+!)
            ld -, (r1+!)
            ld a, B[0x08]
            subi 0x01
            ld B[0x08], a
            bra n=0, 0xFE79
            ld pc, (r6+!)           # RET!

loc_FEC7:   # This routine simply fills an area (starting from the current address in r1)
            # with a set of 24 0x0/0x100 values.
            # It doesn't seem to be called from other places in the routines found here,
            # meant to be used by game code.
            andi 0x00
            ld x, 0x0100
            ld (r1+!), a
            ld (r1+!), x
            ld (r1+!), a
            ld (r1+!), a
            ld (r1+!), a
            ld (r1+!), a
            ld (r1+!), a
            ld (r1+!), a
            ld (r1+!), a
            ld (r1+!), x
            ld (r1+!), a
            ld (r1+!), a
            ld (r1+!), a
            ld (r1+!), a
            ld (r1+!), a
            ld (r1+!), a
            ld (r1+!), a
            ld (r1+!), x
            ld (r1+!), a
            ld (r1+!), a
            ld (r1+!), a
            ld (r1+!), a
            ld (r1+!), a
            ld (r1+!), a
            ld pc, (r6+!)           # RET!

Static data

A 256-word sized sine table can be found at address 0xFEE3. Here's a trace of the data:

sine_part_1 sine_part_2

loc_FEE3_sine_table:
            dw 0000
            dw 0006
            dw 000C
            dw 0012
            dw 0019
            dw 001F
            dw 0025
            dw 002B
            dw 0031
            dw 0038
            dw 003E
            dw 0044
            dw 004A
            dw 0050
            dw 0056
            dw 005C
            dw 0061
            dw 0067
            dw 006D
            dw 0073
            dw 0078
            dw 007E
            dw 0083
            dw 0088
            dw 008E
            dw 0093
            dw 0098
            dw 009D
            dw 00A2
            dw 00A7
            dw 00AB
            dw 00B0
            dw 00B5
            dw 00B9
            dw 00BD
            dw 00C1
            dw 00C5
            dw 00C9
            dw 00CD
            dw 00D1
            dw 00D4
            dw 00D8
            dw 00DB
            dw 00DE
            dw 00E1
            dw 00E4
            dw 00E7
            dw 00EA
            dw 00EC
            dw 00EE
            dw 00F1
            dw 00F3
            dw 00F4
            dw 00F6
            dw 00F8
            dw 00F9
            dw 00FB
            dw 00FC
            dw 00FD
            dw 00FE
            dw 00FE
            dw 00FF
            dw 00FF
            dw 00FF
            dw 0100
            dw 00FF
            dw 00FF
            dw 00FF
            dw 00FE
            dw 00FE
            dw 00FD
            dw 00FC
            dw 00FB
            dw 00F9
            dw 00F8
            dw 00F6
            dw 00F4
            dw 00F3
            dw 00F1
            dw 00EE
            dw 00EC
            dw 00EA
            dw 00E7
            dw 00E4
            dw 00E1
            dw 00DE
            dw 00DB
            dw 00D8
            dw 00D4
            dw 00D1
            dw 00CD
            dw 00C9
            dw 00C5
            dw 00C1
            dw 00BD
            dw 00B9
            dw 00B5
            dw 00B0
            dw 00AB
            dw 00A7
            dw 00A2
            dw 009D
            dw 0098
            dw 0093
            dw 008E
            dw 0088
            dw 0083
            dw 007E
            dw 0078
            dw 0073
            dw 006D
            dw 0067
            dw 0061
            dw 005C
            dw 0056
            dw 0050
            dw 004A
            dw 0044
            dw 003E
            dw 0038
            dw 0031
            dw 002B
            dw 0025
            dw 001F
            dw 0019
            dw 0012
            dw 000C
            dw 0006
            dw 0000
            dw FFFA
            dw FFF4
            dw FFEE
            dw FFE7
            dw FFE1
            dw FFDB
            dw FFD5
            dw FFCF
            dw FFC8
            dw FFC2
            dw FFBC
            dw FFB6
            dw FFB0
            dw FFAA
            dw FFA4
            dw FF9F
            dw FF99
            dw FF93
            dw FF8D
            dw FF88
            dw FF82
            dw FF7D
            dw FF78
            dw FF72
            dw FF6D
            dw FF68
            dw FF63
            dw FF5E
            dw FF59
            dw FF55
            dw FF50
            dw FF4B
            dw FF47
            dw FF43
            dw FF3F
            dw FF3B
            dw FF37
            dw FF33
            dw FF2F
            dw FF2C
            dw FF28
            dw FF25
            dw FF22
            dw FF1F
            dw FF1C
            dw FF19
            dw FF16
            dw FF14
            dw FF12
            dw FF0F
            dw FF0D
            dw FF0C
            dw FF0A
            dw FF08
            dw FF07
            dw FF05
            dw FF04
            dw FF03
            dw FF02
            dw FF02
            dw FF01
            dw FF01
            dw FF01
            dw FF00
            dw FF01
            dw FF01
            dw FF01
            dw FF02
            dw FF02
            dw FF03
            dw FF04
            dw FF05
            dw FF07
            dw FF08
            dw FF0A
            dw FF0C
            dw FF0D
            dw FF0F
            dw FF12
            dw FF14
            dw FF16
            dw FF19
            dw FF1C
            dw FF1F
            dw FF22
            dw FF25
            dw FF28
            dw FF2C
            dw FF2F
            dw FF33
            dw FF37
            dw FF3B
            dw FF3F
            dw FF43
            dw FF47
            dw FF4B
            dw FF50
            dw FF55
            dw FF59
            dw FF5E
            dw FF63
            dw FF68
            dw FF6D
            dw FF72
            dw FF78
            dw FF7D
            dw FF82
            dw FF88
            dw FF8D
            dw FF93
            dw FF99
            dw FF9F
            dw FFA4
            dw FFAA
            dw FFB0
            dw FFB6
            dw FFBC
            dw FFC2
            dw FFC8
            dw FFCF
            dw FFD5
            dw FFDB
            dw FFE1
            dw FFE7
            dw FFEE
            dw FFF4
            dw FFFA

Interrupt vectors definition disassembly

After that, some blank spaces and finally the interrupt vectors definition at the end of the program space:

blank_spaces:
			dw 0000
			dw 0000
			dw 0000
			dw 0000
			dw 0000
			dw 0000
			dw 0000
			dw 0000
			dw 0000
			dw 0000
			dw 0000
			dw 0000
			dw 0000
			dw 0000
			dw 0000
			dw 0000
			dw 0000
			dw 0000
			dw 0000
			dw 0000
			dw 0000
			dw 0000
			dw 0000
			dw 0000
			dw 0000

interrupt_vector_reset:
			dw FC08
interrupt_vector_int0:
			dw 03FA
interrupt_vector_int1:
			dw 03FC
interrupt_vector_int2:
			dw 03FE

Interrupt vectors

  • RESET: at 0xFFFC
  • INT0: at 0xFFFD
  • INT1: at 0xFFFE
  • INT2: at 0xFFFF

SHA-1 hash

The hash for the set of code inside the SVP chip (taken from 0xFC00-0xFFFF) is as follows:

0b951ea9c6094b3c34e4f0b64d031c75c237564f