// Literal port of chunk5 TransformVertex7EBC (src/chunk5.s line 4298- // 4569). Each block of C code is annotated with the chunk5 label/line // it mirrors. 6502 byte arithmetic and flag semantics are preserved // exactly; the goal is for this to behave bit-identically to the // original on every input. #include #include #include #include #include "chunk5Transform.h" #include "chunk5Setup.h" #include "cpu6502.h" static int chunk5DebugTrace(void) { static int cached = -1; if (cached < 0) { cached = (getenv("CHUNK5_TRACE") != NULL) ? 1 : 0; } return cached; } // 6502 status flag bits. We only track the four that TransformVertex7EBC // relies on: C (carry), V (overflow), N (negative), Z (zero). #define FLAG_C 0x01 #define FLAG_Z 0x02 #define FLAG_V 0x40 #define FLAG_N 0x80 // ADC: A = A + M + C. Sets N/V/Z/C from the result. static uint8_t op_adc(uint8_t a, uint8_t m, uint8_t *flags) { int c = (*flags & FLAG_C) ? 1 : 0; int unsignedSum = (int)a + (int)m + c; int signedSum = (int)(int8_t)a + (int)(int8_t)m + c; uint8_t result = (uint8_t)(unsignedSum & 0xFF); *flags &= (uint8_t)~(FLAG_C | FLAG_V | FLAG_N | FLAG_Z); if (unsignedSum > 255) *flags |= FLAG_C; if (signedSum > 127 || signedSum < -128) *flags |= FLAG_V; if (result & 0x80) *flags |= FLAG_N; if (result == 0) *flags |= FLAG_Z; return result; } // SBC: A = A - M - (1 - C). Sets N/V/Z/C from the result. C reflects // "no borrow needed". static uint8_t op_sbc(uint8_t a, uint8_t m, uint8_t *flags) { int c = (*flags & FLAG_C) ? 1 : 0; int unsignedDiff = (int)a - (int)m - (1 - c); int signedDiff = (int)(int8_t)a - (int)(int8_t)m - (1 - c); uint8_t result = (uint8_t)(unsignedDiff & 0xFF); *flags &= (uint8_t)~(FLAG_C | FLAG_V | FLAG_N | FLAG_Z); if (unsignedDiff >= 0) *flags |= FLAG_C; if (signedDiff > 127 || signedDiff < -128) *flags |= FLAG_V; if (result & 0x80) *flags |= FLAG_N; if (result == 0) *flags |= FLAG_Z; return result; } // EOR: A ^= M. Sets N/Z. static uint8_t op_eor(uint8_t a, uint8_t m, uint8_t *flags) { uint8_t result = a ^ m; *flags &= (uint8_t)~(FLAG_N | FLAG_Z); if (result & 0x80) *flags |= FLAG_N; if (result == 0) *flags |= FLAG_Z; return result; } // ASL: bit 7 -> C, 0 -> bit 0. Sets N/Z/C. static uint8_t op_asl(uint8_t a, uint8_t *flags) { uint8_t result = (uint8_t)((a << 1) & 0xFF); *flags &= (uint8_t)~(FLAG_C | FLAG_N | FLAG_Z); if (a & 0x80) *flags |= FLAG_C; if (result & 0x80) *flags |= FLAG_N; if (result == 0) *flags |= FLAG_Z; return result; } // ROL: bit 7 -> C, old C -> bit 0. Sets N/Z/C. static uint8_t op_rol(uint8_t a, uint8_t *flags) { int oldC = (*flags & FLAG_C) ? 1 : 0; uint8_t result = (uint8_t)(((a << 1) | oldC) & 0xFF); *flags &= (uint8_t)~(FLAG_C | FLAG_N | FLAG_Z); if (a & 0x80) *flags |= FLAG_C; if (result & 0x80) *flags |= FLAG_N; if (result == 0) *flags |= FLAG_Z; return result; } // LSR: bit 0 -> C, 0 -> bit 7. Sets N=0/Z/C. static uint8_t op_lsr(uint8_t a, uint8_t *flags) { uint8_t result = a >> 1; *flags &= (uint8_t)~(FLAG_C | FLAG_N | FLAG_Z); if (a & 0x01) *flags |= FLAG_C; if (result == 0) *flags |= FLAG_Z; return result; } // ROR: bit 0 -> C, old C -> bit 7. Sets N/Z/C. static uint8_t op_ror(uint8_t a, uint8_t *flags) { int oldC = (*flags & FLAG_C) ? 1 : 0; uint8_t result = (uint8_t)((a >> 1) | (oldC << 7)); *flags &= (uint8_t)~(FLAG_C | FLAG_N | FLAG_Z); if (a & 0x01) *flags |= FLAG_C; if (result & 0x80) *flags |= FLAG_N; if (result == 0) *flags |= FLAG_Z; return result; } // chunk4 L1818 / MultiplyXY (line 2025-2091). Signed 7x7 -> 14-bit // multiply via 7 shift-add steps with absolute-value preprocessing. // Inputs: y_in, x_in (signed bytes treated as 7-bit magnitude + sign). // Outputs: 16-bit signed result placed in *outA (low byte) and *outY // (high byte) -- caller uses the (A, Y) register pair. static void op_l1818(uint8_t y_in, uint8_t x_in, uint8_t *outA, uint8_t *outY) { uint8_t flags = 0; uint8_t zA5, zC4, zC2, zC5; uint8_t a; // tya; sta $A5 zA5 = y_in; a = y_in; // bpl L1822 if (a & 0x80) { // sec; sbc #$01 flags |= FLAG_C; a = op_sbc(a, 0x01, &flags); // bmi L1824 if (flags & FLAG_N) { // L1824: sta $C4 } else { // L1822: eor #$FF; (fall to L1824) a = op_eor(a, 0xFF, &flags); } } else { // L1822: eor #$FF a = op_eor(a, 0xFF, &flags); } // L1824: sta $C4 zC4 = a; // txa a = x_in; // bpl L1832 if (a & 0x80) { // eor #$FF; clc; adc #$01 a = op_eor(a, 0xFF, &flags); flags &= (uint8_t)~FLAG_C; a = op_adc(a, 0x01, &flags); // bpl L1832 if (flags & FLAG_N) { // lda #$7F (saturate) a = 0x7F; } } // L1832: sta $C2 zC2 = a; // ror $C4 (uses current carry; after the absolute-value path, // carry is whatever the last op left it). This first ror // shifts $C4 right; the carry-out becomes the choice for the // first shift-add. zC4 = op_ror(zC4, &flags); // bcc L183A; lda #$00 -- if carry CLEAR, fall through with A // unchanged; if carry SET, A = 0. if (flags & FLAG_C) { a = 0; } // (otherwise leave A as whatever $C2 was -- this is the // chunk5 quirk: the multiply uses $C2 as the running multiplier // base, and after the abs path A still holds $C2 implicitly. // Actually no, A = $C2 from the previous sta. After ror $C4 we // need A to be the current multiplier value. Looking at chunk5 // line 2046: `ror $C4 / bcc L183A / lda #$00` -- if carry was // SET (bit 0 of $C4 was 1), load 0; else leave A. A had been // set to $C2 just before. So either A = $C2 or A = 0.) // 6-step shift-add loop (L183A through L185D in chunk4.s). // Each step: lsr a; ror $C4; bcs ; adc $C2. After // the loop comes the L1864 final lsr/ror without an add. // Earlier this loop ran 7 times -- one too many -- which // halved every multiply result. Verified against fs2trace // --xform asm trace: L1818($79=3F, $9F=11) = lo=$5E hi=$08 // (was lo=$2F hi=$04 with the off-by-one). for (int step = 0; step < 6; step++) { a = op_lsr(a, &flags); zC4 = op_ror(zC4, &flags); if (!(flags & FLAG_C)) { flags &= (uint8_t)~FLAG_C; // clc implicit a = op_adc(a, zC2, &flags); } } // L1864: lsr a; ror $C4; sta $C5 a = op_lsr(a, &flags); zC4 = op_ror(zC4, &flags); zC5 = a; // txa; eor $A5 a = op_eor(x_in, zA5, &flags); // bpl L187B (signs same -> just return) if (flags & FLAG_N) { // Negate: $C4/$C5 = -$C4/$C5 flags |= FLAG_C; // sec implicit before sbc 0 uint8_t newC4 = op_sbc(0, zC4, &flags); uint8_t newC5 = op_sbc(0, zC5, &flags); zC4 = newC4; zC5 = newC5; } // Output A = $C4, Y = $C5 *outA = zC4; *outY = zC5; } // L80B0: shift-down-by-1 of all three 24-bit accumulators. Mirrors // chunk5.s line 4559-4569. 24-bit value layout per axis: ($1A LSB, // $18 MID, $19 HSB) for X; ($1D, $1B, $1C) for Y; ($20, $1E, $1F) for // Z. The shift drops the bottom bit of each and re-aligns. static void op_l80b0(uint8_t *z18, uint8_t *z19, uint8_t *z1A, uint8_t *z1B, uint8_t *z1C, uint8_t *z1D, uint8_t *z1E, uint8_t *z1F, uint8_t *z20) { uint8_t flags = 0; // lsr $1A; ror $19; ror $18 - X axis shift *z1A = op_lsr(*z1A, &flags); *z19 = op_ror(*z19, &flags); *z18 = op_ror(*z18, &flags); // lsr $1D; ror $1C; ror $1B - Y axis shift *z1D = op_lsr(*z1D, &flags); *z1C = op_ror(*z1C, &flags); *z1B = op_ror(*z1B, &flags); // lsr $20; ror $1F; ror $1E - Z axis shift *z20 = op_lsr(*z20, &flags); *z1F = op_ror(*z1F, &flags); *z1E = op_ror(*z1E, &flags); } // Common implementation of TransformVertex7EBC and TransformVertex80C5. // The only difference between them is the work-counter bias ($51 vs // $C7) which doesn't affect the math, just the per-frame cycle // budget. We don't model that timer. static int transformVertexCommon(uint8_t *ram, const uint8_t *stream, uint8_t destSlot) { uint8_t flags = 0; // Per-vertex transform base accumulator. // // MAME-patched TransformVertex7EBC ($7E8E entry) copies // $2A..$2F into $18..$1D at the top of the routine and then // reads/writes $18..$1F as the working accumulator. Port's // C re-impl skips the copy and reads $2A..$2F directly into // local z18..z1F, which is structurally equivalent. // Use PORT_XFORM_INTERPRETED=1 to run the actual MAME- // patched bytecode instead (byte-faithful within 1 LSB). uint8_t z18, z19, z1A, z1B, z1C, z1D, z1E, z1F, z20; { z18 = ram[0x2A]; z19 = ram[0x2B]; z1A = (ram[0x2B] & 0x80) ? 0xFF : 0x00; z1B = ram[0x2C]; z1C = ram[0x2D]; z1D = (ram[0x2D] & 0x80) ? 0xFF : 0x00; z1E = ram[0x2E]; z1F = ram[0x2F]; z20 = (ram[0x2F] & 0x80) ? 0xFF : 0x00; } // L7F03: Y = 1 (start reading vertex bytes from $8B+1). // Read xLo, xHi from stream, subtract $66/$67 (camera-section // delta X). The first sbc has SEC pre-set; the second sbc // chains the carry. uint8_t z9E, z9F, zA2, zA3; bool xOverflow = false; bool zOverflow = false; flags |= FLAG_C; z9E = op_sbc(stream[1], ram[0x66], &flags); z9F = op_sbc(stream[2], ram[0x67], &flags); if (flags & FLAG_V) { xOverflow = true; // L7F64: ror a; sta $9F; ror $9E // (a holds the result of the previous sbc = z9F) // The ror uses the V-trapped carry from the sbc. z9F = op_ror(z9F, &flags); z9E = op_ror(z9E, &flags); } flags |= FLAG_C; zA2 = op_sbc(stream[3], ram[0x6A], &flags); zA3 = op_sbc(stream[4], ram[0x6B], &flags); if (flags & FLAG_V) { zOverflow = true; if (xOverflow) { // L7F5C: ror a; sta $A3; ror $A2 -- shift Z // down by 1, X already shifted by L7F64. zA3 = op_ror(zA3, &flags); zA2 = op_ror(zA2, &flags); } else { // L7EAD: ror a; sta $A3; ror $A2; lda $9F; // rol a; ror $9F; ror $9E // Z gets shifted right; X gets sign-extending // arithmetic shift right. zA3 = op_ror(zA3, &flags); zA2 = op_ror(zA2, &flags); uint8_t a = z9F; a = op_rol(a, &flags); z9F = op_ror(z9F, &flags); z9E = op_ror(z9E, &flags); (void)a; } } else if (xOverflow) { // L7F64 fall-through after the Z reads succeeded: // rol a; ror $A3; ror $A2 (shift Z down by 1 too so // the scales match the down-shifted X). uint8_t a = zA3; a = op_rol(a, &flags); zA3 = op_ror(zA3, &flags); zA2 = op_ror(zA2, &flags); (void)a; } if (xOverflow || zOverflow) { // L7F7F: shift the three 24-bit base accumulators // right by 1 each so they match the down-shifted // delta scale. Mirror chunk5 line 4400-4411. { uint8_t a = z19; a = op_rol(a, &flags); z19 = op_ror(z19, &flags); z18 = op_ror(z18, &flags); (void)a; } { uint8_t a = z1C; a = op_rol(a, &flags); z1C = op_ror(z1C, &flags); z1B = op_ror(z1B, &flags); (void)a; } { uint8_t a = z1F; a = op_rol(a, &flags); z1F = op_ror(z1F, &flags); z1E = op_ror(z1E, &flags); (void)a; } // chunk5 also dec $2F here. Skip the auto-scale loop // -- jmp L7F96 in the original. goto matrixMultiply; } // L7F1A: auto-scale loop. Shift everything left until any of // the high bytes ($9F, $A3, $19, $1C, $1F) reaches the $40 // threshold (specifically: adc #$40 produces a negative // result, i.e. the original high byte is in [0x40..0xBF] in // unsigned terms = magnitude >= 64). // // Each iteration shifts: $9E/$9F (X delta), $A2/$A3 (Z delta), // and the three 24-bit accumulators left by 1. // // chunk5 increments the zoom counter $2F each iteration; once // $2F overflows from $FF -> $00 the auto-scale halts. We // bound iterations to that count (256 - initial $40 = 192 // max) so all-zero inputs don't spin forever. int autoScaleSteps = 192; while (autoScaleSteps-- > 0) { // Test #1: $9F adc #$40 bmi -> exit flags &= (uint8_t)~FLAG_C; op_adc(z9F, 0x40, &flags); if (flags & FLAG_N) break; // Test #2: $A3 flags &= (uint8_t)~FLAG_C; op_adc(zA3, 0x40, &flags); if (flags & FLAG_N) break; // Test #3: $19 flags &= (uint8_t)~FLAG_C; op_adc(z19, 0x40, &flags); if (flags & FLAG_N) break; // Test #4: $1C flags &= (uint8_t)~FLAG_C; op_adc(z1C, 0x40, &flags); if (flags & FLAG_N) break; // Test #5: $1F flags &= (uint8_t)~FLAG_C; op_adc(z1F, 0x40, &flags); if (flags & FLAG_N) break; // None of the high bytes overflowed -- shift everything // left by 1 (asl/rol cascade). // // MAME's TransformVertex7EBC at $7F03 shifts the deltas // ($9E/$9F, $A2/$A3) AND the 16-bit base accumulators // ($18/$19, $1A/$1B, $1C/$1D) WITHOUT shifting any LO // byte. Source chunk5.s shifts 24-bit (asl LO; rol MID; // rol HI). Port follows MAME here so Z output matches // MAME byte-exactly (project_fs2port_xform_drift.md). z9E = op_asl(z9E, &flags); z9F = op_rol(z9F, &flags); zA2 = op_asl(zA2, &flags); zA3 = op_rol(zA3, &flags); // 16-bit base shift: asl MID; rol HI (skip LO). z18 = op_asl(z18, &flags); z19 = op_rol(z19, &flags); z1B = op_asl(z1B, &flags); z1C = op_rol(z1C, &flags); z1E = op_asl(z1E, &flags); z1F = op_rol(z1F, &flags); } // (drop out either via early break in the test chain above or // by exhausting the iteration count.) matrixMultiply: // L7F96: 6 multiply-accumulate calls. Each multiplies a delta // hi-byte by a matrix entry and accumulates into $18/$19, // $1B/$1C, or $1E/$1F. The post-multiply sign-extension code // (L7FAD..L8049) computes a "high byte" $1A/$1D/$20 that // tracks whether the sum has overflowed into 24-bit territory. // ---- $9F * $79 -> $18/$19, sign-ext to $1A ---- { uint8_t lo, hi; op_l1818(ram[0x79], z9F, &lo, &hi); flags &= (uint8_t)~FLAG_C; z18 = op_adc(lo, z18, &flags); // tya; adc $19; sta $19 z19 = op_adc(hi, z19, &flags); // bpl L7FAD; lda #$FF; bmi L7FAF / L7FAD: lda #$00 uint8_t signByte = (flags & FLAG_N) ? 0xFF : 0x00; // bvc L7FB3; eor #$FF if (flags & FLAG_V) { signByte ^= 0xFF; } z1A = signByte; } // ---- $A3 * $85 -> $18/$19, accumulate sign-ext into $1A ---- { uint8_t lo, hi; op_l1818(ram[0x85], zA3, &lo, &hi); flags &= (uint8_t)~FLAG_C; z18 = op_adc(lo, z18, &flags); z19 = op_adc(hi, z19, &flags); // chunk5's `tya / bpl / lda` only set N from Y; do // NOT modify C. The `adc $1A` that follows inherits // the carry from `tya; adc $19; sta $19` above. uint8_t signByte = ((hi & 0x80) != 0) ? 0xFF : 0x00; z1A = op_adc(signByte, z1A, &flags); } // ---- $9F * $7B -> $1B/$1C, sign-ext to $1D ---- { uint8_t lo, hi; op_l1818(ram[0x7B], z9F, &lo, &hi); flags &= (uint8_t)~FLAG_C; z1B = op_adc(lo, z1B, &flags); z1C = op_adc(hi, z1C, &flags); uint8_t signByte = (flags & FLAG_N) ? 0xFF : 0x00; if (flags & FLAG_V) { signByte ^= 0xFF; } z1D = signByte; } // ---- $A3 * $87 -> $1B/$1C, accumulate sign-ext into $1D ---- { uint8_t lo, hi; op_l1818(ram[0x87], zA3, &lo, &hi); flags &= (uint8_t)~FLAG_C; z1B = op_adc(lo, z1B, &flags); z1C = op_adc(hi, z1C, &flags); // Sign decision uses N from `tya` (= bit 7 of hi). // Carry into `adc $1D` inherited from $1C adc. uint8_t signByte = ((hi & 0x80) != 0) ? 0xFF : 0x00; z1D = op_adc(signByte, z1D, &flags); } // ---- $9F * $7D -> $1E/$1F, sign-ext to $20 ---- { uint8_t lo, hi; op_l1818(ram[0x7D], z9F, &lo, &hi); flags &= (uint8_t)~FLAG_C; z1E = op_adc(lo, z1E, &flags); z1F = op_adc(hi, z1F, &flags); uint8_t signByte = (flags & FLAG_N) ? 0xFF : 0x00; if (flags & FLAG_V) { signByte ^= 0xFF; } z20 = signByte; } // ---- $A3 * $89 -> $1E/$1F, accumulate sign-ext into $20 ---- { uint8_t lo, hi; op_l1818(ram[0x89], zA3, &lo, &hi); flags &= (uint8_t)~FLAG_C; z1E = op_adc(lo, z1E, &flags); z1F = op_adc(hi, z1F, &flags); uint8_t signByte = ((hi & 0x80) != 0) ? 0xFF : 0x00; z20 = op_adc(signByte, z20, &flags); } // L8051: lda $1A; eor $1D; eor $20 // beq L8059 (signs all match) // cmp #$FF; bne L806D (signs all -1 -> match, fall through; // else go to L806D which does extra shift) uint8_t signCheck = z1A ^ z1D ^ z20; if (signCheck != 0 && signCheck != 0xFF) { // L806D: jsr L80B0; jsr L80B0; jsr L80B0; jmp L8091 op_l80b0(&z18, &z19, &z1A, &z1B, &z1C, &z1D, &z1E, &z1F, &z20); op_l80b0(&z18, &z19, &z1A, &z1B, &z1C, &z1D, &z1E, &z1F, &z20); op_l80b0(&z18, &z19, &z1A, &z1B, &z1C, &z1D, &z1E, &z1F, &z20); } else { // L8059: signs consistent -- now check that each // accumulator's high byte AGREES with its sign-ext. // lda $1A; eor $19; bmi L8070 // lda $1D; eor $1C; bmi L8070 // lda $20; eor $1F; bpl L8079; bmi L8070 bool xMismatch = ((z1A ^ z19) & 0x80) != 0; bool yMismatch = ((z1D ^ z1C) & 0x80) != 0; bool zSignZ = ((z20 ^ z1F) & 0x80) != 0; if (xMismatch || yMismatch || zSignZ) { // L8070: jsr L80B0; jsr L80B0; jmp L8091 op_l80b0(&z18, &z19, &z1A, &z1B, &z1C, &z1D, &z1E, &z1F, &z20); op_l80b0(&z18, &z19, &z1A, &z1B, &z1C, &z1D, &z1E, &z1F, &z20); } else { // L8079: lda $19; clc; adc #$40; bmi L808E // lda $1C; clc; adc #$40; bmi L808E // lda $1F; clc; adc #$40; bpl L8091 flags = 0; op_adc(z19, 0x40, &flags); if (flags & FLAG_N) { op_l80b0(&z18, &z19, &z1A, &z1B, &z1C, &z1D, &z1E, &z1F, &z20); } else { flags = 0; op_adc(z1C, 0x40, &flags); if (flags & FLAG_N) { op_l80b0(&z18, &z19, &z1A, &z1B, &z1C, &z1D, &z1E, &z1F, &z20); } else { flags = 0; op_adc(z1F, 0x40, &flags); if (flags & FLAG_N) { op_l80b0(&z18, &z19, &z1A, &z1B, &z1C, &z1D, &z1E, &z1F, &z20); } } } } } // L8091: store result to caller's vertex slot at destSlot.. // destSlot+5. chunk5 layout: lo X, hi X, lo Y, hi Y, lo Z, hi Z. ram[destSlot + 0] = z18; ram[destSlot + 1] = z19; ram[destSlot + 2] = z1B; ram[destSlot + 3] = z1C; ram[destSlot + 4] = z1E; ram[destSlot + 5] = z1F; if (chunk5DebugTrace()) { int16_t outX = (int16_t)((uint16_t)z18 | ((uint16_t)z19 << 8)); int16_t outY = (int16_t)((uint16_t)z1B | ((uint16_t)z1C << 8)); int16_t outZ = (int16_t)((uint16_t)z1E | ((uint16_t)z1F << 8)); int16_t streamX = (int16_t)((uint16_t)stream[1] | ((uint16_t)stream[2] << 8)); int16_t streamZ = (int16_t)((uint16_t)stream[3] | ((uint16_t)stream[4] << 8)); int16_t cam66 = (int16_t)((uint16_t)ram[0x66] | ((uint16_t)ram[0x67] << 8)); int16_t cam6A = (int16_t)((uint16_t)ram[0x6A] | ((uint16_t)ram[0x6B] << 8)); int16_t base4A = (int16_t)((uint16_t)ram[0x4A] | ((uint16_t)ram[0x4B] << 8)); int16_t base4D = (int16_t)((uint16_t)ram[0x4D] | ((uint16_t)ram[0x4E] << 8)); int16_t base50 = (int16_t)((uint16_t)ram[0x50] | ((uint16_t)ram[0x51] << 8)); fprintf(stderr, " xform[%02X]: stream=(%d,%d) cam66=%d cam6A=%d base=(%d,%d,%d) -> (%d,%d,%d) overflow=%c%c\n", destSlot, streamX, streamZ, cam66, cam6A, base4A, base4D, base50, outX, outY, outZ, xOverflow ? 'X' : '-', zOverflow ? 'Z' : '-'); fprintf(stderr, " deltas after auto-scale: dx=$%02X%02X dz=$%02X%02X " "matrix bytes: $79=$%02X $7B=$%02X $7D=$%02X $85=$%02X $87=$%02X $89=$%02X\n", z9F, z9E, zA3, zA2, ram[0x79], ram[0x7B], ram[0x7D], ram[0x85], ram[0x87], ram[0x89]); } // chunk5 returns via `lda #$05; jmp AddTo8B` which advances // $8B by 5 (op + 4 vertex bytes). return 5; } // PORT_XFORM_INTERPRETED=1 runs the MAME-patched chunk5 transform // bytecode directly via the 6502 interpreter. This bypasses the // C re-implementation entirely and produces byte-identical V1/V2 // output to MAME. Set this to compare drawlists against MAME's // frozen-frame capture. // // Entry point is $7E8E (= the actual TransformVertex7EBC entry in // the MAME-patched binary, NOT $7EBC like source). The routine // ends with `JMP $6806` which we treat as the stop PC. Inputs: // $66..$6B = cam (set by frame setup) // $79..$89 = matrix // $2A..$2F = base (set by L631D, copied to $18..$1D inside the // transform at $7E9B) // $8B/$8C = stream cursor (we set to offset of `stream` in ram) // Y = destination slot offset ($CB for V1, $D4 for V2) // Output: // $CB..$D0 (V1) or $D4..$D9 (V2) = 6 bytes of V (X lo/hi, Y lo/hi, // Z lo/hi) // $4A..$52 = 24-bit signed intermediate static int chunk5InterpretTransform7EBC(uint8_t *ram, const uint8_t *stream, uint8_t destSlot) { ptrdiff_t streamOff = stream - ram; if (streamOff < 0 || streamOff > 0xFFFF) { return 5; } // Save the ZP slots we touch so the port's other systems // don't see corrupted state. uint8_t save8B = ram[0x8B]; uint8_t save8C = ram[0x8C]; uint8_t saveE5 = ram[0xE5]; // Point ($8B) at the opcode byte; the transform's first read // is `LDA ($8B),Y` with Y=1 which fetches stream[1] = vertex X lo. ram[0x8B] = (uint8_t)(streamOff & 0xFF); ram[0x8C] = (uint8_t)((streamOff >> 8) & 0xFF); Cpu6502T cpu; cpu6502Init(&cpu, ram); cpu.y = destSlot; cpu.s = 0xFD; cpu.flagD = 0; cpu.flagI = 1; // Push a sentinel return address ($6806) so any stray RTS // inside the transform exits cleanly. JMP $6806 (the tail- // call exit) is detected as the stop PC by cpu6502Run. cpu6502PushReturn(&cpu, 0x6806); bool ok = cpu6502Run(&cpu, 0x7E8E, 0x6806, 1000000); (void)ok; ram[0x8B] = save8B; ram[0x8C] = save8C; ram[0xE5] = saveE5; // MAME-patched returns "5" (= LDA #$05; JMP $6806) so the // dispatcher advances the cursor by 5 bytes. Match that. return 5; } int chunk5TransformVertex7EBC(uint8_t *ram, const uint8_t *stream, uint8_t destSlot) { if (getenv("PORT_XFORM_INTERPRETED") != NULL) { return chunk5InterpretTransform7EBC(ram, stream, destSlot); } return transformVertexCommon(ram, stream, destSlot); } // chunk5 TransformVertex80C5 (chunk5.s line 4576-4707). Companion of // 7EBC: same matrix multiply but the stream provides a full // (Xlo, Xhi, Ylo, Yhi, Zlo, Zhi) triplet rather than X/Z only, and // the multiplier path uses ZPScale (16-bit signed multiply via // chunk4 ScaleC2ByC4) for all 9 matrix entries instead of the // 8-bit op_l1818. Used by opcodes $00/$01/$02 (xform-A vertex emit). // // The 6-byte stream layout (after opcode): // stream[1..2] = X (lo, hi) subtracted from $66/$67 // stream[3..4] = Y (lo, hi) subtracted from $68/$69 // stream[5..6] = Z (lo, hi) subtracted from $6A/$6B // // Matrix at $78..$89 (9 16-bit signed coefficients): // M[axis_out][delta_in] uses these slots (lo at addr, hi at addr+1): // X_out: $78 (X), $7E (Y), $84 (Z) // Y_out: $7A (X), $80 (Y), $86 (Z) // Z_out: $7C (X), $82 (Y), $88 (Z) // // Output is three int16 components written to destSlot..destSlot+5 // (X lo/hi, Y lo/hi, Z lo/hi). Returns the advance count for $8B // (always 7: opcode + 6 stream bytes). int chunk5TransformVertex80C5(uint8_t *ram, const uint8_t *stream, uint8_t destSlot) { // L80D2..L810E: 16-bit signed deltas with overflow recovery. // chunk5's overflow paths (L81D9/L820F/L821E) shift earlier // axes right when a later axis triggers signed overflow on // the SBC. We model that with a simple cascade: once an axis // sees an overflow it gets halved, and any preceding axis // gets halved too so the multiplies stay in proportion. int32_t streamX = (int16_t)((uint16_t)stream[1] | ((uint16_t)stream[2] << 8)); int32_t streamY = (int16_t)((uint16_t)stream[3] | ((uint16_t)stream[4] << 8)); int32_t streamZ = (int16_t)((uint16_t)stream[5] | ((uint16_t)stream[6] << 8)); int32_t camX = (int16_t)((uint16_t)ram[0x66] | ((uint16_t)ram[0x67] << 8)); int32_t camY = (int16_t)((uint16_t)ram[0x68] | ((uint16_t)ram[0x69] << 8)); int32_t camZ = (int16_t)((uint16_t)ram[0x6A] | ((uint16_t)ram[0x6B] << 8)); int32_t dx = streamX - camX; int32_t dy = streamY - camY; int32_t dz = streamZ - camZ; // Halve any axis that overflowed signed-16 (mirrors chunk5's // ror-on-V-flag recovery). if (dx < -32768 || dx > 32767) { dx >>= 1; } if (dy < -32768 || dy > 32767) { dy >>= 1; dx >>= 1; // L820F also re-shifts X if Y overflows } if (dz < -32768 || dz > 32767) { dz >>= 1; dx >>= 1; // L821E shifts X and Y when Z overflows dy >>= 1; } // L8110: auto-scale -- if any hi byte + $40 has bit 7 set // (= |delta_hi| >= $40, i.e., the value sits outside the // [-$4000, +$3FFF] band), arithmetic-shift all three deltas // right by 1. { uint8_t dxHi = (uint8_t)((uint32_t)dx >> 8); uint8_t dyHi = (uint8_t)((uint32_t)dy >> 8); uint8_t dzHi = (uint8_t)((uint32_t)dz >> 8); if (((uint8_t)(dxHi + 0x40) & 0x80) != 0 || ((uint8_t)(dyHi + 0x40) & 0x80) != 0 || ((uint8_t)(dzHi + 0x40) & 0x80) != 0) { dx >>= 1; dy >>= 1; dz >>= 1; } } // Read the 9 matrix coefficients. int16_t M_X_dX = (int16_t)((uint16_t)ram[0x78] | ((uint16_t)ram[0x79] << 8)); int16_t M_Y_dX = (int16_t)((uint16_t)ram[0x7A] | ((uint16_t)ram[0x7B] << 8)); int16_t M_Z_dX = (int16_t)((uint16_t)ram[0x7C] | ((uint16_t)ram[0x7D] << 8)); int16_t M_X_dY = (int16_t)((uint16_t)ram[0x7E] | ((uint16_t)ram[0x7F] << 8)); int16_t M_Y_dY = (int16_t)((uint16_t)ram[0x80] | ((uint16_t)ram[0x81] << 8)); int16_t M_Z_dY = (int16_t)((uint16_t)ram[0x82] | ((uint16_t)ram[0x83] << 8)); int16_t M_X_dZ = (int16_t)((uint16_t)ram[0x84] | ((uint16_t)ram[0x85] << 8)); int16_t M_Y_dZ = (int16_t)((uint16_t)ram[0x86] | ((uint16_t)ram[0x87] << 8)); int16_t M_Z_dZ = (int16_t)((uint16_t)ram[0x88] | ((uint16_t)ram[0x89] << 8)); // ZPScale calls + L8234 sums: out = M*dX + M*dY + M*dZ for each // output axis. Sum is straight 16-bit wraparound add (chunk5 // does adc on the lo bytes then adc-with-carry on the hi // bytes; in C that's just int16 plus). int16_t outX = (int16_t)((uint16_t)chunk5ScaleC2ByC4((int16_t)dx, M_X_dX) + (uint16_t)chunk5ScaleC2ByC4((int16_t)dy, M_X_dY) + (uint16_t)chunk5ScaleC2ByC4((int16_t)dz, M_X_dZ)); int16_t outY = (int16_t)((uint16_t)chunk5ScaleC2ByC4((int16_t)dx, M_Y_dX) + (uint16_t)chunk5ScaleC2ByC4((int16_t)dy, M_Y_dY) + (uint16_t)chunk5ScaleC2ByC4((int16_t)dz, M_Y_dZ)); int16_t outZ = (int16_t)((uint16_t)chunk5ScaleC2ByC4((int16_t)dx, M_Z_dX) + (uint16_t)chunk5ScaleC2ByC4((int16_t)dy, M_Z_dY) + (uint16_t)chunk5ScaleC2ByC4((int16_t)dz, M_Z_dZ)); // L8234 increments $30 each time the running sum's hi byte // sits outside the [-$40, +$3F] band (`adc #$40; bpl` skips // the inc). After all three sums, if $30 != 0 the L81D8 tail // arithmetic-shifts each output right by 1. We replicate by // testing each output and halving all three if any triggers. { uint8_t hiX = (uint8_t)((uint16_t)outX >> 8); uint8_t hiY = (uint8_t)((uint16_t)outY >> 8); uint8_t hiZ = (uint8_t)((uint16_t)outZ >> 8); bool needHalve = ((uint8_t)(hiX + 0x40) & 0x80) != 0 || ((uint8_t)(hiY + 0x40) & 0x80) != 0 || ((uint8_t)(hiZ + 0x40) & 0x80) != 0; if (needHalve) { outX = (int16_t)(outX >> 1); outY = (int16_t)(outY >> 1); outZ = (int16_t)(outZ >> 1); } } // L8091-equivalent: store 6 bytes (lo, hi, lo, hi, lo, hi). ram[destSlot + 0] = (uint8_t)((uint16_t)outX & 0xFFu); ram[destSlot + 1] = (uint8_t)(((uint16_t)outX >> 8) & 0xFFu); ram[destSlot + 2] = (uint8_t)((uint16_t)outY & 0xFFu); ram[destSlot + 3] = (uint8_t)(((uint16_t)outY >> 8) & 0xFFu); ram[destSlot + 4] = (uint8_t)((uint16_t)outZ & 0xFFu); ram[destSlot + 5] = (uint8_t)(((uint16_t)outZ >> 8) & 0xFFu); return 7; // chunk5 AddTo8B(7) at L813A }