"the effect is not on screen on psx-runtime, so this code never ran. Currently
drawing: PolyFT4 x584, PolyF4+semi x184, PolyG3+semi x176, PolyFT3 x151" — the
diagnostic added last commit, doing its job on the first run. The effect simply
was not up when the tool looked.
That is the real problem, and it is not a diagnostic problem. Every tool here
needs the effect RUNNING, and requiring it to be on screen at the instant a
button is clicked means racing a transient animation. Losing that race produces
a different-looking failure in each tool — no writes recorded, no candidate
fired, no table found, the block was not reached — four symptoms, one cause,
none of them naming it. Running lockstep beforehand appeared to help only
because it takes long enough for the effect to come round again.
So the tools wait. Press the button first, then trigger the effect; each polls
the display list until its class appears, up to two minutes, and each side is
waited for INDEPENDENTLY in its own thread. That last part matters: it removes
the need to hold two emulators inside the same effect simultaneously, which was
never realistic by hand.
Wired into scale_trace, packet_writers, range_writers and colour_inputs — every
tool that needs the effect. Timing out reports what WAS drawing, because "wrong
scene" and "tool is broken" look identical otherwise. A transient DebugError
mid-poll does not abort the wait: the emulator can be mid-pause, and one failed
poll is not a reason to stop waiting for something that has not happened yet.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The phase test came back tested: false — psx-runtime's scale never left 128
during the sampling window — and the tool then reported "region-differs"
anyway, stating as established the exact thing it had failed to check. Fixed:
an untested phase question now downgrades the verdict instead of being ignored.
But a scale that refuses to move is a finding, not a failed setup step. Across
every run the oracle's has come back different — 52, 64, 68, 76, 96, 126, 128 —
while psx-runtime's has read 128 each time colour_inputs sampled it. And 128 is
the NEUTRAL scale: x*128>>7 leaves x unchanged, so at that value this routine
applies no fade at all. Bright unfaded polygons are exactly what the code would
then produce, which is the reported symptom.
scale_trace samples $s6 on both emulators and compares VARIATION rather than
values. That is the point: it needs no frame alignment, no matching buffer
half, and no shared animation phase. Those three have each produced a confident
wrong answer in this investigation, and a register that sweeps on one side
while sitting still on the other is a difference none of them can manufacture.
Four outcomes, kept apart because they point four different ways: ours pinned
and theirs sweeping (the suspected fault, called out specially when the pinned
value is the neutral one), both sweeping (the fade runs, so the difference is
in the values), both constant (most likely nothing is animating right now, and
that says nothing), and theirs pinned while ours sweeps (the opposite of the
expectation, not to be reported as if it confirmed it).
This is a HYPOTHESIS with supporting observations, not a conclusion. One
probe_regs run did read 72 on psx-runtime, so the register is not always 128
and a single reading either way settles nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The characterised difference is not conclusive, and the shape says why. At
0x000E4E00 the two hold the SAME palette entries offset by exactly four bytes;
elsewhere psx-runtime holds f8 88 08 where the oracle holds f8 f8 b0; and in
places psx-runtime holds 16-bit negatives (-100, -98, -19 — coordinates) where
the oracle holds colours. That is consistent with a real layout divergence AND
with the two being at different points in the animation, which they are:
scale 128 against 72.
The churn check does not settle it. It re-reads after 0.4s, so it catches a
region being written continuously but not one rewritten only when the animation
STEPS — which is exactly how a region can report static: true and still be
phase-driven. Every "region-differs" result so far rests on that gap.
So: sample the region on psx-runtime at two different values of $s6. One
emulator, no alignment, no oracle. If its own copy moves with the scale, the
region is animation-driven and comparing it across two emulators at different
scales proves nothing — including every earlier result. If it does not move,
the difference is real.
A scale that never changes within the window reports "untested" rather than
"not phase dependent". Those are different, and treating the second as the
first would license the comparison on no evidence at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The run now says region_static: true with region_churn_bytes: 0 and 2747 of
23088 bytes differing. By the tool's own criterion that is a real divergence —
the region is not mid-write — and the report still only carried a number.
A count is compatible with several completely different causes: psx-runtime has
not written the data at all, psx-runtime is writing where the oracle does not,
or both hold values that disagree. Those point in different directions, and
"2747" cannot be read to tell them apart.
The report now carries the differing runs with both sides' bytes side by side,
coalesced so that a 4-byte entry differing in three of its bytes reads as one
finding rather than three, plus a plain statement of the shape:
* zeros on one side -> that side has not written this
* data on both -> a genuine disagreement, and whether it falls on
4-byte boundaries (whole entries) or not (stray bytes)
Also worth recording against the earlier reading: with the effect OFF the same
region is byte-identical and completely static across eight samples. So this
difference appears with the effect, which is what makes it interesting rather
than ambient.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The region comparison reported 2747 of 23088 bytes differing and called it
phase-independent. Reading the same region directly, minutes later, gave ZERO
differing bytes — eight samples over ten seconds, byte-identical between the two
emulators, and psx-runtime's copy unchanged throughout.
The difference between the two readings is that the effect was running for one
and not the other. Both emulators are on the same scene right now (PolyFT4 x576,
PolyF4+semi x184, PolyFT3 x1 on each) with the effect off.
So "compare the whole region instead" only escapes phase if the region is not
itself being written, and this one is — while the effect runs. That is worth
knowing rather than papering over, because it splits the finding cleanly:
* with the effect OFF the region is byte-identical, so the base colour data
agrees between the emulators
* with it ON the two differ, so the divergence is in what the effect WRITES
there, not in the table it starts from
The tool now re-reads the region after a short delay and reports the churn. A
difference in a region that is changing gets its own verdict saying the two
sides may simply be caught at different moments, and pointing at the write
trace as the next instrument. A difference in a region that is NOT changing is
reported as real, which is the only case where that claim is earned.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The capture works now — $sp reads 0x1F800234, scratchpad, matching the oracle —
and the run finally compared through both emulators' own pointers. It reported
"source-differs", and that is still not trustworthy.
$s4 MOVES. Across two samples psx-runtime's went 0x800E2634 -> 0x800E4634 while
$s6 went 72 -> 128; the oracle's moved the same way. The pointer tracks the
animation, so a window at each side's own $s4 compares different ENTRIES of the
same array whenever the two are at different moments — which they always are.
The 0x1A30 gap between the two source addresses is not the 0x8000 double buffer
and not a divergence; it is elapsed time.
This is the third time a phase difference has been reported as a data
difference (frame numbers, buffer halves, now array offsets), so the fix is the
same shape as before: measure something phase cannot move. When the two scales
disagree, the whole enclosing region is compared instead. If the arrays match,
the data is fine wherever each pointer sits; if they do not, that is real and
independent of where either side happened to be.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every register came back 0x00000000 — including $sp, which cannot be zero in
running code. The struct had gpr[32] and the JSON emitted it; nothing ever
wrote to it. The patch that added the fill did the replacement in memory and
never wrote the file, and the next edit re-read from disk without it. Two
thirds of a change, compiling and running and reporting 32 zeros.
Zero then flowed all the way downstream without anything objecting: 0 minus the
load offset masks to 0x1FFFFFF4, which looks like an address, so colour_inputs
read "psx-runtime's table" from unmapped memory and reported a difference
against the oracle. The same document carried "differing_bytes": 0 and
"source_identical": false — the read had come back short, so zip() compared
nothing while the buffers still differed.
Three guards, because one bad value crossed four steps unchallenged:
* probe_registers refuses a capture where every register is zero and says so,
rather than returning zeros as data
* plausible_pointer() rejects anything that is not a RAM address before it is
used as one, including the masked underflow that got through
* the comparison refuses unequal read lengths instead of reporting a
difference between a buffer and nothing
Scratchpad is deliberately not a plausible table pointer: $sp lives there, and
accepting it would compare the wrong region while looking entirely reasonable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Clicking a colour-only row aborted the Studio:
type_error.302: type must be string, but is null
nlohmann's value<T>() THROWS when a key exists but holds null, rather than
returning the default. colour_inputs wrote "block_leader": null for a probe
that had not fired, and reading it took the process down. A viewer must never
die on the content of a file it is shown, so this is fixed on both sides:
* the report omits absent keys instead of writing nulls — "missing" and
"present but null" should not be different things for it to express
* every string and integer read in the newer loaders goes through null-safe
accessors, so a malformed or unexpected document degrades to defaults
Underneath it was a real failure the crash was hiding:
pc_probe_dump: malformed reply (Expecting value: line 1 column 80)
That handler builds its JSON with repeated send_line() calls — header, one line
per slot, one per sample — so the first newline lands in the MIDDLE of the
document. DebugConn stopped at the first newline and got a truncated object,
and the error named the server rather than the reader. It now stops at a
newline only when what has arrived is a complete JSON document, counted by
braces with quotes and escapes tracked (a parse attempt per chunk would
dominate the read on a megabyte of hex).
So the probe never actually reported $s4; the whole run fell back to the
content search without that being visible as a failure.
frames_load_test now loads packet_writers/gte_check/colour_inputs/lockstep from
a real directory too. That mode would have caught this before it reached a
click: the crashing file loads cleanly under it now.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The last three runs were inconclusive for the same reason: the tool had the
oracle's $s4 and no way to get psx-runtime's, so it read the oracle's ADDRESS
on psx-runtime. That address holds an index list there, not colours — a fact
about memory layout, not about the colour data, and the tool kept reporting it
as though it were about the colour data.
Now both sides are read through their own pointers. psx-runtime's comes from
the block-leader probe: $s4 is callee-saved and set outside the loop, so its
value at block entry is its value at the store. The comparison is finally
between the table each routine actually reads.
The content search stays as a fallback for when the probe cannot fire, but it
is now visibly a fallback -- the report records `compared_by`, and the Studio
says outright that a content search cannot know which table psx-runtime's code
reads. Two answers of different quality should not look alike.
A ±0x8000 address delta is called out as the double buffer rather than as a
difference, which is what it was the whole time.
probe_registers() is shared between the two tools rather than duplicated. Two
copies of "find the enclosing block" would drift, and the failure would be
silent: both would return real register values, from different blocks.
Also adds a bare "Read psx-runtime registers" button, since psx-runtime has no
PC breakpoint and this is otherwise the only way to see what its code held.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The tool reported "table-absent-on-native" for every colour writer, and that
claim was too strong. Checking psx-runtime's RAM by hand:
* the oracle's full 32-byte table is indeed absent as a contiguous run
* but its first 4 bytes appear SIX times, on a regular 0x18 stride
* and the region at 0x0E4600 is BYTE-IDENTICAL between the two emulators —
same palette (f85050, f8f8b0, 080808, f8f8f8), same order
So the data exists. What differs is the arrangement, and that points somewhere
completely different from "this was never built": upstream data generation
versus a layout or record-size difference.
At the oracle's own pointer address (0x0E4BF8) psx-runtime holds 16-bit values
where 0x38 recurs every five halfwords followed by four ascending numbers --
an index list, not colours. The two emulators keep different structures at that
address, which is itself the finding.
The search is now graduated (32/16/12/8/4 bytes), reports the longest prefix
present, how many times, and on what stride if it is regular. "Absent" is
reserved for genuinely finding nothing.
Both verdicts now also state what the tool CANNOT determine: which table
psx-runtime's own code reads. That needs its $s4, and psx-runtime has no PC
breakpoint. Leaving that unsaid is what let the earlier verdict read as more
conclusive than it was.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The tool ran clean and reported "source-differs". The report was wrong, and the
data it printed is what shows why.
The oracle's half is solid and self-confirming: its table at 0x000E4BF8 holds
(248,80,80) and (8,8,8), and scaled by the recorded factor of 126 those give
(244,78,78) and (7,7,7) -- the colours in its own display list. That is the
colour table, verified by reproducing its own output rather than by assumption.
psx-runtime's bytes at that same address are not a colour table at all: 24 of
24 odd bytes are zero, so it is an array of 16-bit values. Two different
structures, not one structure with different contents.
The assumption underneath was that the table sits at the same address on both,
and it does not. Measured in the same report: the oracle's packet pointer was
0x8010D9A0 while psx-runtime's packets were at 0x00115078..0x00115974. The two
allocators place the same structures at different addresses, so reading the
oracle's pointer on psx-runtime reads unrelated memory -- and the difference
reported was entirely the tool's own doing.
The table is now LOCATED rather than assumed: search psx-runtime's RAM for the
oracle's actual bytes. Three outcomes instead of two, and the new one is the
most informative:
* found, identical -> input fine; scale or arithmetic is at fault
* found, differing -> the table itself diverged
* not found anywhere -> psx-runtime never built this table at all, so the
divergence is upstream of the colour routine
A structural check refuses to read a 16-bit array as a colour table, and a
needle shorter than 12 bytes is refused because a short match in 2 MB is
meaningless and would return an arbitrary address.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"pc_hit_last: server closed without replying", and every retry after it worse.
Two bugs, neither of which failed in a way that pointed at its cause.
A DuckStation breakpoint PAUSES the emulator when it fires. A paused oracle
pumps its debug socket only from a Qt idle timer -- 100 ms with a gamepad
attached, 1000 ms without -- so a headless oracle is a ~1 Hz server the moment
the breakpoint hits. Polling it every 100 ms stacks up connections it cannot
accept, and that surfaces as "server closed without replying": a
network-shaped error with a pacing-shaped cause. Polling is now 1.5 s, the
wait has headroom, and one dropped reply around the hit is survived rather
than treated as failure, because the hit parks the emulator mid-exchange.
Worse, the tool cleared the breakpoint and exited WITHOUT resuming. So the
first run left the oracle parked at 1 Hz permanently and every attempt after
it failed for a different reason than the first -- which is a miserable thing
to debug from the outside. It now resumes in the finally block, and resumes
BEFORE arming too, so a previously stranded oracle recovers on its own instead
of needing a restart.
ORACLE_PAUSED_POLL_S and oracle_resume() live in psx_gpu_frame because this is
the third time this behaviour has cost time: it also explains the 2 MB read
that could not finish while parked, and the earlier decision never to pause the
oracle for a snapshot.
Studio gets a Resume button next to the oracle's status for the same reason --
the symptom (everything times out) does not resemble the cause (it is parked).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
packet_writers found the code; this says which of its two inputs is wrong.
Every vertex colour is source_rgb * $s6 >> 7, so a wrong colour is either a
wrong SOURCE or a wrong SCALE, and those point in opposite directions: one
means look upstream at whatever fills the table, the other means look at this
routine. Guessing between them is how an investigation goes sideways.
The obvious approach — break at the PC on both emulators and diff registers —
is not available. The oracle has pc_break and captures all 32 GPRs on hit;
psx-runtime has no equivalent, and adding one means emitting per-instruction
hooks from the recompiler, which is a different project. Its pc_probe fires at
basic-block leaders with a fixed register set.
The asymmetry turns out not to matter, because the register we need from the
oracle is a POINTER. Once it reports $s4, the source table's ADDRESS is known,
and the same address can be read on both sides with read_ram, which both
support. So the comparison that settles it is of memory, not registers, and it
needs the breakpoint on one side only.
Reports the scale it saw as well, since a neutral scale means the code takes
the branch that skips modulation entirely — a different path with different
consequences.
The oracle failing to reach the PC is treated as a normal outcome and
explained, not as a crash: these are overlay addresses, so they only exist
while the right overlay is resident and the oracle has to be at the same point
in the game.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>