fs2port/port/src/chunk5Transform.c

785 lines
34 KiB
C

// 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 <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#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 <skip>; 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
}