mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
PGXP: carry precision through GTE-as-multiplier arithmetic and lwl/lwr word copies
Fixes Ape Escape terrain seams: CPU-built subdivision midpoints and lwl/lwr-copied grid corners drew native next to precise neighbours. Adds pgxp_tri_ring/pgxp_store_ring. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1176,6 +1176,40 @@ default-on mod corrected textures, unticking it did nothing, and ticking it
|
||||
kept texture correction on after the player switched the mod off. Netplay
|
||||
then runs with PGXP fully off.
|
||||
|
||||
### G1.13 — CPU-built vertices: the GTE as a multiplier, and word copies (2026-10-06)
|
||||
|
||||
**Symptom.** Ape Escape with the PGXP mod: thin light/dark lines along the
|
||||
ground's grid edges at high internal resolution (bead beads-eio.3.274; present
|
||||
on the shipped f7f0ad10 pin, not a regression). Isolation on one savestate:
|
||||
PGXP off, or `geometry` off with texture correction on, removes them; texture
|
||||
correction off, nearest filtering, frame smoothing off do not. The
|
||||
`pgxp_tri_ring` showed whole ground cells drawn native next to precise
|
||||
neighbours and decals (0.9 px apart on the same projected vertex).
|
||||
|
||||
**Two producers dropped precision.** The `pgxp_store_ring` pinned both:
|
||||
1. *Subdivided near cells* (0x8001DB74). Edge midpoints are the projection of
|
||||
the 3D midpoint, computed on the CPU: CTC2 packs two tracked X (or Y)
|
||||
halves into a light-matrix row, MVMVA weights them by the vertex depths,
|
||||
MFC2 reads MAC, ADDU adds a rounding bias, DIV by the depth sum, MFLO, SH.
|
||||
CTC2 / CFC2 had no hook and MULT/DIV recorded imprecise results.
|
||||
2. *The terrain grid* (0x80044B00) copies projected corners with the
|
||||
unaligned-copy idiom `lwl/lwr` + `swl/swr` on aligned addresses, which the
|
||||
engine treated as untrackable.
|
||||
|
||||
**Fix (cpu-mode).** A scalar tier: registers carry an optional full-range
|
||||
precise value beside their half shadows. CTC2 / CFC2 are hooked (codegen 18;
|
||||
the BIOS emitter fingerprint now covers `pgxp_hook_emitter`), MVMVA recomputes
|
||||
its rows from the precise matrix halves / IR / translation and leaves scalars
|
||||
on MAC1-3 / IR1-3, and MFC2, ADD/SUB(I)(U), shifts, MULT/DIV, MF/MT HI/LO and
|
||||
OR-moves carry them; SH / SW turn a scalar that fits back into a coordinate
|
||||
half. The guest rounded that arithmetic its own way, so such halves are
|
||||
marked derived (`PGXP_F_DX/DY`): the GPU believes them within (-1, +2) px of
|
||||
the guest integer and the tolerance clamp ignores the guest's own rounding
|
||||
pixel. LWL at byte 3 / LWR at byte 0 (and SWL / SWR likewise) move the whole
|
||||
word and copy its shadow. Result in the Fossil Field scene: every ground
|
||||
triangle dataflow-precise, `tri_mixed` from ~10% of triangles to 474 of 2.05M,
|
||||
seams gone (`test_pgxp` pins the sequence).
|
||||
|
||||
## IR1 — Internal resolution presets (Native … 8K) and the GL scale ceiling (2026-09-26)
|
||||
|
||||
**What the player gets.** Settings → Display → **Internal resolution**: Native,
|
||||
|
||||
@@ -1 +1 @@
|
||||
13265e961396ac707b39701fc5787ae62c34e9c92c4b8b7fe3ea69e1cc86791e
|
||||
26dc9778f248831f353bb001d9cd64b928c57b566612c2df289ce1efc12db081
|
||||
|
||||
@@ -1 +1 @@
|
||||
2b30598a6f3db8c1a0c03eaa1f121d241fa53bd69e789ae202caf4d51ebe7ab6
|
||||
f15f850430c9c2a540f0155f4c1cc049ce2b020ad0836f86d5f8e98d603ac397
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -198,8 +198,9 @@
|
||||
* register-register frustum-edge emit; exact/keep NCLIP sites for every
|
||||
* sign-branch shape.
|
||||
* 17: the two lines merged (master exact-NCLIP emission, release keep-site
|
||||
* shapes and cull-scale sites). */
|
||||
#define PSX_OVERLAY_CODEGEN_VER 17
|
||||
* shapes and cull-scale sites).
|
||||
* 18: + PGXP_COP2 hooks on CFC2 / CTC2 (GTE-as-multiplier scalar tier). */
|
||||
#define PSX_OVERLAY_CODEGEN_VER 18
|
||||
|
||||
/* Auto codegen hash (generated by hash_codegen.cmake from the recompiler codegen
|
||||
* sources). Folded into the cache PATH next to cg<N> (gcc/<arch-abi>/cg<N>_<hash>/)
|
||||
|
||||
@@ -312,6 +312,77 @@ typedef struct PGXPWordMiss {
|
||||
/* Returns the total number of misses ever recorded. */
|
||||
uint64_t pgxp_word_miss_ring(const PGXPWordMiss **ring, uint32_t *cap);
|
||||
|
||||
/* MVMVA as a multiplier (scalar tier, cpu-mode). gte.cpp reports every
|
||||
* MVMVA with the operands exactly as it used them and the results it wrote;
|
||||
* the engine recomputes the rows from the precise matrix elements (CTC2'd
|
||||
* tracked halves), vector (IR scalars or tracked V halves) and translation,
|
||||
* and leaves the results as scalars on MAC1..3 / IR1..3 for MFC2. */
|
||||
typedef struct PGXPMvmva {
|
||||
uint32_t mx, vv, tv; /* matrix / vector / translation selectors */
|
||||
int shift; /* 12 when sf = 1, else 0 */
|
||||
int16_t m[3][3]; /* matrix elements as used */
|
||||
int16_t v[3]; /* vector as used */
|
||||
int64_t t[3]; /* translation as used (already << 12) */
|
||||
int32_t mac[3]; /* MAC1..3 written */
|
||||
int32_t ir[3]; /* IR1..3 written */
|
||||
uint32_t flag; /* FLAG after the op */
|
||||
} PGXPMvmva;
|
||||
void pgxp_gte_mvmva(const PGXPMvmva *op);
|
||||
/* Called after every GTE command (its function code): the IR / MAC scalars it
|
||||
* overwrote end there. */
|
||||
void pgxp_gte_op_end(uint32_t func);
|
||||
|
||||
/* Geometry-corrected triangles, newest at (seq - 1) % cap (TCP pgxp_tri_ring).
|
||||
* Always-on: every triangle prepare_precise_triangle resolves, with each
|
||||
* vertex's packet integers, the position handed to the rasterizer and where
|
||||
* that position came from. Two triangles that share a packet vertex (same
|
||||
* integers, same frame) but were handed different positions are a crack:
|
||||
* the ring is what localizes a seam to the producer of its vertices. */
|
||||
#define PGXP_TRI_RING_CAP 65536u
|
||||
typedef struct PGXPTriRecord {
|
||||
uint64_t seq;
|
||||
uint32_t frame; /* s_frame_count when the triangle was drawn */
|
||||
uint32_t src_addr; /* GP0 packet base address (0xFFFFFFFF = none) */
|
||||
uint8_t op; /* GP0 opcode */
|
||||
uint8_t pass; /* drawn inside a render-pass checkpoint (frame */
|
||||
/* smoothing redraws); set by the engine */
|
||||
uint8_t src[3]; /* PGXP_SRC_* per vertex */
|
||||
uint8_t vidx[3]; /* packet word index of each vertex */
|
||||
int16_t raw_x[3]; /* parsed 11-bit packet integers (pre-offset) */
|
||||
int16_t raw_y[3];
|
||||
uint32_t word[3]; /* the packet words */
|
||||
int32_t x16[3]; /* position handed to the rasterizer (16.16, */
|
||||
int32_t y16[3]; /* draw offset and widescreen already applied) */
|
||||
} PGXPTriRecord;
|
||||
void pgxp_note_triangle_detail(const PGXPTriRecord *rec);
|
||||
/* Returns the total number of triangles ever recorded. */
|
||||
uint64_t pgxp_tri_ring(const PGXPTriRecord **ring, uint32_t *cap);
|
||||
|
||||
/* Shadowed memory writes, newest at (seq - 1) % cap (TCP pgxp_store_ring).
|
||||
* Always-on while the engine is armed: every hooked SW / SH / SB / SWC2 into
|
||||
* RAM or scratchpad, with the storing PC, the source register's shadow state
|
||||
* and the shadow the destination word holds afterwards. Walking a packet
|
||||
* word's provenance backwards (store PC -> source register -> the load that
|
||||
* filled it) is how a precision loss is pinned to one instruction. */
|
||||
#define PGXP_STORE_RING_CAP 262144u
|
||||
typedef struct PGXPStoreRecord {
|
||||
uint64_t seq;
|
||||
uint32_t frame;
|
||||
uint32_t pc; /* g_debug_last_store_pc of the store */
|
||||
uint32_t addr; /* guest address written */
|
||||
uint32_t value; /* the value stored (half / byte for SH / SB) */
|
||||
uint8_t op; /* primary opcode (0x2B SW, 0x29 SH, 0x3A SWC2) */
|
||||
uint8_t reg; /* source GPR (or GTE data register for SWC2) */
|
||||
uint8_t src_live; /* source shadow belongs to this generation */
|
||||
uint8_t pad;
|
||||
uint16_t src_flags; /* source shadow flags (1 X, 2 Y, 4 Z) */
|
||||
uint16_t dst_flags; /* destination word shadow flags afterwards */
|
||||
int32_t dst_x16, dst_y16;
|
||||
} PGXPStoreRecord;
|
||||
uint64_t pgxp_store_ring(const PGXPStoreRecord **ring, uint32_t *cap);
|
||||
/* The host's store-PC breadcrumb and frame counter, for the rings above. */
|
||||
void pgxp_set_trace_sources(const uint32_t *store_pc, const uint64_t *frame);
|
||||
|
||||
/* Debug read of one shadow slot (TCP pgxp_shadow). `space`: 0 = guest
|
||||
* address (RAM / scratchpad), 1 = GPR index (32 = HI, 33 = LO), 2 = GTE data
|
||||
* register. Returns 0 when the slot does not exist; *live says whether it
|
||||
|
||||
@@ -124,8 +124,12 @@ void append_pgxp_hooks(uint32_t instr, std::string& code) {
|
||||
code, instr, reg_name(rt));
|
||||
return;
|
||||
case 0x12: { /* COP2 register transfers */
|
||||
/* Control transfers too: engines pack tracked vertex halves into a
|
||||
* matrix row with CTC2 and use MVMVA as a weighted-sum multiplier
|
||||
* (the scalar tier in pgxp.cpp follows the result back out). */
|
||||
const uint32_t cop_op = (instr >> 21) & 0x1F;
|
||||
if ((cop_op == 0x00 && rt != 0) || cop_op == 0x04) /* MFC2 / MTC2 */
|
||||
if (((cop_op == 0x00 || cop_op == 0x02) && rt != 0) || /* MFC2 / CFC2 */
|
||||
cop_op == 0x04 || cop_op == 0x06) /* MTC2 / CTC2 */
|
||||
code = fmt::format("{}\n PGXP_COP2(0x{:08X}u, {}, 0u);",
|
||||
code, instr, reg_name(rt));
|
||||
return;
|
||||
|
||||
@@ -66,6 +66,33 @@ int main() {
|
||||
CHECK(!emission_ends_on_preprocessor_directive("x = 1; /* #endif in a comment */"),
|
||||
"a '#' that is not the line's first token is not a directive");
|
||||
|
||||
/* COP2 register transfers all carry their PGXP_COP2 hook - the control
|
||||
* forms too, so the engine can follow vertex halves packed into a matrix
|
||||
* row (CTC2) through MVMVA and back (CFC2). A transfer to $zero writes no
|
||||
* register and stays unhooked. */
|
||||
{
|
||||
const auto hooked = [](uint32_t instr) {
|
||||
std::string code = "stmt;";
|
||||
PSXRecomp::append_pgxp_hooks(instr, code);
|
||||
return code;
|
||||
};
|
||||
const uint32_t mfc2 = 0x4805C800u; /* mfc2 a1, $25 */
|
||||
const uint32_t cfc2 = 0x4845C800u; /* cfc2 a1, $25 */
|
||||
const uint32_t mtc2 = 0x48994800u; /* mtc2 t9, $9 */
|
||||
const uint32_t ctc2 = 0x48C34000u; /* ctc2 v1, $8 */
|
||||
const uint32_t cfc2_zero = 0x4840C800u; /* cfc2 zero, $25 */
|
||||
CHECK(hooked(mfc2).find("PGXP_COP2(0x4805C800u, cpu->gpr[5], 0u);") != std::string::npos,
|
||||
"MFC2 carries PGXP_COP2 with its destination value");
|
||||
CHECK(hooked(cfc2).find("PGXP_COP2(0x4845C800u, cpu->gpr[5], 0u);") != std::string::npos,
|
||||
"CFC2 carries PGXP_COP2 with its destination value");
|
||||
CHECK(hooked(mtc2).find("PGXP_COP2(0x48994800u, cpu->gpr[25], 0u);") != std::string::npos,
|
||||
"MTC2 carries PGXP_COP2 with its source value");
|
||||
CHECK(hooked(ctc2).find("PGXP_COP2(0x48C34000u, cpu->gpr[3], 0u);") != std::string::npos,
|
||||
"CTC2 carries PGXP_COP2 with its source value");
|
||||
CHECK(hooked(cfc2_zero) == "stmt;",
|
||||
"a CFC2 to $zero writes no register and is not hooked");
|
||||
}
|
||||
|
||||
std::printf(failures ? "FAILED (%d)\n" : "ALL PASS\n", failures);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
|
||||
@@ -198,8 +198,9 @@
|
||||
* register-register frustum-edge emit; exact/keep NCLIP sites for every
|
||||
* sign-branch shape.
|
||||
* 17: the two lines merged (master exact-NCLIP emission, release keep-site
|
||||
* shapes and cull-scale sites). */
|
||||
#define PSX_OVERLAY_CODEGEN_VER 17
|
||||
* shapes and cull-scale sites).
|
||||
* 18: + PGXP_COP2 hooks on CFC2 / CTC2 (GTE-as-multiplier scalar tier). */
|
||||
#define PSX_OVERLAY_CODEGEN_VER 18
|
||||
|
||||
/* Auto codegen hash (generated by hash_codegen.cmake from the recompiler codegen
|
||||
* sources). Folded into the cache PATH next to cg<N> (gcc/<arch-abi>/cg<N>_<hash>/)
|
||||
|
||||
@@ -312,6 +312,77 @@ typedef struct PGXPWordMiss {
|
||||
/* Returns the total number of misses ever recorded. */
|
||||
uint64_t pgxp_word_miss_ring(const PGXPWordMiss **ring, uint32_t *cap);
|
||||
|
||||
/* MVMVA as a multiplier (scalar tier, cpu-mode). gte.cpp reports every
|
||||
* MVMVA with the operands exactly as it used them and the results it wrote;
|
||||
* the engine recomputes the rows from the precise matrix elements (CTC2'd
|
||||
* tracked halves), vector (IR scalars or tracked V halves) and translation,
|
||||
* and leaves the results as scalars on MAC1..3 / IR1..3 for MFC2. */
|
||||
typedef struct PGXPMvmva {
|
||||
uint32_t mx, vv, tv; /* matrix / vector / translation selectors */
|
||||
int shift; /* 12 when sf = 1, else 0 */
|
||||
int16_t m[3][3]; /* matrix elements as used */
|
||||
int16_t v[3]; /* vector as used */
|
||||
int64_t t[3]; /* translation as used (already << 12) */
|
||||
int32_t mac[3]; /* MAC1..3 written */
|
||||
int32_t ir[3]; /* IR1..3 written */
|
||||
uint32_t flag; /* FLAG after the op */
|
||||
} PGXPMvmva;
|
||||
void pgxp_gte_mvmva(const PGXPMvmva *op);
|
||||
/* Called after every GTE command (its function code): the IR / MAC scalars it
|
||||
* overwrote end there. */
|
||||
void pgxp_gte_op_end(uint32_t func);
|
||||
|
||||
/* Geometry-corrected triangles, newest at (seq - 1) % cap (TCP pgxp_tri_ring).
|
||||
* Always-on: every triangle prepare_precise_triangle resolves, with each
|
||||
* vertex's packet integers, the position handed to the rasterizer and where
|
||||
* that position came from. Two triangles that share a packet vertex (same
|
||||
* integers, same frame) but were handed different positions are a crack:
|
||||
* the ring is what localizes a seam to the producer of its vertices. */
|
||||
#define PGXP_TRI_RING_CAP 65536u
|
||||
typedef struct PGXPTriRecord {
|
||||
uint64_t seq;
|
||||
uint32_t frame; /* s_frame_count when the triangle was drawn */
|
||||
uint32_t src_addr; /* GP0 packet base address (0xFFFFFFFF = none) */
|
||||
uint8_t op; /* GP0 opcode */
|
||||
uint8_t pass; /* drawn inside a render-pass checkpoint (frame */
|
||||
/* smoothing redraws); set by the engine */
|
||||
uint8_t src[3]; /* PGXP_SRC_* per vertex */
|
||||
uint8_t vidx[3]; /* packet word index of each vertex */
|
||||
int16_t raw_x[3]; /* parsed 11-bit packet integers (pre-offset) */
|
||||
int16_t raw_y[3];
|
||||
uint32_t word[3]; /* the packet words */
|
||||
int32_t x16[3]; /* position handed to the rasterizer (16.16, */
|
||||
int32_t y16[3]; /* draw offset and widescreen already applied) */
|
||||
} PGXPTriRecord;
|
||||
void pgxp_note_triangle_detail(const PGXPTriRecord *rec);
|
||||
/* Returns the total number of triangles ever recorded. */
|
||||
uint64_t pgxp_tri_ring(const PGXPTriRecord **ring, uint32_t *cap);
|
||||
|
||||
/* Shadowed memory writes, newest at (seq - 1) % cap (TCP pgxp_store_ring).
|
||||
* Always-on while the engine is armed: every hooked SW / SH / SB / SWC2 into
|
||||
* RAM or scratchpad, with the storing PC, the source register's shadow state
|
||||
* and the shadow the destination word holds afterwards. Walking a packet
|
||||
* word's provenance backwards (store PC -> source register -> the load that
|
||||
* filled it) is how a precision loss is pinned to one instruction. */
|
||||
#define PGXP_STORE_RING_CAP 262144u
|
||||
typedef struct PGXPStoreRecord {
|
||||
uint64_t seq;
|
||||
uint32_t frame;
|
||||
uint32_t pc; /* g_debug_last_store_pc of the store */
|
||||
uint32_t addr; /* guest address written */
|
||||
uint32_t value; /* the value stored (half / byte for SH / SB) */
|
||||
uint8_t op; /* primary opcode (0x2B SW, 0x29 SH, 0x3A SWC2) */
|
||||
uint8_t reg; /* source GPR (or GTE data register for SWC2) */
|
||||
uint8_t src_live; /* source shadow belongs to this generation */
|
||||
uint8_t pad;
|
||||
uint16_t src_flags; /* source shadow flags (1 X, 2 Y, 4 Z) */
|
||||
uint16_t dst_flags; /* destination word shadow flags afterwards */
|
||||
int32_t dst_x16, dst_y16;
|
||||
} PGXPStoreRecord;
|
||||
uint64_t pgxp_store_ring(const PGXPStoreRecord **ring, uint32_t *cap);
|
||||
/* The host's store-PC breadcrumb and frame counter, for the rings above. */
|
||||
void pgxp_set_trace_sources(const uint32_t *store_pc, const uint64_t *frame);
|
||||
|
||||
/* Debug read of one shadow slot (TCP pgxp_shadow). `space`: 0 = guest
|
||||
* address (RAM / scratchpad), 1 = GPR index (32 = HI, 33 = LO), 2 = GTE data
|
||||
* register. Returns 0 when the slot does not exist; *live says whether it
|
||||
|
||||
@@ -5467,6 +5467,124 @@ static void handle_pgxp_miss_ring(int id, const char *json)
|
||||
free(buf);
|
||||
}
|
||||
|
||||
/* pgxp_tri_ring — the always-on ring of geometry-corrected triangles:
|
||||
* {"cmd":"pgxp_tri_ring","frame":F,"pass":0|1,"x0":..,"y0":..,"x1":..,
|
||||
* "y1":..,"count":N}. Every filter is optional: frame keeps one guest frame,
|
||||
* pass keeps game (0) or render-pass (1) triangles, the box keeps triangles
|
||||
* with any packet vertex inside [x0,x1]x[y0,y1] (packet integers). Returns
|
||||
* the newest N matches (default 4096) oldest first. Per triangle: packet
|
||||
* address, opcode, and per vertex the packet word, its integers, the 16.16
|
||||
* position handed to the rasterizer and its source (0 native, 1 dataflow,
|
||||
* 2 fallback). Join vertices by word across triangles to find cracks. */
|
||||
static void handle_pgxp_tri_ring(int id, const char *json)
|
||||
{
|
||||
const PGXPTriRecord *ring; uint32_t cap;
|
||||
const uint64_t total = pgxp_tri_ring(&ring, &cap);
|
||||
const uint64_t avail = total < cap ? total : cap;
|
||||
const int frame = json_get_int(json, "frame", -1);
|
||||
const int pass = json_get_int(json, "pass", -1);
|
||||
const int has_box = strstr(json, "\"x0\"") != NULL;
|
||||
const int x0 = json_get_int(json, "x0", -2048), y0 = json_get_int(json, "y0", -2048);
|
||||
const int x1 = json_get_int(json, "x1", 2047), y1 = json_get_int(json, "y1", 2047);
|
||||
int count = json_get_int(json, "count", 4096);
|
||||
if (count < 1) count = 1;
|
||||
if (count > 16384) count = 16384;
|
||||
/* Walk newest -> oldest collecting matches, then emit oldest first. */
|
||||
uint64_t *hits = (uint64_t *)malloc((size_t)count * sizeof(uint64_t));
|
||||
if (!hits) { send_err(id, "oom"); return; }
|
||||
int n = 0;
|
||||
for (uint64_t k = 0; k < avail && n < count; k++) {
|
||||
const uint64_t seq = total - 1 - k;
|
||||
const PGXPTriRecord *r = &ring[seq % cap];
|
||||
if (frame >= 0 && r->frame != (uint32_t)frame) continue;
|
||||
if (pass >= 0 && r->pass != (uint8_t)pass) continue;
|
||||
if (has_box) {
|
||||
int in = 0;
|
||||
for (int i = 0; i < 3; i++)
|
||||
if (r->raw_x[i] >= x0 && r->raw_x[i] <= x1 &&
|
||||
r->raw_y[i] >= y0 && r->raw_y[i] <= y1) in = 1;
|
||||
if (!in) continue;
|
||||
}
|
||||
hits[n++] = seq;
|
||||
}
|
||||
const size_t sz = (size_t)n * 400u + 256u;
|
||||
char *buf = (char *)malloc(sz);
|
||||
if (!buf) { free(hits); send_err(id, "oom"); return; }
|
||||
size_t p = (size_t)snprintf(buf, sz, "{\"id\":%d,\"ok\":true,\"total\":%llu,\"count\":%d,\"tris\":[",
|
||||
id, (unsigned long long)total, n);
|
||||
for (int j = n - 1; j >= 0; j--) {
|
||||
const PGXPTriRecord *r = &ring[hits[j] % cap];
|
||||
p += (size_t)snprintf(buf + p, sz - p,
|
||||
"%s{\"seq\":%llu,\"f\":%u,\"a\":\"0x%08X\",\"op\":\"0x%02X\",\"pass\":%u,\"v\":[",
|
||||
j == n - 1 ? "" : ",", (unsigned long long)r->seq, r->frame, r->src_addr,
|
||||
r->op, r->pass);
|
||||
for (int i = 0; i < 3; i++)
|
||||
p += (size_t)snprintf(buf + p, sz - p,
|
||||
"%s[%u,\"0x%08X\",%d,%d,%d,%d,%u]", i ? "," : "", r->vidx[i], r->word[i],
|
||||
r->raw_x[i], r->raw_y[i], r->x16[i], r->y16[i], r->src[i]);
|
||||
p += (size_t)snprintf(buf + p, sz - p, "]}");
|
||||
}
|
||||
snprintf(buf + p, sz - p, "]}");
|
||||
debug_server_send_line(buf);
|
||||
free(buf);
|
||||
free(hits);
|
||||
}
|
||||
|
||||
/* pgxp_store_ring — the always-on ring of shadowed memory writes:
|
||||
* {"cmd":"pgxp_store_ring","addr_lo":"0x..","addr_hi":"0x..","pc":"0x..",
|
||||
* "frame":F,"count":N}. addr range is [lo, hi); pc keeps one storing
|
||||
* instruction; frame keeps one guest frame.
|
||||
* Returns the newest N matches oldest first: store PC, address, value,
|
||||
* opcode, source register and its shadow (live, flags) and the destination
|
||||
* shadow afterwards (flags, 16.16 halves). */
|
||||
static void handle_pgxp_store_ring(int id, const char *json)
|
||||
{
|
||||
const PGXPStoreRecord *ring; uint32_t cap;
|
||||
const uint64_t total = pgxp_store_ring(&ring, &cap);
|
||||
if (!cap) { send_err(id, "pgxp store ring not allocated (engine never armed)"); return; }
|
||||
const uint64_t avail = total < cap ? total : cap;
|
||||
char s[32];
|
||||
uint32_t lo = 0, hi = 0xFFFFFFFFu, pc = 0;
|
||||
int has_pc = 0;
|
||||
if (json_get_str(json, "addr_lo", s, sizeof s)) lo = hex_to_u32(s);
|
||||
if (json_get_str(json, "addr_hi", s, sizeof s)) hi = hex_to_u32(s);
|
||||
if (json_get_str(json, "pc", s, sizeof s)) { pc = hex_to_u32(s); has_pc = 1; }
|
||||
const int frame = json_get_int(json, "frame", -1);
|
||||
int count = json_get_int(json, "count", 1024);
|
||||
if (count < 1) count = 1;
|
||||
if (count > 65536) count = 65536;
|
||||
uint64_t *hits = (uint64_t *)malloc((size_t)count * sizeof(uint64_t));
|
||||
if (!hits) { send_err(id, "oom"); return; }
|
||||
int n = 0;
|
||||
for (uint64_t k = 0; k < avail && n < count; k++) {
|
||||
const uint64_t seq = total - 1 - k;
|
||||
const PGXPStoreRecord *r = &ring[seq % cap];
|
||||
if (frame >= 0 && r->frame != (uint32_t)frame) continue;
|
||||
if (r->addr < lo || r->addr >= hi) continue;
|
||||
if (has_pc && r->pc != pc) continue;
|
||||
hits[n++] = seq;
|
||||
}
|
||||
const size_t sz = (size_t)n * 200u + 256u;
|
||||
char *buf = (char *)malloc(sz);
|
||||
if (!buf) { free(hits); send_err(id, "oom"); return; }
|
||||
size_t p = (size_t)snprintf(buf, sz, "{\"id\":%d,\"ok\":true,\"total\":%llu,\"count\":%d,\"stores\":[",
|
||||
id, (unsigned long long)total, n);
|
||||
for (int j = n - 1; j >= 0; j--) {
|
||||
const PGXPStoreRecord *r = &ring[hits[j] % cap];
|
||||
p += (size_t)snprintf(buf + p, sz - p,
|
||||
"%s{\"seq\":%llu,\"f\":%u,\"pc\":\"0x%08X\",\"a\":\"0x%08X\",\"v\":\"0x%08X\","
|
||||
"\"op\":\"0x%02X\",\"reg\":%u,\"src_live\":%u,\"src_flags\":%u,"
|
||||
"\"dst_flags\":%u,\"x16\":%d,\"y16\":%d}",
|
||||
j == n - 1 ? "" : ",", (unsigned long long)r->seq, r->frame, r->pc, r->addr,
|
||||
r->value, r->op, r->reg, r->src_live, r->src_flags, r->dst_flags,
|
||||
r->dst_x16, r->dst_y16);
|
||||
}
|
||||
snprintf(buf + p, sz - p, "]}");
|
||||
debug_server_send_line(buf);
|
||||
free(buf);
|
||||
free(hits);
|
||||
}
|
||||
|
||||
/* pgxp — live-tune the value-propagation engine for one-toggle isolation runs
|
||||
* without a rebuild: {"cmd":"pgxp","cpu_mode":0|1,"tolerance":F,
|
||||
* "position_fallback":0|1,"preserve_projection":0|1,"culling":0|1}. Fields
|
||||
@@ -15045,6 +15163,8 @@ static const CmdEntry s_commands[] = {
|
||||
{ "gpu_state", handle_gpu_state },
|
||||
{ "geom_correction", handle_geom_correction },
|
||||
{ "pgxp", handle_pgxp },
|
||||
{ "pgxp_tri_ring", handle_pgxp_tri_ring },
|
||||
{ "pgxp_store_ring", handle_pgxp_store_ring },
|
||||
{ "pgxp_shadow", handle_pgxp_shadow },
|
||||
{ "pgxp_miss_ring", handle_pgxp_miss_ring },
|
||||
{ "ws_aspect_cone_site", handle_ws_aspect_cone_site },
|
||||
@@ -15676,6 +15796,9 @@ void debug_server_init(int port)
|
||||
}
|
||||
s_call_focus_seq = 0;
|
||||
|
||||
/* PGXP rings stamp each record with the store PC and frame. */
|
||||
pgxp_set_trace_sources(&g_debug_last_store_pc, &s_frame_count);
|
||||
|
||||
/* EvCB walk ring (~240 KB). */
|
||||
if (!s_evcb_ring) s_evcb_ring = (EvCBSnapshot *)calloc(EVCB_RING_CAP, sizeof(EvCBSnapshot));
|
||||
s_evcb_ring_seq = 0;
|
||||
|
||||
@@ -2344,6 +2344,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
|
||||
psx_gte_stall(cpu);
|
||||
#endif
|
||||
gte_write_ctrl(cpu, (uint8_t)rd, cpu->gpr[rt]);
|
||||
psx_pgxp_cop2(cpu, insn, cpu->gpr[rt], 0);
|
||||
return 0;
|
||||
}
|
||||
if (cop_op & 0x10) {
|
||||
|
||||
+19
-3
@@ -4365,6 +4365,8 @@ static void prepare_precise_triangle(int i0, int i1, int i2,
|
||||
const int idx[3] = { i0, i1, i2 };
|
||||
int32_t fx[3], fy[3];
|
||||
int any_precise = 0, n_precise = 0;
|
||||
PGXPTriRecord rec;
|
||||
memset(&rec, 0, sizeof rec);
|
||||
for (int i = 0; i < 3; i++) {
|
||||
uint32_t word = gp0_cmd_buf[idx[i]];
|
||||
int32_t raw_x, raw_y;
|
||||
@@ -4374,10 +4376,12 @@ static void prepare_precise_triangle(int i0, int i1, int i2,
|
||||
: gp0_cmd_source_addr + (uint32_t)idx[i] * 4u;
|
||||
int32_t px, py;
|
||||
uint16_t sz;
|
||||
int src = PGXP_SRC_NATIVE;
|
||||
px = raw_x * 65536;
|
||||
py = raw_y * 65536;
|
||||
if (geometry && pgxp_get_precise_vertex(addr, word, raw_x, raw_y,
|
||||
&px, &py, &sz) != PGXP_SRC_NATIVE) {
|
||||
if (geometry)
|
||||
src = pgxp_get_precise_vertex(addr, word, raw_x, raw_y, &px, &py, &sz);
|
||||
if (src != PGXP_SRC_NATIVE) {
|
||||
any_precise = 1;
|
||||
n_precise++;
|
||||
}
|
||||
@@ -4386,9 +4390,21 @@ static void prepare_precise_triangle(int i0, int i1, int i2,
|
||||
any_precise = 1;
|
||||
fx[i] = (int32_t)((int64_t)px + (int64_t)(vx[i] - raw_x) * 65536);
|
||||
fy[i] = (int32_t)((int64_t)py + (int64_t)(vy[i] - raw_y) * 65536);
|
||||
rec.src[i] = (uint8_t)src;
|
||||
rec.vidx[i] = (uint8_t)idx[i];
|
||||
rec.raw_x[i] = (int16_t)raw_x;
|
||||
rec.raw_y[i] = (int16_t)raw_y;
|
||||
rec.word[i] = word;
|
||||
rec.x16[i] = fx[i];
|
||||
rec.y16[i] = fy[i];
|
||||
}
|
||||
if (geometry)
|
||||
if (geometry) {
|
||||
pgxp_note_triangle(n_precise); /* G1.1 crack exposure: mixed share */
|
||||
rec.frame = (uint32_t)s_frame_count;
|
||||
rec.src_addr = gp0_cmd_source_addr;
|
||||
rec.op = (uint8_t)(gp0_cmd_buf[0] >> 24);
|
||||
pgxp_note_triangle_detail(&rec);
|
||||
}
|
||||
if (!any_precise) {
|
||||
gr_set_precise_triangle(0, 0,0, 0,0, 0,0);
|
||||
return;
|
||||
|
||||
@@ -1686,6 +1686,21 @@ void gte_mvmva(GTEState* gte, uint32_t instr) {
|
||||
}
|
||||
|
||||
gte->set_error_flag();
|
||||
|
||||
/* PGXP scalar tier: MVMVA used as a multiplier on tracked vertex halves
|
||||
* (CTC2'd matrix rows) - hand the engine the operands as used and the
|
||||
* results, so MFC2 of MAC / IR can carry the precise weighted sum. */
|
||||
if (!s_gte_replay_sandbox) {
|
||||
PGXPMvmva op;
|
||||
op.mx = mx; op.vv = vv; op.tv = tv; op.shift = shift;
|
||||
std::memcpy(op.m, M, sizeof op.m);
|
||||
std::memcpy(op.v, V, sizeof op.v);
|
||||
std::memcpy(op.t, T, sizeof op.t);
|
||||
op.mac[0] = gte->MAC1; op.mac[1] = gte->MAC2; op.mac[2] = gte->MAC3;
|
||||
op.ir[0] = gte->IR1; op.ir[1] = gte->IR2; op.ir[2] = gte->IR3;
|
||||
op.flag = gte->FLAG;
|
||||
pgxp_gte_mvmva(&op);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -2239,6 +2254,7 @@ extern "C" void gte_execute(CPUState* cpu, uint32_t cmd) {
|
||||
}
|
||||
#endif
|
||||
gte_run_command(>e, cmd);
|
||||
pgxp_gte_op_end(func); /* IR / MAC scalars the op overwrote */
|
||||
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
if (func == 0x01 || func == 0x30) gte_rtp_record(>e, cmd);
|
||||
|
||||
+564
-40
@@ -55,6 +55,12 @@ enum {
|
||||
PGXP_F_VY = 1u << 1, /* high half (screen Y) tracked */
|
||||
PGXP_F_VZ = 1u << 2, /* projected depth rode along intact */
|
||||
PGXP_F_PROJECTION = 1u << 3,
|
||||
/* The tracked half was DERIVED by guest arithmetic that rounds (a CPU
|
||||
* divide of a precise sum, see the scalar tier below), so the guest
|
||||
* integer is that arithmetic's rounding of the value, not its floor: the
|
||||
* precise value may sit up to one pixel below or two above it. */
|
||||
PGXP_F_DX = 1u << 4,
|
||||
PGXP_F_DY = 1u << 5,
|
||||
PGXP_F_VXY = PGXP_F_VX | PGXP_F_VY,
|
||||
};
|
||||
|
||||
@@ -78,6 +84,26 @@ static PGXPValue *s_ram = nullptr; /* lazily allocated, 72 MiB VA */
|
||||
static PGXPValue s_scratch[PGXP_SCRATCH_WORDS];
|
||||
static PGXPValue s_gpr[34]; /* 32 GPRs + HI + LO */
|
||||
static PGXPValue s_gte[32]; /* GTE data registers */
|
||||
static PGXPValue s_gtc[32]; /* GTE control registers (CTC2) */
|
||||
|
||||
/* Scalar tier (cpu-mode). Engines also use the GTE as a multiplier for
|
||||
* vertex arithmetic: Ape Escape's terrain subdivider packs two projected X
|
||||
* (or Y) halves into a light-matrix row with CTC2, weights them with MVMVA
|
||||
* by their depths, reads MAC back with MFC2 and DIVs by the depth sum to get
|
||||
* the screen position of an edge midpoint. A 16.16 half cannot hold those
|
||||
* intermediate products, so registers carry an optional full-range precise
|
||||
* value beside their half shadows: valid while `gen` is current and `value`
|
||||
* still equals the register, killed by every hooked write that does not
|
||||
* produce one. It turns back into a half shadow only when stored (SH / SW)
|
||||
* as a 16-bit coordinate. */
|
||||
struct PGXPScalar {
|
||||
double v; /* precise value of the 32-bit register */
|
||||
uint32_t value; /* the register value it describes */
|
||||
uint32_t gen;
|
||||
};
|
||||
static PGXPScalar s_gpr_s[34]; /* GPRs + HI + LO */
|
||||
static PGXPScalar s_gte_s[32]; /* GTE data registers (IR, MAC) */
|
||||
static PGXPScalar s_gtc_s[32]; /* GTE control (TR / BK words) */
|
||||
|
||||
static uint32_t s_gen = 1;
|
||||
static int s_enabled = 0;
|
||||
@@ -97,6 +123,7 @@ static inline void recompute_active(void) {
|
||||
}
|
||||
|
||||
static PGXPStats s_stats;
|
||||
static PGXPStoreRecord *s_store_ring = nullptr; /* pgxp_store_ring; with s_ram */
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* Lifecycle */
|
||||
@@ -113,12 +140,19 @@ extern "C" void pgxp_invalidate_all(void) {
|
||||
std::memset(s_scratch, 0, sizeof(s_scratch));
|
||||
std::memset(s_gpr, 0, sizeof(s_gpr));
|
||||
std::memset(s_gte, 0, sizeof(s_gte));
|
||||
std::memset(s_gtc, 0, sizeof(s_gtc));
|
||||
std::memset(s_gpr_s, 0, sizeof(s_gpr_s));
|
||||
std::memset(s_gte_s, 0, sizeof(s_gte_s));
|
||||
std::memset(s_gtc_s, 0, sizeof(s_gtc_s));
|
||||
s_gen = 1;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" void pgxp_set_enabled(int enabled) {
|
||||
int resized = 0;
|
||||
if (enabled && !s_store_ring)
|
||||
s_store_ring = (PGXPStoreRecord *)std::calloc(PGXP_STORE_RING_CAP,
|
||||
sizeof(PGXPStoreRecord));
|
||||
if (enabled && !s_ram) {
|
||||
s_ram = (PGXPValue *)std::calloc(PGXP_RAM_WORDS, sizeof(PGXPValue));
|
||||
if (s_ram)
|
||||
@@ -220,6 +254,62 @@ extern "C" void pgxp_note_triangle(int precise) {
|
||||
else s_stats.tri_native++;
|
||||
}
|
||||
|
||||
static PGXPTriRecord s_tri_ring[PGXP_TRI_RING_CAP];
|
||||
static uint64_t s_tri_seq = 0;
|
||||
|
||||
static int ck_in_pass(void); /* render-pass checkpoint open */
|
||||
|
||||
extern "C" void pgxp_note_triangle_detail(const PGXPTriRecord *rec) {
|
||||
PGXPTriRecord *r = &s_tri_ring[s_tri_seq % PGXP_TRI_RING_CAP];
|
||||
*r = *rec;
|
||||
r->seq = s_tri_seq++;
|
||||
r->pass = ck_in_pass() ? 1u : 0u;
|
||||
}
|
||||
|
||||
extern "C" uint64_t pgxp_tri_ring(const PGXPTriRecord **ring, uint32_t *cap) {
|
||||
*ring = s_tri_ring;
|
||||
*cap = PGXP_TRI_RING_CAP;
|
||||
return s_tri_seq;
|
||||
}
|
||||
|
||||
/* Store PC and frame counter belong to the host (debug server); it hands
|
||||
* their addresses over once (pgxp_set_trace_sources) so the engine stays
|
||||
* linkable on its own in the white-box tests. */
|
||||
static const uint32_t *s_trace_pc = nullptr;
|
||||
static const uint64_t *s_trace_frame = nullptr;
|
||||
static uint64_t s_store_seq = 0;
|
||||
|
||||
extern "C" void pgxp_set_trace_sources(const uint32_t *store_pc,
|
||||
const uint64_t *frame) {
|
||||
s_trace_pc = store_pc;
|
||||
s_trace_frame = frame;
|
||||
}
|
||||
|
||||
static void note_store(uint32_t op, uint32_t reg, const PGXPValue *src,
|
||||
uint32_t addr, uint32_t value, const PGXPValue *dst) {
|
||||
if (!s_store_ring) return;
|
||||
PGXPStoreRecord *r = &s_store_ring[s_store_seq % PGXP_STORE_RING_CAP];
|
||||
r->seq = s_store_seq++;
|
||||
r->frame = s_trace_frame ? (uint32_t)*s_trace_frame : 0u;
|
||||
r->pc = s_trace_pc ? *s_trace_pc : 0u;
|
||||
r->addr = addr;
|
||||
r->value = value;
|
||||
r->op = (uint8_t)op;
|
||||
r->reg = (uint8_t)reg;
|
||||
r->src_live = (src && src->gen == s_gen) ? 1u : 0u;
|
||||
r->pad = 0;
|
||||
r->src_flags = src ? src->flags : 0u;
|
||||
r->dst_flags = (dst && dst->gen == s_gen) ? dst->flags : 0u;
|
||||
r->dst_x16 = dst ? dst->x16 : 0;
|
||||
r->dst_y16 = dst ? dst->y16 : 0;
|
||||
}
|
||||
|
||||
extern "C" uint64_t pgxp_store_ring(const PGXPStoreRecord **ring, uint32_t *cap) {
|
||||
*ring = s_store_ring;
|
||||
*cap = s_store_ring ? PGXP_STORE_RING_CAP : 0u;
|
||||
return s_store_ring ? s_store_seq : 0u;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* Address mapping + validation */
|
||||
/* ------------------------------------------------------------------------- */
|
||||
@@ -258,7 +348,8 @@ static PGXPValue *s_ck_ram = nullptr; /* s_ram the bits index */
|
||||
static PGXPJournalEntry *s_ck_log = nullptr;
|
||||
static size_t s_ck_n = 0, s_ck_cap = 0;
|
||||
static int s_ck_lossy = 0;
|
||||
static PGXPValue s_ck_gpr[34], s_ck_gte[32];
|
||||
static PGXPValue s_ck_gpr[34], s_ck_gte[32], s_ck_gtc[32];
|
||||
static PGXPScalar s_ck_gpr_s[34], s_ck_gte_s[32], s_ck_gtc_s[32];
|
||||
static uint32_t s_ck_gen = 0, s_ck_suppress = 0;
|
||||
static int s_ck_deferred = 0;
|
||||
|
||||
@@ -297,6 +388,8 @@ static void ck_wrapped(void) {
|
||||
if (s_ck_depth != 0) s_ck_lossy = 1; /* shadows were cleared */
|
||||
}
|
||||
|
||||
static int ck_in_pass(void) { return s_ck_depth != 0; }
|
||||
|
||||
static inline PGXPValue *pgxp_ptr_w(uint32_t addr) {
|
||||
PGXPValue *pv = pgxp_ptr(addr);
|
||||
ck_note(pv);
|
||||
@@ -314,6 +407,10 @@ extern "C" void pgxp_checkpoint_begin(void) {
|
||||
s_ck_n = 0;
|
||||
std::memcpy(s_ck_gpr, s_gpr, sizeof s_gpr);
|
||||
std::memcpy(s_ck_gte, s_gte, sizeof s_gte);
|
||||
std::memcpy(s_ck_gtc, s_gtc, sizeof s_gtc);
|
||||
std::memcpy(s_ck_gpr_s, s_gpr_s, sizeof s_gpr_s);
|
||||
std::memcpy(s_ck_gte_s, s_gte_s, sizeof s_gte_s);
|
||||
std::memcpy(s_ck_gtc_s, s_gtc_s, sizeof s_gtc_s);
|
||||
s_ck_gen = s_gen;
|
||||
s_ck_suppress = s_suppress;
|
||||
s_ck_deferred = s_deferred_invalidate;
|
||||
@@ -331,6 +428,10 @@ extern "C" void pgxp_checkpoint_rollback(void) {
|
||||
s_ck_n = 0;
|
||||
std::memcpy(s_gpr, s_ck_gpr, sizeof s_gpr);
|
||||
std::memcpy(s_gte, s_ck_gte, sizeof s_gte);
|
||||
std::memcpy(s_gtc, s_ck_gtc, sizeof s_gtc);
|
||||
std::memcpy(s_gpr_s, s_ck_gpr_s, sizeof s_gpr_s);
|
||||
std::memcpy(s_gte_s, s_ck_gte_s, sizeof s_gte_s);
|
||||
std::memcpy(s_gtc_s, s_ck_gtc_s, sizeof s_gtc_s);
|
||||
s_gen = s_ck_gen;
|
||||
/* A watchdog abort can leave a suppress bracket open: the machine it
|
||||
* interrupted is gone, so its bracket is too. */
|
||||
@@ -354,9 +455,9 @@ static inline void pv_validate(PGXPValue *pv, uint32_t actual) {
|
||||
uint32_t diff = pv->value ^ actual;
|
||||
if (diff) pv->flags &= (uint16_t)~PGXP_F_PROJECTION;
|
||||
if ((pv->flags & PGXP_F_VX) && (diff & 0x0000FFFFu))
|
||||
pv->flags &= (uint16_t)~(PGXP_F_VX | PGXP_F_VZ);
|
||||
pv->flags &= (uint16_t)~(PGXP_F_VX | PGXP_F_DX | PGXP_F_VZ);
|
||||
if ((pv->flags & PGXP_F_VY) && (diff & 0xFFFF0000u))
|
||||
pv->flags &= (uint16_t)~(PGXP_F_VY | PGXP_F_VZ);
|
||||
pv->flags &= (uint16_t)~(PGXP_F_VY | PGXP_F_DY | PGXP_F_VZ);
|
||||
pv->value = actual;
|
||||
}
|
||||
|
||||
@@ -383,6 +484,67 @@ static inline uint32_t f_shamt(uint32_t i) { return (i >> 6) & 31u; }
|
||||
static inline uint32_t f_funct(uint32_t i) { return i & 63u; }
|
||||
static inline int32_t f_simm(uint32_t i) { return (int32_t)(int16_t)(i & 0xFFFFu); }
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* Scalar tier helpers */
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
static inline void sc_kill(PGXPScalar *s) { s->gen = 0; }
|
||||
static inline void sc_set(PGXPScalar *s, double v, uint32_t value) {
|
||||
s->v = v; s->value = value; s->gen = s_gen;
|
||||
}
|
||||
static inline int sc_valid(const PGXPScalar *s, uint32_t value) {
|
||||
return s->gen == s_gen && s->value == value;
|
||||
}
|
||||
|
||||
/* A GPR write that produces no scalar ends whatever scalar the register had. */
|
||||
static inline void gpr_written(uint32_t r) { if (r != 0) sc_kill(&s_gpr_s[r]); }
|
||||
|
||||
/* A register holding one 16-bit half, sign- or zero-extended (LH / LHU, or an
|
||||
* MTC2 / CTC2 of such a register), whose half shadow is tracked: its precise
|
||||
* value as a number. */
|
||||
static inline int half_scalar(const PGXPValue *pv, uint32_t value, double *out) {
|
||||
if (!pv || pv->gen != s_gen || pv->value != value) return 0;
|
||||
if ((pv->flags & PGXP_F_VXY) != PGXP_F_VXY) return 0;
|
||||
const int32_t sx = (int32_t)(int16_t)(value & 0xFFFFu);
|
||||
const int32_t zx = (int32_t)(value & 0xFFFFu);
|
||||
if ((int32_t)value != sx && (int32_t)value != zx) return 0;
|
||||
if (pv->y16 != (((int32_t)value >> 16) << 16)) return 0; /* exact extension */
|
||||
double v = pv->x16 / 65536.0; /* the half read as int16 */
|
||||
if ((int32_t)value == zx && zx != sx) v += 65536.0; /* LHU of a negative half */
|
||||
*out = v;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* The precise value of a register: its scalar, else its half shadow as a
|
||||
* number, else the exact integer (*precise = 0). */
|
||||
static inline double reg_scalar(const PGXPScalar *s, const PGXPValue *pv,
|
||||
uint32_t value, int is_unsigned, int *precise) {
|
||||
double v;
|
||||
if (s && sc_valid(s, value)) { *precise = 1; return s->v; }
|
||||
if (pv && half_scalar(pv, value, &v)) { *precise = 1; return v; }
|
||||
*precise = 0;
|
||||
return is_unsigned ? (double)value : (double)(int32_t)value;
|
||||
}
|
||||
|
||||
static inline double gpr_scalar(uint32_t r, uint32_t value, int is_unsigned,
|
||||
int *precise) {
|
||||
if (r == 0) { *precise = 0; return 0.0; }
|
||||
return reg_scalar(&s_gpr_s[r], &s_gpr[r], value, is_unsigned, precise);
|
||||
}
|
||||
|
||||
/* A scalar that is about to become a 16-bit coordinate half: accept it when
|
||||
* it describes `half` (the stored integer) within the derived window, and
|
||||
* say whether the guest integer is its floor (plain) or a rounding of it
|
||||
* (derived). Returns the 16.16 value, or 0 with *ok = 0. */
|
||||
static inline int32_t scalar_to_half(double v, int32_t half, int *ok, int *derived) {
|
||||
const double d = v - (double)half;
|
||||
*ok = 0; *derived = 0;
|
||||
if (!(d > -1.0 && d < 2.0)) return 0; /* also rejects NaN */
|
||||
*ok = 1;
|
||||
*derived = std::floor(v) != (double)half;
|
||||
return (int32_t)std::floor(v * 65536.0);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* Memory-mode hooks: loads / stores */
|
||||
/* ------------------------------------------------------------------------- */
|
||||
@@ -401,8 +563,15 @@ extern "C" void psx_pgxp_load(struct CPUState *cpu, uint32_t instr,
|
||||
uint32_t rt = f_rt(instr);
|
||||
if (rt == 0) return;
|
||||
PGXPValue *dst = &s_gpr[rt];
|
||||
gpr_written(rt); /* memory carries no scalars */
|
||||
|
||||
switch (f_op(instr)) {
|
||||
uint32_t lop = f_op(instr);
|
||||
/* LWL at byte 3 and LWR at byte 0 of a word each load the WHOLE word
|
||||
* (little-endian R3000A): the unaligned-copy idiom lwl/lwr on an aligned
|
||||
* address is a plain word load, so it carries the word's shadow. */
|
||||
if ((lop == 0x22 && (addr & 3u) == 3u) || (lop == 0x26 && (addr & 3u) == 0u))
|
||||
lop = 0x23;
|
||||
switch (lop) {
|
||||
case 0x23: { /* LW */
|
||||
PGXPValue *src = pgxp_ptr_w(addr); /* validation may mutate it */
|
||||
if (src && src->gen == s_gen) {
|
||||
@@ -430,6 +599,8 @@ extern "C" void psx_pgxp_load(struct CPUState *cpu, uint32_t instr,
|
||||
if ((src->flags & want) && ((src->value ^ actual_half) & mask) == 0) {
|
||||
dst->x16 = hi_half ? src->y16 : src->x16;
|
||||
dst->flags |= PGXP_F_VX;
|
||||
if (src->flags & (hi_half ? PGXP_F_DY : PGXP_F_DX))
|
||||
dst->flags |= PGXP_F_DX;
|
||||
}
|
||||
}
|
||||
return;
|
||||
@@ -449,14 +620,38 @@ extern "C" void psx_pgxp_store(struct CPUState *cpu, uint32_t instr,
|
||||
uint32_t rt = f_rt(instr);
|
||||
PGXPValue *src = (rt != 0) ? &s_gpr[rt] : nullptr;
|
||||
|
||||
switch (f_op(instr)) {
|
||||
uint32_t sop = f_op(instr);
|
||||
/* SWL at byte 3 and SWR at byte 0 each store the WHOLE word (the
|
||||
* unaligned-copy idiom on an aligned address): a plain word store. */
|
||||
if ((sop == 0x2A && (addr & 3u) == 3u) || (sop == 0x2E && (addr & 3u) == 0u))
|
||||
sop = 0x2B;
|
||||
switch (sop) {
|
||||
case 0x2B: { /* SW */
|
||||
PGXPValue before;
|
||||
std::memset(&before, 0, sizeof before);
|
||||
if (src) before = *src;
|
||||
if (src && src->gen == s_gen) {
|
||||
pv_validate(src, value);
|
||||
*dst = *src;
|
||||
} else {
|
||||
pv_reset(dst, value);
|
||||
}
|
||||
/* A register holding one coordinate as a full word (a scalar that
|
||||
* fits a half, e.g. a CPU-divided position) stores as that half,
|
||||
* with an exact sign extension above it. */
|
||||
if (s_cpu_mode && rt != 0 && sc_valid(&s_gpr_s[rt], value) &&
|
||||
(int32_t)value == (int32_t)(int16_t)(value & 0xFFFFu)) {
|
||||
int ok, derived;
|
||||
int32_t v16 = scalar_to_half(s_gpr_s[rt].v, (int16_t)(value & 0xFFFFu),
|
||||
&ok, &derived);
|
||||
if (ok) {
|
||||
pv_reset(dst, value);
|
||||
dst->x16 = v16;
|
||||
dst->y16 = ((int32_t)value >> 16) << 16;
|
||||
dst->flags = (uint16_t)(PGXP_F_VXY | (derived ? PGXP_F_DX : 0));
|
||||
}
|
||||
}
|
||||
note_store(0x2B, rt, src ? &before : nullptr, addr, value, dst);
|
||||
return;
|
||||
}
|
||||
case 0x29: { /* SH */
|
||||
@@ -468,13 +663,28 @@ extern "C" void psx_pgxp_store(struct CPUState *cpu, uint32_t instr,
|
||||
* half-write — the vertex it described no longer exists whole. */
|
||||
if (hi_half) dst->value = (dst->value & 0x0000FFFFu) | (half << 16);
|
||||
else dst->value = (dst->value & 0xFFFF0000u) | half;
|
||||
dst->flags &= (uint16_t)~((hi_half ? PGXP_F_VY : PGXP_F_VX) | PGXP_F_VZ | PGXP_F_PROJECTION);
|
||||
dst->flags &= (uint16_t)~((hi_half ? (PGXP_F_VY | PGXP_F_DY) : (PGXP_F_VX | PGXP_F_DX)) |
|
||||
PGXP_F_VZ | PGXP_F_PROJECTION);
|
||||
dst->z = 0;
|
||||
if (src && src->gen == s_gen && (src->flags & PGXP_F_VX) &&
|
||||
((src->value ^ value) & 0xFFFFu) == 0) {
|
||||
if (hi_half) { dst->y16 = src->x16; dst->flags |= PGXP_F_VY; }
|
||||
else { dst->x16 = src->x16; dst->flags |= PGXP_F_VX; }
|
||||
int ok = 0, derived = 0;
|
||||
int32_t v16 = 0;
|
||||
/* Hooks pass either the full register or just the stored half (the
|
||||
* interpreters), so the scalar is matched on the stored half. */
|
||||
if (s_cpu_mode && rt != 0 && s_gpr_s[rt].gen == s_gen &&
|
||||
(s_gpr_s[rt].value & 0xFFFFu) == half)
|
||||
v16 = scalar_to_half(s_gpr_s[rt].v, (int16_t)half, &ok, &derived);
|
||||
if (ok) {
|
||||
/* A scalar (e.g. a midpoint computed by MVMVA + DIV) becomes a
|
||||
* coordinate half here. */
|
||||
if (hi_half) { dst->y16 = v16; dst->flags |= (uint16_t)(PGXP_F_VY | (derived ? PGXP_F_DY : 0)); }
|
||||
else { dst->x16 = v16; dst->flags |= (uint16_t)(PGXP_F_VX | (derived ? PGXP_F_DX : 0)); }
|
||||
} else if (src && src->gen == s_gen && (src->flags & PGXP_F_VX) &&
|
||||
((src->value ^ value) & 0xFFFFu) == 0) {
|
||||
const uint16_t d = (src->flags & PGXP_F_DX) ? (hi_half ? PGXP_F_DY : PGXP_F_DX) : 0;
|
||||
if (hi_half) { dst->y16 = src->x16; dst->flags |= (uint16_t)(PGXP_F_VY | d); }
|
||||
else { dst->x16 = src->x16; dst->flags |= (uint16_t)(PGXP_F_VX | d); }
|
||||
}
|
||||
note_store(0x29, rt, src, addr, half, dst);
|
||||
return;
|
||||
}
|
||||
case 0x28: { /* SB */
|
||||
@@ -482,7 +692,8 @@ extern "C" void psx_pgxp_store(struct CPUState *cpu, uint32_t instr,
|
||||
uint32_t shift = (addr & 3u) * 8u;
|
||||
dst->value = (dst->value & ~(0xFFu << shift)) |
|
||||
((value & 0xFFu) << shift);
|
||||
dst->flags &= (uint16_t)~(((addr & 2u) ? PGXP_F_VY : PGXP_F_VX) | PGXP_F_VZ | PGXP_F_PROJECTION);
|
||||
dst->flags &= (uint16_t)~(((addr & 2u) ? (PGXP_F_VY | PGXP_F_DY) : (PGXP_F_VX | PGXP_F_DX)) |
|
||||
PGXP_F_VZ | PGXP_F_PROJECTION);
|
||||
dst->z = 0;
|
||||
return;
|
||||
}
|
||||
@@ -511,6 +722,7 @@ extern "C" void psx_pgxp_cop2(struct CPUState *cpu, uint32_t instr,
|
||||
} else {
|
||||
pv_reset(dst, value);
|
||||
}
|
||||
sc_kill(&s_gte_s[f_rt(instr)]); /* memory carries no scalars */
|
||||
gte_sxy2_mirror(f_rt(instr));
|
||||
return;
|
||||
}
|
||||
@@ -518,12 +730,14 @@ extern "C" void psx_pgxp_cop2(struct CPUState *cpu, uint32_t instr,
|
||||
PGXPValue *dst = pgxp_ptr_w(addr);
|
||||
if (!dst) return;
|
||||
PGXPValue *src = &s_gte[f_rt(instr)];
|
||||
const PGXPValue before = *src;
|
||||
if (src->gen == s_gen) {
|
||||
pv_validate(src, value);
|
||||
*dst = *src;
|
||||
} else {
|
||||
pv_reset(dst, value);
|
||||
}
|
||||
note_store(0x3A, f_rt(instr), &before, addr, value, dst);
|
||||
return;
|
||||
}
|
||||
case 0x12: { /* COP2 register transfers */
|
||||
@@ -538,29 +752,71 @@ extern "C" void psx_pgxp_cop2(struct CPUState *cpu, uint32_t instr,
|
||||
} else {
|
||||
pv_reset(&s_gpr[rt], value);
|
||||
}
|
||||
/* MFC2 of MAC / IR carries an MVMVA result's scalar. */
|
||||
if (s_cpu_mode && sc_valid(&s_gte_s[f_rd(instr)], value))
|
||||
s_gpr_s[rt] = s_gte_s[f_rd(instr)];
|
||||
else
|
||||
gpr_written(rt);
|
||||
return;
|
||||
}
|
||||
case 0x04: { /* MTC2: gte[rd] <- gpr[rt] */
|
||||
uint32_t rt = f_rt(instr);
|
||||
PGXPValue *dst = &s_gte[f_rd(instr)];
|
||||
uint32_t rd = f_rd(instr);
|
||||
PGXPValue *dst = &s_gte[rd];
|
||||
if (rt != 0 && s_gpr[rt].gen == s_gen) {
|
||||
pv_validate(&s_gpr[rt], value);
|
||||
*dst = s_gpr[rt];
|
||||
} else {
|
||||
pv_reset(dst, value);
|
||||
}
|
||||
/* IR0..IR3 hold the low half sign-extended: a scalar that fits
|
||||
* a half moves in as the register's precise value. */
|
||||
sc_kill(&s_gte_s[rd]);
|
||||
if (s_cpu_mode && rd >= 8 && rd <= 11 && rt != 0 &&
|
||||
sc_valid(&s_gpr_s[rt], value) &&
|
||||
(int32_t)value == (int32_t)(int16_t)(value & 0xFFFFu))
|
||||
sc_set(&s_gte_s[rd], s_gpr_s[rt].v, value);
|
||||
/* An SXYP write (rd==15) already shifted the FIFO shadows
|
||||
* (pgxp_gte_reg_written, from the GTE register write). */
|
||||
gte_sxy2_mirror(f_rd(instr));
|
||||
gte_sxy2_mirror(rd);
|
||||
return;
|
||||
}
|
||||
case 0x02: { /* CFC2: control regs carry no
|
||||
positions */
|
||||
case 0x02: { /* CFC2: gpr[rt] <- gtc[rd] */
|
||||
uint32_t rt = f_rt(instr);
|
||||
if (rt != 0) pv_reset(&s_gpr[rt], value);
|
||||
if (rt == 0) return;
|
||||
uint32_t rd = f_rd(instr);
|
||||
if (s_cpu_mode && s_gtc[rd].gen == s_gen) {
|
||||
pv_validate(&s_gtc[rd], value);
|
||||
s_gpr[rt] = s_gtc[rd];
|
||||
} else {
|
||||
pv_reset(&s_gpr[rt], value);
|
||||
}
|
||||
if (s_cpu_mode && sc_valid(&s_gtc_s[rd], value))
|
||||
s_gpr_s[rt] = s_gtc_s[rd];
|
||||
else
|
||||
gpr_written(rt);
|
||||
return;
|
||||
}
|
||||
default: /* CTC2: nothing shadowed */
|
||||
case 0x06: { /* CTC2: gtc[rd] <- gpr[rt] */
|
||||
/* Control registers are written only by CTC2, so their shadows
|
||||
* stay exact until the next CTC2: matrix rows packed from two
|
||||
* tracked halves (the GTE-as-multiplier idiom) and translation
|
||||
* words carrying a scalar. */
|
||||
uint32_t rt = f_rt(instr);
|
||||
uint32_t rd = f_rd(instr);
|
||||
if (s_cpu_mode && rt != 0 && s_gpr[rt].gen == s_gen) {
|
||||
pv_validate(&s_gpr[rt], value);
|
||||
s_gtc[rd] = s_gpr[rt];
|
||||
} else {
|
||||
pv_reset(&s_gtc[rd], value);
|
||||
}
|
||||
if (s_cpu_mode && rt != 0 && sc_valid(&s_gpr_s[rt], value))
|
||||
s_gtc_s[rd] = s_gpr_s[rt];
|
||||
else
|
||||
sc_kill(&s_gtc_s[rd]);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -588,6 +844,18 @@ static inline int32_t comp16(const PGXPValue *pv, uint32_t value, int hi_half) {
|
||||
return ((int32_t)(int16_t)(hi_half ? (value >> 16) : value)) << 16;
|
||||
}
|
||||
|
||||
/* The derived-half marks (PGXP_F_DX / DY) an operand contributes through
|
||||
* comp16: a tracked derived half keeps its rounding window in the sum. */
|
||||
static inline uint16_t derived_bits(const PGXPValue *pv, uint32_t value) {
|
||||
if (!pv || pv->gen != s_gen) return 0;
|
||||
uint16_t d = 0;
|
||||
if ((pv->flags & PGXP_F_DX) && (pv->flags & PGXP_F_VX) &&
|
||||
((pv->value ^ value) & 0x0000FFFFu) == 0) d |= PGXP_F_DX;
|
||||
if ((pv->flags & PGXP_F_DY) && (pv->flags & PGXP_F_VY) &&
|
||||
((pv->value ^ value) & 0xFFFF0000u) == 0) d |= PGXP_F_DY;
|
||||
return d;
|
||||
}
|
||||
|
||||
static inline int comp_tracked(const PGXPValue *pv, uint32_t value, int hi_half) {
|
||||
if (!pv || pv->gen != s_gen) return 0;
|
||||
if (!hi_half)
|
||||
@@ -621,9 +889,15 @@ static inline int halves_independent(uint32_t a, uint32_t b, uint32_t r, int sub
|
||||
static inline int half_exact(const PGXPValue *pv, uint32_t value, int hi_half) {
|
||||
if (!comp_tracked(pv, value, hi_half)) return 0;
|
||||
int32_t v16 = hi_half ? pv->y16 : pv->x16;
|
||||
const int32_t half = (int32_t)(int16_t)(hi_half ? (value >> 16) : value);
|
||||
/* A derived half keeps its rounding window around the integer. */
|
||||
if (pv->flags & (hi_half ? PGXP_F_DY : PGXP_F_DX)) {
|
||||
const int64_t d = (int64_t)v16 - (int64_t)half * 65536;
|
||||
return d > -65536 && d < 2 * 65536;
|
||||
}
|
||||
/* A saturated projection shadow sits beyond the clamped word; it is not
|
||||
* the value of that half and must not be carried as one. */
|
||||
return (v16 >> 16) == (int32_t)(int16_t)(hi_half ? (value >> 16) : value);
|
||||
return (v16 >> 16) == half;
|
||||
}
|
||||
|
||||
static inline int carry_field(PGXPValue *dst, const PGXPValue *src,
|
||||
@@ -631,10 +905,15 @@ static inline int carry_field(PGXPValue *dst, const PGXPValue *src,
|
||||
if (!half_exact(src, sval, hi_half)) return 0;
|
||||
uint32_t sh = hi_half ? 16u : 0u;
|
||||
if ((((sval ^ result) >> sh) & 0x7FFu) != 0) return 0;
|
||||
int32_t frac = (hi_half ? src->y16 : src->x16) & 0xFFFF;
|
||||
int32_t v = (int32_t)((uint32_t)(int32_t)(int16_t)(result >> sh) << 16) | frac;
|
||||
if (hi_half) { dst->y16 = v; dst->flags |= PGXP_F_VY; }
|
||||
else { dst->x16 = v; dst->flags |= PGXP_F_VX; }
|
||||
/* Keep the precise offset from the operand's integer and rebase it on
|
||||
* the result half's integer (equal field, flag bits may differ). */
|
||||
const int32_t from = (int32_t)(int16_t)(sval >> sh);
|
||||
const int32_t to = (int32_t)(int16_t)(result >> sh);
|
||||
const int32_t v = (int32_t)((int64_t)(hi_half ? src->y16 : src->x16) +
|
||||
((int64_t)to - from) * 65536);
|
||||
const uint16_t d = src->flags & (hi_half ? PGXP_F_DY : PGXP_F_DX);
|
||||
if (hi_half) { dst->y16 = v; dst->flags |= (uint16_t)(PGXP_F_VY | d); }
|
||||
else { dst->x16 = v; dst->flags |= (uint16_t)(PGXP_F_VX | d); }
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -687,11 +966,107 @@ static inline const PGXPValue *gpr_shadow(uint32_t r, PGXPValue *copy) {
|
||||
return copy;
|
||||
}
|
||||
|
||||
/* Scalar tier for one ALU op (cpu-mode): the destination register and the
|
||||
* precise value of the result, when an operand carries a precise value and the
|
||||
* op is exact arithmetic on it (no 32-bit wrap). Operands follow the hook
|
||||
* convention: ADD-type s1 = rs, s2 = rt; immediate s1 = rs, s2 = imm; shifts
|
||||
* s1 = rt, s2 = shamt (or rs for the variable forms); MF/MT HI/LO s1 = the
|
||||
* moved value. Computed before the half tier resets the destination, which
|
||||
* may alias a source. */
|
||||
static int alu_scalar(uint32_t instr, uint32_t result, uint32_t s1, uint32_t s2,
|
||||
int *dst_reg, double *out) {
|
||||
const uint32_t op = f_op(instr);
|
||||
int pa = 0, pb = 0;
|
||||
double a, b;
|
||||
*dst_reg = -1;
|
||||
if (op == 0) {
|
||||
const uint32_t funct = f_funct(instr);
|
||||
const uint32_t rs = f_rs(instr), rt = f_rt(instr), rd = f_rd(instr);
|
||||
switch (funct) {
|
||||
case 0x10: case 0x12: { /* MFHI / MFLO */
|
||||
const PGXPScalar *hl = &s_gpr_s[funct == 0x10 ? PGXP_REG_HI : PGXP_REG_LO];
|
||||
*dst_reg = (int)rd;
|
||||
if (!sc_valid(hl, result)) return 0;
|
||||
*out = hl->v;
|
||||
return 1;
|
||||
}
|
||||
case 0x11: case 0x13: /* MTHI / MTLO */
|
||||
*dst_reg = funct == 0x11 ? PGXP_REG_HI : PGXP_REG_LO;
|
||||
a = gpr_scalar(rs, result, 0, &pa);
|
||||
if (!pa) return 0;
|
||||
*out = a;
|
||||
return 1;
|
||||
case 0x00: case 0x02: case 0x03: /* SLL / SRL / SRA */
|
||||
case 0x04: case 0x06: case 0x07: { /* SLLV / SRLV / SRAV */
|
||||
*dst_reg = (int)rd;
|
||||
const uint32_t sh = s2 & 31u; /* shamt, or rs for the V forms */
|
||||
a = gpr_scalar(rt, s1, 0, &pa);
|
||||
if (!pa) return 0;
|
||||
const int left = (funct & 3u) == 0;
|
||||
const int logical = (funct & 3u) == 2;
|
||||
if (left) {
|
||||
const int64_t exact = (int64_t)(int32_t)s1 * ((int64_t)1 << sh);
|
||||
if (exact != (int64_t)(int32_t)result) return 0; /* wrapped */
|
||||
*out = std::ldexp(a, (int)sh);
|
||||
return 1;
|
||||
}
|
||||
if (logical && (int32_t)s1 < 0) return 0; /* not a division */
|
||||
*out = std::ldexp(a, -(int)sh); /* floor(a/2^sh) is the guest */
|
||||
return 1;
|
||||
}
|
||||
case 0x20: case 0x21: case 0x22: case 0x23: { /* ADD(U) / SUB(U) */
|
||||
*dst_reg = (int)rd;
|
||||
const int sub = funct >= 0x22;
|
||||
a = gpr_scalar(rs, s1, 0, &pa);
|
||||
b = gpr_scalar(rt, s2, 0, &pb);
|
||||
if (!pa && !pb) return 0;
|
||||
const int64_t exact = sub ? (int64_t)(int32_t)s1 - (int64_t)(int32_t)s2
|
||||
: (int64_t)(int32_t)s1 + (int64_t)(int32_t)s2;
|
||||
if (exact != (int64_t)(int32_t)result) return 0;
|
||||
*out = sub ? a - b : a + b;
|
||||
return 1;
|
||||
}
|
||||
case 0x25: /* OR: only the MOVE idiom */
|
||||
*dst_reg = (int)rd;
|
||||
if (rs != 0 && rt != 0) return 0;
|
||||
a = gpr_scalar(rs != 0 ? rs : rt, result, 0, &pa);
|
||||
if (!pa) return 0;
|
||||
*out = a;
|
||||
return 1;
|
||||
default:
|
||||
*dst_reg = (int)rd;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
*dst_reg = (int)f_rt(instr);
|
||||
if (op == 0x08 || op == 0x09) { /* ADDI / ADDIU */
|
||||
a = gpr_scalar(f_rs(instr), s1, 0, &pa);
|
||||
if (!pa) return 0;
|
||||
const int64_t exact = (int64_t)(int32_t)s1 + (int64_t)f_simm(instr);
|
||||
if (exact != (int64_t)(int32_t)result) return 0;
|
||||
*out = a + (double)f_simm(instr);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void alu_halves(uint32_t instr, uint32_t result, uint32_t s1, uint32_t s2);
|
||||
|
||||
extern "C" void psx_pgxp_alu(struct CPUState *cpu, uint32_t instr,
|
||||
uint32_t result, uint32_t s1, uint32_t s2) {
|
||||
(void)cpu;
|
||||
if (!g_pgxp_active) return;
|
||||
int dst = -1;
|
||||
double v = 0.0;
|
||||
const int has = s_cpu_mode && alu_scalar(instr, result, s1, s2, &dst, &v);
|
||||
alu_halves(instr, result, s1, s2);
|
||||
if (dst > 0 && dst < 34) {
|
||||
if (has) sc_set(&s_gpr_s[dst], v, result);
|
||||
else sc_kill(&s_gpr_s[dst]);
|
||||
}
|
||||
}
|
||||
|
||||
static void alu_halves(uint32_t instr, uint32_t result, uint32_t s1, uint32_t s2) {
|
||||
uint32_t op = f_op(instr);
|
||||
PGXPValue ca, cb;
|
||||
|
||||
@@ -732,6 +1107,7 @@ extern "C" void psx_pgxp_alu(struct CPUState *cpu, uint32_t instr,
|
||||
if (comp_tracked(src, s1, 0)) {
|
||||
dst->y16 = src->x16;
|
||||
dst->flags |= PGXP_F_VY;
|
||||
if (src->flags & PGXP_F_DX) dst->flags |= PGXP_F_DY;
|
||||
}
|
||||
dst->x16 = 0;
|
||||
dst->flags |= PGXP_F_VX; /* low half exactly zero */
|
||||
@@ -739,6 +1115,7 @@ extern "C" void psx_pgxp_alu(struct CPUState *cpu, uint32_t instr,
|
||||
if (comp_tracked(src, s1, 1)) {
|
||||
dst->x16 = src->y16;
|
||||
dst->flags |= PGXP_F_VX;
|
||||
if (src->flags & PGXP_F_DY) dst->flags |= PGXP_F_DX;
|
||||
}
|
||||
dst->y16 = ((int32_t)result >> 16) << 16; /* 0 or sign fill */
|
||||
dst->flags |= PGXP_F_VY;
|
||||
@@ -788,7 +1165,7 @@ extern "C" void psx_pgxp_alu(struct CPUState *cpu, uint32_t instr,
|
||||
: comp16(a, s1, 0) + comp16(b, s2, 0);
|
||||
dst->y16 = is_sub ? comp16(a, s1, 1) - comp16(b, s2, 1)
|
||||
: comp16(a, s1, 1) + comp16(b, s2, 1);
|
||||
dst->flags = PGXP_F_VXY;
|
||||
dst->flags = (uint16_t)(PGXP_F_VXY | derived_bits(a, s1) | derived_bits(b, s2));
|
||||
/* A vertex plus / minus an offset is still that vertex. */
|
||||
if (comp_tracked(a, s1, 0) && comp_tracked(a, s1, 1))
|
||||
keep_depth(dst, a, b);
|
||||
@@ -839,7 +1216,7 @@ extern "C" void psx_pgxp_alu(struct CPUState *cpu, uint32_t instr,
|
||||
pv_reset(dst, result);
|
||||
dst->x16 = comp16(a, s1, 0) + (neg ? -dx : dx);
|
||||
dst->y16 = comp16(a, s1, 1); /* no carry crossed: Y untouched */
|
||||
dst->flags = PGXP_F_VXY;
|
||||
dst->flags = (uint16_t)(PGXP_F_VXY | derived_bits(a, s1));
|
||||
if (comp_tracked(a, s1, 0) && comp_tracked(a, s1, 1))
|
||||
keep_depth(dst, a, nullptr);
|
||||
return;
|
||||
@@ -864,18 +1241,147 @@ extern "C" void psx_pgxp_alu(struct CPUState *cpu, uint32_t instr,
|
||||
extern "C" void psx_pgxp_muldiv(struct CPUState *cpu, uint32_t instr,
|
||||
uint32_t hi, uint32_t lo,
|
||||
uint32_t s1, uint32_t s2) {
|
||||
(void)cpu; (void)instr; (void)s1; (void)s2;
|
||||
(void)cpu;
|
||||
if (!g_pgxp_active) return;
|
||||
/* Products/quotients of screen coordinates are not screen coordinates:
|
||||
* record the results as known-but-imprecise so MFHI/MFLO stay honest. */
|
||||
* the half shadows of HI/LO are known-but-imprecise. The scalar tier
|
||||
* (cpu-mode) does carry a product or quotient when an operand is precise:
|
||||
* a weighted sum divided by its weight total IS a screen coordinate (the
|
||||
* perspective-correct midpoint of an edge), and it turns back into a
|
||||
* half when stored. */
|
||||
pv_reset(&s_gpr[PGXP_REG_HI], hi);
|
||||
pv_reset(&s_gpr[PGXP_REG_LO], lo);
|
||||
sc_kill(&s_gpr_s[PGXP_REG_HI]);
|
||||
sc_kill(&s_gpr_s[PGXP_REG_LO]);
|
||||
if (!s_cpu_mode) return;
|
||||
const uint32_t funct = f_funct(instr);
|
||||
const int is_unsigned = (funct & 1u) != 0;
|
||||
if (is_unsigned && ((int32_t)s1 < 0 || (int32_t)s2 < 0))
|
||||
return; /* stay within the signed range */
|
||||
int pa = 0, pb = 0;
|
||||
const double a = gpr_scalar(f_rs(instr), s1, 0, &pa);
|
||||
const double b = gpr_scalar(f_rt(instr), s2, 0, &pb);
|
||||
if (!pa && !pb) return;
|
||||
if (funct == 0x18 || funct == 0x19) { /* MULT / MULTU */
|
||||
const int64_t exact = (int64_t)(int32_t)s1 * (int64_t)(int32_t)s2;
|
||||
if (exact != (int64_t)(int32_t)lo) return; /* HI is not a sign fill */
|
||||
sc_set(&s_gpr_s[PGXP_REG_LO], a * b, lo);
|
||||
return;
|
||||
}
|
||||
if (funct == 0x1A || funct == 0x1B) { /* DIV / DIVU */
|
||||
if (s2 == 0 || b == 0.0) return; /* divide by zero: no quotient */
|
||||
if (s1 == 0x80000000u && s2 == 0xFFFFFFFFu) return; /* overflow */
|
||||
sc_set(&s_gpr_s[PGXP_REG_LO], a / b, lo);
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* GTE producer */
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/* A tracked half as the precise value of the 16-bit integer the GTE used:
|
||||
* the shadow must describe that integer (its half of `value`), and the
|
||||
* precise value must floor to it, or lie in the derived window when the half
|
||||
* came out of rounding guest arithmetic. */
|
||||
static inline int half_precise(const PGXPValue *pv, int hi, int16_t used, double *out) {
|
||||
if (!pv || pv->gen != s_gen) return 0;
|
||||
if (!(pv->flags & (hi ? PGXP_F_VY : PGXP_F_VX))) return 0;
|
||||
const uint16_t half = hi ? (uint16_t)(pv->value >> 16) : (uint16_t)(pv->value & 0xFFFFu);
|
||||
if ((int16_t)half != used) return 0;
|
||||
const int32_t v16 = hi ? pv->y16 : pv->x16;
|
||||
const double v = v16 / 65536.0, d = v - (double)used;
|
||||
if (pv->flags & (hi ? PGXP_F_DY : PGXP_F_DX)) {
|
||||
if (!(d > -1.0 && d < 2.0)) return 0;
|
||||
} else if ((v16 >> 16) != (int32_t)used) {
|
||||
return 0;
|
||||
}
|
||||
*out = v;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* After a GTE command: every command except NCLIP / AVSZ3 / AVSZ4 rewrites
|
||||
* IR0..3 / MAC1..3, so their scalars end there. MVMVA settles its own
|
||||
* results (pgxp_gte_mvmva, before this runs). MAC0 never carries one. */
|
||||
extern "C" void pgxp_gte_op_end(uint32_t func) {
|
||||
sc_kill(&s_gte_s[24]);
|
||||
if (func == 0x06 || func == 0x2D || func == 0x2E || func == 0x12) return;
|
||||
for (int r = 8; r <= 11; r++) sc_kill(&s_gte_s[r]);
|
||||
for (int r = 25; r <= 27; r++) sc_kill(&s_gte_s[r]);
|
||||
}
|
||||
|
||||
static void mvmva_results_reset(const PGXPMvmva *op) {
|
||||
/* The data-register shadows of the results describe the old values. */
|
||||
for (int r = 0; r < 3; r++) {
|
||||
pv_reset(&s_gte[25 + r], (uint32_t)op->mac[r]);
|
||||
pv_reset(&s_gte[9 + r], (uint32_t)op->ir[r]);
|
||||
sc_kill(&s_gte_s[25 + r]);
|
||||
sc_kill(&s_gte_s[9 + r]);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" void pgxp_gte_mvmva(const PGXPMvmva *op) {
|
||||
if (!g_pgxp_active) return;
|
||||
if (!s_cpu_mode || op->mx > 2 || op->tv == 2) { /* reserved / FC bug */
|
||||
mvmva_results_reset(op);
|
||||
return;
|
||||
}
|
||||
static const int kBase[3] = { 0, 8, 16 }; /* RT, LLM, LCM */
|
||||
double m[3][3], v[3], t[3];
|
||||
int any = 0;
|
||||
for (int k = 0; k < 9; k++) {
|
||||
const int r = k / 3, c = k % 3;
|
||||
const PGXPValue *pv = &s_gtc[kBase[op->mx] + (k >> 1)];
|
||||
double p;
|
||||
if (half_precise(pv, k & 1, op->m[r][c], &p)) { m[r][c] = p; any = 1; }
|
||||
else m[r][c] = (double)op->m[r][c];
|
||||
}
|
||||
for (int c = 0; c < 3; c++) {
|
||||
double p;
|
||||
int pr = 0;
|
||||
if (op->vv < 3) {
|
||||
const PGXPValue *pv = &s_gte[2 * op->vv + (c == 2 ? 1 : 0)];
|
||||
pr = half_precise(pv, c == 1, op->v[c], &p);
|
||||
} else {
|
||||
p = reg_scalar(&s_gte_s[9 + c], &s_gte[9 + c],
|
||||
(uint32_t)(int32_t)op->v[c], 0, &pr);
|
||||
}
|
||||
v[c] = pr ? p : (double)op->v[c];
|
||||
any |= pr;
|
||||
}
|
||||
for (int r = 0; r < 3; r++) {
|
||||
t[r] = (double)op->t[r];
|
||||
if (op->tv > 1) continue;
|
||||
const int reg = (op->tv == 0 ? 5 : 13) + r; /* TR / BK words */
|
||||
int pr = 0;
|
||||
const double p = reg_scalar(&s_gtc_s[reg], &s_gtc[reg],
|
||||
(uint32_t)(int32_t)(op->t[r] >> 12), 0, &pr);
|
||||
if (pr) { t[r] = p * 4096.0; any = 1; }
|
||||
}
|
||||
/* Operands are read (the vector may be IR itself); now the results. */
|
||||
mvmva_results_reset(op);
|
||||
if (!any) return;
|
||||
/* FLAG: MAC1/2/3 positive overflow bits 30/29/28, negative 27/26/25;
|
||||
* IR1/2/3 saturation bits 24/23/22 (psx-spx GTE FLAG). */
|
||||
static const uint32_t kMacOverflow[3] = { (1u << 30) | (1u << 27),
|
||||
(1u << 29) | (1u << 26),
|
||||
(1u << 28) | (1u << 25) };
|
||||
static const uint32_t kIrSat[3] = { 1u << 24, 1u << 23, 1u << 22 };
|
||||
for (int r = 0; r < 3; r++) {
|
||||
if (op->flag & kMacOverflow[r]) continue;
|
||||
/* The integers reported must reproduce the guest MAC: proves the
|
||||
* operands handed over are the ones the GTE used. */
|
||||
const int64_t exact = op->t[r] + (int64_t)op->m[r][0] * op->v[0] +
|
||||
(int64_t)op->m[r][1] * op->v[1] +
|
||||
(int64_t)op->m[r][2] * op->v[2];
|
||||
if ((exact >> op->shift) != (int64_t)op->mac[r]) continue;
|
||||
const double p = (t[r] + m[r][0] * v[0] + m[r][1] * v[1] + m[r][2] * v[2]) /
|
||||
(op->shift ? 4096.0 : 1.0);
|
||||
sc_set(&s_gte_s[25 + r], p, (uint32_t)op->mac[r]);
|
||||
if (!(op->flag & kIrSat[r]) && op->ir[r] == op->mac[r])
|
||||
sc_set(&s_gte_s[9 + r], p, (uint32_t)op->ir[r]);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" int pgxp_project_precise(int64_t mac1, int64_t mac2, int64_t mac3,
|
||||
int shift, int32_t ir1, int32_t ir2,
|
||||
uint32_t sz3, uint32_t h,
|
||||
@@ -911,12 +1417,13 @@ extern "C" int pgxp_project_precise(int64_t mac1, int64_t mac2, int64_t mac3,
|
||||
}
|
||||
|
||||
/* Defined below with the GPU consumer. */
|
||||
static inline int pgxp_agrees(int32_t p16, int32_t native, int16_t half);
|
||||
static inline int pgxp_agrees(int32_t p16, int32_t native, int16_t half,
|
||||
int derived);
|
||||
|
||||
extern "C" int pgxp_ppp_accept(int32_t x16, int32_t y16, uint32_t packed) {
|
||||
const int16_t hx = (int16_t)(packed & 0xFFFFu);
|
||||
const int16_t hy = (int16_t)(packed >> 16);
|
||||
if (pgxp_agrees(x16, hx, hx) && pgxp_agrees(y16, hy, hy)) {
|
||||
if (pgxp_agrees(x16, hx, hx, 0) && pgxp_agrees(y16, hy, hy, 0)) {
|
||||
s_stats.ppp_produced++;
|
||||
return 1;
|
||||
}
|
||||
@@ -979,7 +1486,7 @@ extern "C" int pgxp_get_gte_sxy_checked(uint32_t index, uint32_t expect,
|
||||
|
||||
/* Defined below with the GPU consumer. */
|
||||
static inline int pgxp_accept(int32_t px, int32_t py, int32_t int_x,
|
||||
int32_t int_y, uint32_t word);
|
||||
int32_t int_y, uint32_t word, uint16_t flags);
|
||||
|
||||
extern "C" int pgxp_gte_nclip_precise(const uint32_t sxy[3], int64_t *cross) {
|
||||
if (!g_pgxp_active) return 0;
|
||||
@@ -993,7 +1500,7 @@ extern "C" int pgxp_gte_nclip_precise(const uint32_t sxy[3], int64_t *cross) {
|
||||
* value the GTE can produce (its saturation limits are -0x400 and
|
||||
* 0x3FF), so the register half stands in for the native parse. */
|
||||
if (pgxp_accept(pv->x16, pv->y16, (int16_t)(sxy[i] & 0xFFFFu),
|
||||
(int16_t)(sxy[i] >> 16), sxy[i]) != 0)
|
||||
(int16_t)(sxy[i] >> 16), sxy[i], pv->flags) != 0)
|
||||
return 0;
|
||||
x[i] = pv->x16;
|
||||
y[i] = pv->y16;
|
||||
@@ -1040,28 +1547,43 @@ extern "C" void pgxp_gte_reg_written(int reg, uint32_t value) {
|
||||
* when the half is at or beyond the GTE saturation limits: there the guest
|
||||
* integer is a clamp that says nothing about the vertex, or (beyond them) a
|
||||
* CPU-modified word the GPU's 11-bit parse wraps. */
|
||||
static inline int pgxp_agrees(int32_t p16, int32_t native, int16_t half) {
|
||||
static inline int pgxp_agrees(int32_t p16, int32_t native, int16_t half,
|
||||
int derived) {
|
||||
const int64_t d = (int64_t)p16 - (int64_t)native * 65536;
|
||||
/* A derived half (PGXP_F_DX/DY): the guest integer is the rounding its
|
||||
* own arithmetic chose, so the value may sit one pixel either side of
|
||||
* the truncation window - still bounded, still the same vertex. */
|
||||
if (derived) return d > -65536 && d < 2 * 65536;
|
||||
if (!s_preserve_projection || half <= -0x400 || half >= 0x3FF)
|
||||
return (p16 >> 16) == native;
|
||||
const int64_t d = (int64_t)p16 - (int64_t)native * 65536;
|
||||
return d > -(int64_t)PGXP_PPP_AGREE_BELOW * 65536 &&
|
||||
d < (int64_t)PGXP_PPP_AGREE_ABOVE * 65536;
|
||||
}
|
||||
|
||||
/* Tolerance-clamp distance of one axis: how far the precise value moved the
|
||||
* vertex from the guest integer. For a derived half the first pixel above
|
||||
* the integer is the guest arithmetic's own rounding, not movement. */
|
||||
static inline float pgxp_moved(int32_t p16, int32_t native, int derived) {
|
||||
const float d = (float)((int64_t)p16 - (int64_t)native * 65536) * (1.0f / 65536.0f);
|
||||
if (!derived) return std::fabs(d);
|
||||
return d < 0.0f ? -d : (d > 1.0f ? d - 1.0f : 0.0f);
|
||||
}
|
||||
|
||||
/* The consumer-side safeguards on one candidate position: truncation
|
||||
* agreement on both axes, then the tolerance clamp. Returns 0 when accepted,
|
||||
* 1 for a truncation reject, 2 for a tolerance reject. Shared by the GPU
|
||||
* lookup and the precise NCLIP so culling and drawing believe exactly the
|
||||
* same vertices. */
|
||||
* same vertices. `flags` are the shadow's (derived halves). */
|
||||
static inline int pgxp_accept(int32_t px, int32_t py, int32_t int_x,
|
||||
int32_t int_y, uint32_t word) {
|
||||
if (!pgxp_agrees(px, int_x, (int16_t)(word & 0xFFFFu)) ||
|
||||
!pgxp_agrees(py, int_y, (int16_t)(word >> 16)))
|
||||
int32_t int_y, uint32_t word, uint16_t flags) {
|
||||
const int dx_derived = (flags & PGXP_F_DX) != 0;
|
||||
const int dy_derived = (flags & PGXP_F_DY) != 0;
|
||||
if (!pgxp_agrees(px, int_x, (int16_t)(word & 0xFFFFu), dx_derived) ||
|
||||
!pgxp_agrees(py, int_y, (int16_t)(word >> 16), dy_derived))
|
||||
return 1;
|
||||
if (s_tolerance >= 0.0f) {
|
||||
float dx = (float)((int64_t)px - (int64_t)int_x * 65536) * (1.0f / 65536.0f);
|
||||
float dy = (float)((int64_t)py - (int64_t)int_y * 65536) * (1.0f / 65536.0f);
|
||||
if (std::fabs(dx) > s_tolerance || std::fabs(dy) > s_tolerance)
|
||||
if (pgxp_moved(px, int_x, dx_derived) > s_tolerance ||
|
||||
pgxp_moved(py, int_y, dy_derived) > s_tolerance)
|
||||
return 2;
|
||||
}
|
||||
return 0;
|
||||
@@ -1090,6 +1612,7 @@ static int pgxp_get_precise_vertex_impl(uint32_t addr, uint32_t packet_word,
|
||||
|
||||
int32_t px = 0, py = 0;
|
||||
uint16_t pz = 0;
|
||||
uint16_t pflags = 0;
|
||||
int have = 0;
|
||||
|
||||
if (s_enabled && addr != 0xFFFFFFFFu) {
|
||||
@@ -1100,6 +1623,7 @@ static int pgxp_get_precise_vertex_impl(uint32_t addr, uint32_t packet_word,
|
||||
px = pv->x16;
|
||||
py = pv->y16;
|
||||
pz = (pv->flags & PGXP_F_VZ) ? pv->z : 0;
|
||||
pflags = pv->flags;
|
||||
have = PGXP_SRC_DATAFLOW;
|
||||
} else {
|
||||
s_stats.value_mismatch++;
|
||||
@@ -1126,7 +1650,7 @@ static int pgxp_get_precise_vertex_impl(uint32_t addr, uint32_t packet_word,
|
||||
* packet; a precise position whose integer part disagrees (a
|
||||
* wrapped/CPU-modified coordinate) must not be believed. Then the
|
||||
* tolerance clamp. */
|
||||
const int why = pgxp_accept(px, py, int_x, int_y, packet_word);
|
||||
const int why = pgxp_accept(px, py, int_x, int_y, packet_word, pflags);
|
||||
if (why == 1) {
|
||||
s_stats.trunc_reject++;
|
||||
have = 0;
|
||||
|
||||
@@ -426,6 +426,7 @@ static void exec_one(CPUState* cpu) {
|
||||
break;
|
||||
case 0x06: /* CTC2 */
|
||||
gte_write_ctrl(cpu, RD(insn), rt_val);
|
||||
psx_pgxp_cop2(cpu, insn, rt_val, 0);
|
||||
break;
|
||||
default:
|
||||
if (cop_op & 0x10) {
|
||||
|
||||
@@ -162,6 +162,7 @@ int pgxp_probe_precise_vertex(uint32_t addr, uint32_t packet_word,
|
||||
return test_word_precise(addr) ? PGXP_SRC_DATAFLOW : PGXP_SRC_NATIVE;
|
||||
}
|
||||
void pgxp_note_triangle(int precise) { (void)precise; }
|
||||
void pgxp_note_triangle_detail(const PGXPTriRecord *rec) { (void)rec; }
|
||||
void pgxp_note_rect_bypass(int all_precise) {
|
||||
if (all_precise) g_test_rect_bypass++;
|
||||
else g_test_rect_partial++;
|
||||
|
||||
@@ -192,6 +192,7 @@ STUBS_C = r"""
|
||||
#include "gpu.h"
|
||||
#include "gpu_render.h"
|
||||
#include "cpu_state.h"
|
||||
#include "pgxp.h"
|
||||
#include "color_lut.h"
|
||||
#include "ws_aspect_cone_math.h"
|
||||
#include "ws_ui_group.h"
|
||||
@@ -263,6 +264,7 @@ int gte_precision_load_word(uint32_t addr, uint32_t packed,
|
||||
void pgxp_invalidate_all(void) {}
|
||||
void pgxp_invalidate_word(uint32_t addr) { (void)addr; }
|
||||
void pgxp_note_triangle(int precise) { (void)precise; }
|
||||
void pgxp_note_triangle_detail(const PGXPTriRecord *rec) { (void)rec; }
|
||||
void pgxp_note_rect_bypass(int all_precise) { (void)all_precise; }
|
||||
int pgxp_probe_precise_vertex(uint32_t addr, uint32_t packet_word,
|
||||
int32_t int_x, int32_t int_y)
|
||||
|
||||
@@ -780,6 +780,139 @@ int main(void) {
|
||||
pgxp_set_projection_tracking(0);
|
||||
}
|
||||
|
||||
/* --- scalar tier: GTE-as-multiplier edge midpoint (Ape Escape terrain
|
||||
* subdivision, 0x8001DB74): CTC2 two tracked X halves into a light-
|
||||
* matrix row, MVMVA by the vertex depths, MFC2 MAC1, ADDU the rounding
|
||||
* bias, DIV by the depth sum, MFLO, SH into the vertex table. --- */
|
||||
{
|
||||
pgxp_set_cpu_mode(1);
|
||||
pgxp_set_tolerance(1.0f);
|
||||
const uint32_t TBL = 0x1F800280u;
|
||||
/* v0 (100.9, 50.5) z 200, v1 (121.9, 60.25) z 300 */
|
||||
const uint32_t w0 = (50u << 16) | 100u, w1 = (60u << 16) | 121u;
|
||||
pgxp_gte_push_sxy((100 << 16) | 0xE666, (50 << 16) | 0x8000, 200, w0);
|
||||
psx_pgxp_cop2(nullptr, SWC2(14), w0, TBL + 0);
|
||||
pgxp_gte_push_sxy((121 << 16) | 0xE666, (60 << 16) | 0x4000, 300, w1);
|
||||
psx_pgxp_cop2(nullptr, SWC2(14), w1, TBL + 8);
|
||||
|
||||
/* lh t0,0(s1); lh v0,8(s1); andi v1,t0,0xffff; sll a0,v0,16;
|
||||
* or v1,v1,a0; ctc2 v1,$8 */
|
||||
psx_pgxp_load(nullptr, LH(17, 8), TBL + 0, 100u);
|
||||
psx_pgxp_load(nullptr, LH(17, 2), TBL + 8, 121u);
|
||||
psx_pgxp_alu(nullptr, ANDI(8, 3, 0xFFFF), 100u, 100u, 0xFFFFu);
|
||||
psx_pgxp_alu(nullptr, SLL(2, 4, 16), 121u << 16, 121u, 16u);
|
||||
psx_pgxp_alu(nullptr, OR(3, 4, 3), (121u << 16) | 100u, 100u, 121u << 16);
|
||||
psx_pgxp_cop2(nullptr, enc_cop2(0x06, 3, 8), (121u << 16) | 100u, 0);
|
||||
/* mtc2 t9(200),IR1; mtc2 t8(300),IR2; mtc2 zero,IR3 (exact) */
|
||||
psx_pgxp_alu(nullptr, ADDIU(0, 25, 200), 200u, 0u, 200u);
|
||||
psx_pgxp_alu(nullptr, ADDIU(0, 24, 300), 300u, 0u, 300u);
|
||||
psx_pgxp_cop2(nullptr, MTC2(25, 9), 200u, 0);
|
||||
psx_pgxp_cop2(nullptr, MTC2(24, 10), 300u, 0);
|
||||
psx_pgxp_cop2(nullptr, MTC2(0, 11), 0u, 0);
|
||||
|
||||
/* MVMVA sf=0 mx=LLM v=IR cv=none: MAC1 = 100*200 + 121*300 = 56300 */
|
||||
PGXPMvmva op;
|
||||
std::memset(&op, 0, sizeof op);
|
||||
op.mx = 1; op.vv = 3; op.tv = 3; op.shift = 0;
|
||||
op.m[0][0] = 100; op.m[0][1] = 121;
|
||||
op.v[0] = 200; op.v[1] = 300; op.v[2] = 0;
|
||||
op.mac[0] = 56300; op.ir[0] = 0x7FFF;
|
||||
op.flag = 1u << 24; /* IR1 saturated */
|
||||
pgxp_gte_mvmva(&op);
|
||||
pgxp_gte_op_end(0x12);
|
||||
|
||||
/* mfc2 a1,MAC1; addu a1,a1,a3(0); addu v0,t9,t8; div a1,v0; mflo a1 */
|
||||
psx_pgxp_cop2(nullptr, MFC2(5, 25), 56300u, 0);
|
||||
psx_pgxp_alu(nullptr, ADDIU(0, 7, 0), 0u, 0u, 0u);
|
||||
psx_pgxp_alu(nullptr, ADDU(5, 7, 5), 56300u, 56300u, 0u);
|
||||
psx_pgxp_alu(nullptr, ADDU(25, 24, 2), 500u, 200u, 300u);
|
||||
psx_pgxp_muldiv(nullptr, enc_r(5, 2, 0, 0, 0x1A), 56300u % 500u,
|
||||
56300u / 500u, 56300u, 500u);
|
||||
psx_pgxp_alu(nullptr, enc_r(0, 0, 5, 0, 0x12), 112u, 112u, 0u);
|
||||
/* sh a1,0x48(s1) */
|
||||
psx_pgxp_store(nullptr, SH(17, 5), TBL + 0x48, 112u);
|
||||
|
||||
/* precise midpoint: (100.9*200 + 121.9*300) / 500 = 113.5 */
|
||||
uint32_t live = 0, value = 0, flags = 0; int32_t sx = 0, sy = 0; uint16_t sz = 0;
|
||||
int lv = 0;
|
||||
CHECK(pgxp_debug_shadow(0, TBL + 0x48, &lv, &value, &flags, &sx, &sy, &sz));
|
||||
(void)live;
|
||||
CHECK(lv == 1 && (flags & 1u) != 0);
|
||||
CHECK(std::fabs(sx / 65536.0 - 113.5) < 1e-3);
|
||||
CHECK((flags & 0x10u) != 0); /* derived: floor 113 != 112 */
|
||||
|
||||
/* y half from a non-derived value; then the whole word reaches a
|
||||
* packet and the GPU believes it inside the derived window. */
|
||||
psx_pgxp_load(nullptr, LH(17, 6), TBL + 2, 50u);
|
||||
psx_pgxp_store(nullptr, SH(17, 6), TBL + 0x4A, 50u);
|
||||
const uint32_t mid = (50u << 16) | 112u;
|
||||
psx_pgxp_cop2(nullptr, LWC2(12), mid, TBL + 0x48);
|
||||
psx_pgxp_cop2(nullptr, SWC2(12), mid, ADDR_A);
|
||||
int32_t x, y; uint16_t z;
|
||||
CHECK(lookup(ADDR_A, mid, 112, 50, &x, &y, &z) == PGXP_SRC_DATAFLOW);
|
||||
CHECK(std::fabs(x / 65536.0 - 113.5) < 1e-3 && y == ((50 << 16) | 0x8000));
|
||||
|
||||
/* tolerance clamps movement beyond the guest rounding pixel */
|
||||
pgxp_set_tolerance(0.25f);
|
||||
CHECK(lookup(ADDR_A, mid, 112, 50, &x, &y, &z) == PGXP_SRC_NATIVE);
|
||||
pgxp_set_tolerance(1.0f);
|
||||
|
||||
/* a scalar does not survive a write that does not produce one: the
|
||||
* register reloaded from memory stores imprecise */
|
||||
psx_pgxp_load(nullptr, LW(17, 5), TBL + 0x100, 112u);
|
||||
psx_pgxp_store(nullptr, SH(17, 5), TBL + 0x58, 112u);
|
||||
CHECK(pgxp_debug_shadow(0, TBL + 0x58, &lv, &value, &flags, &sx, &sy, &sz));
|
||||
CHECK((flags & 1u) == 0);
|
||||
|
||||
/* the lwl/lwr + swl/swr word-copy idiom on an aligned address moves
|
||||
* whole words, so it carries the vertex (Ape Escape's terrain grid
|
||||
* copies its projected corners this way, 0x80044B00) */
|
||||
{
|
||||
const uint32_t LWL = enc_i(0x22, 17, 11, 0), LWR = enc_i(0x26, 17, 11, 0);
|
||||
const uint32_t SWL = enc_i(0x2A, 17, 11, 0), SWR = enc_i(0x2E, 17, 11, 0);
|
||||
psx_pgxp_load(nullptr, LWL, TBL + 3, w0);
|
||||
psx_pgxp_load(nullptr, LWR, TBL + 0, w0);
|
||||
psx_pgxp_store(nullptr, SWL, TBL + 0x103, w0);
|
||||
psx_pgxp_store(nullptr, SWR, TBL + 0x100, w0);
|
||||
int32_t cx, cy; uint16_t cz;
|
||||
CHECK(lookup(TBL + 0x100, w0, 100, 50, &cx, &cy, &cz) == PGXP_SRC_DATAFLOW);
|
||||
CHECK(cx == ((100 << 16) | 0xE666) && cy == ((50 << 16) | 0x8000));
|
||||
/* a genuinely unaligned piece still drops the word */
|
||||
psx_pgxp_store(nullptr, enc_i(0x2A, 17, 11, 0), TBL + 0x101, w0);
|
||||
CHECK(lookup(TBL + 0x100, w0, 100, 50, &cx, &cy, &cz) != PGXP_SRC_DATAFLOW);
|
||||
}
|
||||
|
||||
/* a MAC1 that overflowed (FLAG bit 27, negative) carries nothing;
|
||||
* MAC2's flags (bits 29/26) leave MAC1 alone */
|
||||
{
|
||||
PGXPMvmva o2 = op;
|
||||
o2.flag = 1u << 27;
|
||||
pgxp_gte_mvmva(&o2);
|
||||
psx_pgxp_cop2(nullptr, MFC2(5, 25), 56300u, 0);
|
||||
psx_pgxp_muldiv(nullptr, enc_r(5, 2, 0, 0, 0x1A), 300u, 112u, 56300u, 500u);
|
||||
psx_pgxp_alu(nullptr, enc_r(0, 0, 5, 0, 0x12), 112u, 112u, 0u);
|
||||
psx_pgxp_store(nullptr, SH(17, 5), TBL + 0x68, 112u);
|
||||
CHECK(pgxp_debug_shadow(0, TBL + 0x68, &lv, &value, &flags, &sx, &sy, &sz));
|
||||
CHECK((flags & 1u) == 0);
|
||||
o2.flag = (1u << 29) | (1u << 26);
|
||||
pgxp_gte_mvmva(&o2);
|
||||
psx_pgxp_cop2(nullptr, MFC2(5, 25), 56300u, 0);
|
||||
psx_pgxp_muldiv(nullptr, enc_r(5, 2, 0, 0, 0x1A), 300u, 112u, 56300u, 500u);
|
||||
psx_pgxp_alu(nullptr, enc_r(0, 0, 5, 0, 0x12), 112u, 112u, 0u);
|
||||
psx_pgxp_store(nullptr, SH(17, 5), TBL + 0x70, 112u);
|
||||
CHECK(pgxp_debug_shadow(0, TBL + 0x70, &lv, &value, &flags, &sx, &sy, &sz));
|
||||
CHECK((flags & 1u) != 0);
|
||||
}
|
||||
|
||||
/* the scalar tier is cpu-mode only */
|
||||
pgxp_set_cpu_mode(0);
|
||||
psx_pgxp_cop2(nullptr, MFC2(5, 25), 56300u, 0);
|
||||
psx_pgxp_store(nullptr, SH(17, 5), TBL + 0x60, 112u);
|
||||
CHECK(pgxp_debug_shadow(0, TBL + 0x60, &lv, &value, &flags, &sx, &sy, &sz));
|
||||
CHECK((flags & 1u) == 0);
|
||||
pgxp_set_tolerance(-1.0f);
|
||||
}
|
||||
|
||||
/* --- stats sanity: dataflow hits were counted --- */
|
||||
{
|
||||
PGXPStats st;
|
||||
|
||||
@@ -35,6 +35,8 @@ set(_files
|
||||
recompiler/src/full_function_emitter.cpp
|
||||
recompiler/src/full_function_emitter.h
|
||||
recompiler/src/strict_translator.cpp
|
||||
recompiler/src/pgxp_hook_emitter.cpp
|
||||
recompiler/src/pgxp_hook_emitter.h
|
||||
recompiler/src/main_bios.cpp
|
||||
recompiler/src/function_discovery.cpp
|
||||
recompiler/src/bios_address_model.cpp
|
||||
|
||||
Reference in New Issue
Block a user