First clean run — both sides sampled, no errors, no fallback:
psx-runtime 72, 74, 76, 78 steps of 2
oracle 12, 16, 20, 24 steps of 4
Both sweep smoothly; neither jumps. The ranges are disjoint (72-78 against
12-24), which is the two sides sitting at different points in the fade, not a
fault.
But the report could not say whether psx-runtime's series was per-frame or
free-running, because native_sampling was only set on the fallback path. Those
support completely different claims: steps of 2 across CONSECUTIVE frames is a
smooth fade, while steps of 2 across samples ninety frames apart is nothing at
all. The field is now set before either path runs and overwritten by whichever
one did.
And the eight-sample floor was applied to per-frame data, where it does not
belong. Consecutive frames measure the increment DIRECTLY rather than sampling
a distribution — four adjacent frames stepping by 2 IS the step. The floor
still applies to free-running samples, which say nothing however many there
are. Applying it to per-frame discarded a measurement that was already
conclusive.
A per-frame result now also states what the increment means: at a median step
of 2 the scale crosses its whole range in about 64 frames, roughly a second,
which does not produce hard bands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous commit claimed two fixes and shipped one. Its patch script hit a
failed assertion on the third replacement and exited BEFORE writing the file,
so the two edits it had already made in memory were discarded — the same shape
as the probe fill that was written in memory and never saved. The commit
message described work that was not in the tree, which is worse than not doing
it, and only the tests I wrote alongside caught it.
Both edits are now in:
* every per-frame claim checks native_sampling, so "these steps are the
animation increment" cannot be printed over data the fallback produced
* a fallback run gets its own granularity label saying outright that its
steps are aliased and compare nothing
The reported document had native_sampling "free-running-fallback" AND
granularity "native-measured-per-frame" at once. That combination is now
unreachable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The report contradicted itself: native_sampling "free-running-fallback" AND
granularity "native-measured-per-frame" in the same document — the data is
aliased, and its steps are the animation increment. Both cannot be true. The
guard existed and sat on a different branch than the one that fired, so the
claim I said last commit could not be made was made on the very next run.
Free-running samples land about ninety frames apart, so their differences are
aliasing. Presenting them as an increment invites precisely the wrong
conclusion — that our fade jumps by 44 — when nothing measured that. Every
per-frame claim now checks the sampling method, a test asserts that for each
one by reading the source, and a fallback run gets its own label saying the
steps mean nothing.
The other half is why the fallback ran at all. Per-frame now reports "a block
leader fired but no register sample came with it", which was accurate and
narrow: it insisted on the SINGLE nearest leader and threw away frames where a
leader fired but the recorded sample belonged to a different one. It now takes
a sample from whichever fired leader has one, nearest first — true of the frame
rather than of one address.
Data so far, with the caveat attached: psx-runtime 26..128, oracle 76..86, both
aliased. Neither says how either one moves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Repeated runs capturing nothing from psx-runtime while the effect was being
replayed, and the reported reason was wrong twice over.
The scene check ran AFTER the finally block resumed psx-runtime, so it reported
what was on screen seconds later rather than during sampling — and for a brief
effect that reliably says "not on screen" when it was there the whole time. It
now checks while still parked.
And per-frame mode capturing nothing is not the same as the code being
unreachable. The free-running probe demonstrably works here: it returned 4800
hits and real register values ($s4=0x800E2634, $sp=0x1F800234, scratchpad, as
the oracle also reports). So per-frame returning empty is a property of
stepping, not of the block. It now retries free-running automatically, which
produces data with a stated caveat instead of a fourth empty run — and records
which method produced the numbers, so aliased steps are never presented as an
animation increment.
The failure message also separates two causes that need different fixes: no
candidate fired at all (the block starts further back, or this overlay is not
running through COMPILED blocks — the probe only fires at compiled block
leaders), versus a leader fired but no register sample came with it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"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>
Reported from a live session: DuckStation being paused repeatedly through the
animation while psx-runtime was not. Two things behind it.
Per-frame sampling never swept. It flattened every search window and took the
first sixteen candidates, which is one window — the nearest 0x3C bytes — so a
block starting earlier than that was never found. The failure then read "the
block was not reached on any of 12 stepped frames", which is true and points
the reader at the effect not drawing rather than at a search that barely
looked. Each stepped frame now arms the NEXT window, so the sweep advances with
the animation instead of being spent before it starts.
And on failure it ASKED whether the effect was drawing instead of checking.
class_on_screen answers that directly, and the two causes need opposite
responses: nothing on screen means get the game to the scene, a covered range
means the block starts further back. It now says which, and how far it looked.
The larger point is the one the observation exposes. Stepping psx-runtime frame
by frame while the oracle free-runs measures two different things, and for
"does OUR fade sweep or jump" the oracle is not needed at all — consecutive
frames give the real increment with no alignment, no shared phase and no second
emulator. Requiring both sides forced the operator to hold two emulators inside
the same effect at once, which is the hardest part of this whole exercise. A
per-frame series of five or more samples now stands on its own, and says
plainly whether our fade is smooth or coarse.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two bugs, both reported from a live session: psx-runtime visibly parked while
the oracle ran on untouched, and the oracle then said it had no samples.
The sides were sampled SEQUENTIALLY. psx-runtime was sampled to completion
first and the oracle asked afterwards, by which time the effect had finished
over there and the breakpoint could never fire. Replaying the animation does
not help, because the two halves of the run are minutes apart. They now run on
threads and are watched during the same pass.
And per-frame sampling could only ever capture one value. It paused
psx-runtime, then called probe_registers — which arms the probe and SLEEPS
waiting for it to fire. On a paused emulator no code executes, so the block
leader is never reached and every sample after the first comes back empty. That
is where the "1 sample" run came from, and the verdict drawn from it.
The order has to be arm -> STEP -> read, so the block executes between arming
and reading, and sample_per_frame now does that itself rather than delegating
to a helper whose whole method is waiting for a running emulator.
Both are pinned by tests that read the source order directly, because both
failures produced plausible output rather than an error: one sample looks like
a sample, and an empty oracle looks like an oracle that was not there.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
psx-runtime finally produced a real series — 7 samples, 48/94/128, apparent
steps of 40-46 — against the oracle's 122/124/126/128 stepping by 2. Twenty
times coarser, and it would explain hard edges instead of a gradient.
It is not evidence. Both series are sampled about 1.5s apart, roughly ninety
frames, so the difference between consecutive readings is aliasing rather than
the animation increment. A fade stepping by 2 every frame, sampled every ninety
frames, produces arbitrary-looking jumps too — which is exactly what 48/94/128
could be.
--per-frame steps ONE frame between reads, so the deltas ARE the increment.
That needs one emulator rather than two, which is the property that has made
every successful measurement in this investigation work: no frame alignment, no
matching buffer half, no shared phase. If psx-runtime's per-frame step is 2-4
the fade is smooth and the fault is elsewhere; if it is 40 the fade is coarse
and that is the picture.
The oracle's series stays non-frame-adjacent, so its apparent step is an upper
bound only, and the report says so rather than comparing the two as if they
were measured the same way.
Also fixes the Studio passing a bare "--gap" as the off case for a boolean
flag, which would have failed in argparse before the tool ran.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The tool reported "native-not-animating" from ONE psx-runtime sample, with the
note "never moved from 128 across 1 samples". That is not a claim anyone can
make, and it agreed with a hypothesis already disproved twice here — the most
dangerous shape a bug can take, because it confirms what the reader suspects.
The cause was the previous commit. Caching the block leader to make sampling
affordable REPLACED the sweep instead of preceding it, so the moment the cached
address stopped firing — an overlay reloading elsewhere, or that block simply
not being reached on a pass — the run ended. A 24-sample request became one
sample, and then a conclusion was drawn from it.
Both halves fixed. The cached leader is now tried first and the full sweep
still follows it, and a failed sample drops the cache and continues rather than
ending the series. Any "constant" verdict requires at least five samples from
that side; below it the result is too-few-samples, stated as such. Variation
needs no floor, because two different values prove movement however few samples
there are.
The oracle's own series is worth recording: 112, 116, 120, 124, 128 — five
distinct values in even steps of 4. Whatever psx-runtime does, the oracle
sweeps its fade smoothly.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
First complete both-sides comparison, and the verdict is both-animate: the fade
runs on psx-runtime too. That is the second time the pinned-scale idea has been
disproved by measurement, and it stays disproved.
The shapes differ though, and min/max cannot express it:
oracle 122, 124, 126, 128 steps of 2, a 6-wide band
psx-runtime 28, 128 nothing between, a 100-wide jump
A fade that sweeps smoothly and one that jumps between extremes produce very
different pictures from identical geometry — which is the reported symptom. So
describe() now reports step size alongside range, and both-animate compares
granularity rather than stopping at "both vary".
It refuses to compare granularity below eight samples per side, because two
distinct values out of three is ALSO exactly what sparse sampling of a smooth
ramp looks like. The run above had three and four; that is suggestive and
nothing more.
Which is why sampling was the real problem: every psx-runtime sample re-ran the
whole block-leader sweep, so a run capped out at three. The leader is found once
and reused, and the default sample count is up to 24.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
psx-runtime's $s6 read 66, 72 and 128 across three samples. It varies. The fade
animates on our side too, so "the scale is pinned at the neutral value" — which
the last commit put forward on the strength of three separate single-sample
runs all happening to catch 128 — is disproved.
That is worth stating rather than quietly moving on. Three single readings
taken for other purposes looked like a pattern; the first tool that sampled
repeatedly killed it immediately. The lesson is the same one this investigation
keeps teaching: single samples of an animating quantity are not evidence about
it, and the fix is always to measure variation rather than values.
Second problem, in the tool rather than the theory: the oracle produced no
samples and the report said only "oracle": null, verdict "incomplete". That
states there is no comparison without saying whether the emulator was
unreachable, never reached the PC, or refused the breakpoint — three causes
needing three different responses, and the reason is known at the point of
failure and nowhere else. Both samplers now return their reason alongside their
values.
It also threw away the half that DID answer, which in this run was the half
that disproved the hypothesis. A one-sided result is not a comparison, but it
is not nothing either, and the Studio now says so explicitly instead of showing
an empty panel.
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>