-
Notifications
You must be signed in to change notification settings - Fork 7
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.
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).
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.
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:
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
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.
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!
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!
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!
A 256-word sized sine table can be found at address 0xFEE3. Here's a trace of the data:
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
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
- RESET: at 0xFFFC
- INT0: at 0xFFFD
- INT1: at 0xFFFE
- INT2: at 0xFFFF
The hash for the set of code inside the SVP chip (taken from 0xFC00-0xFFFF) is as follows:
0b951ea9c6094b3c34e4f0b64d031c75c237564f