feat(tom): OP shadow-resolve hit/miss counters — make hi-res Stage 2's silent failure diagnosable (#338) (#362)

* feat(video): hi-res Stage 2 — fractional-walk source supersampling (#338)

Implements Stage 2 of docs/hires-upscaling-design.md: blits whose A1
SOURCE walk consumes a fraction (XADDINC with fractional FINC, or the
XADD0 + fractional-FSTEP column scaler) onto a 16bpp CRY destination
now sample the source at 2x density for the shadow surface, producing
real sub-pixel detail instead of Stage 1's box replication.  The stock
walk, stock writes and every control decision (transparency, colour
key, Z, clip, LFU select) are untouched and made once at 1x; the
shadow only carries content (design sections 4/5).

- shadowfb.{c,h}: ShadowHiresStoreCryBlock() stores an N*N block with
  per-subpixel {value16, frac16} under the stock value's tag.
- blitter.c fast engine (blitter_generic, DSTA2 branch): qualifying
  uninhibited pixels derive sub-samples by re-reading the source at
  pos + inc/2 (and pos + step/2 for the FSTEP shape) and applying the
  same data path (LFU / ADDDSEL / SRCSHADE incl. its frac16); GOURD and
  PATDSEL are source-independent and keep replication.
- blitter.c accurate engine (BlitterMidsummer2 pixel-mode dwrite):
  same shapes; sub-samples read through ADDRGEN and apply
  SRCSHADE-lane-0 + LFU replicas (a direct ADDARRAY call would clobber
  its persistent carry state).  The sread states now also cache
  a1_frac at read time, and the collapsed-copy fast path routes DSTA2
  UPDA1F blits through the state machine only while hi-res is active.
- tom.c: the Nx CRY renderer consumes per-subpixel entries under
  true-color too (ShadowFBCryRGB per entry), completing the frac16
  composition Stage 1 deferred.
- test/tools/hires_shot.c: evidence tool — scripted-input screenshots
  plus % of non-uniform 2x2 blocks per frame (the box-replication
  property failing on beneficiaries is the feature).

All new work is gated behind shadowHiresActive: option off is
structurally bit-identical to develop.

Co-Authored-By: Claude Fable 5 <[email protected]>

* fix(video): hi-res epoch trusted window 2 -> 16 frames (#338 Stage 2)

The design's now/now-1 trusted window assumed the 3D view is redrawn
every presented frame.  Doom -- the Stage 2 anchor -- runs its engine
at ~10-15Hz double-buffered, so displayed blocks are routinely 3-8
presented frames old and the 2-frame window rejected every one of
them: measured 0.00% supersampled output on E1M1 gameplay, vs 8.68%
with the window widened.  16 frames (~267ms NTSC) covers slow engines
while still bounding the R3 stale-structure class (a false positive
needs a value-coincident dead tag AND 16 frames without a rewrite,
and self-heals at the next real write).  Window comparison measured:
2 -> 0.00%, 16 -> 8.68%, infinity -> 8.68% (16 saturates the benefit).

Co-Authored-By: Claude Fable 5 <[email protected]>

* chore: gitignore hires_shot tool binary

Co-Authored-By: Claude Fable 5 <[email protected]>

* fix(blitter): exact ADDDSEL semantics in the hi-res sub-sample mirror

Copilot review: the Stage 2 ADDDSEL sub-sample derivation diverged from
the stock DSTA2 data path in two ways -- an unsigned clamp where stock
does a SIGNED saturated byte add (source sign-extended) when TOPBEN is
clear, and a saturate where stock MASKS to 12 bits when TOPNEN is clear.
Mirrored exactly. Also documents why PIXEL_OFFSET_16(sub) is correct:
the macro token-pastes into sub_x/sub_y/sub_width/sub_pitch, reusing the
stock addressing formula over the explicit 16.16 position rather than
transcribing it.

make TEST_EXPORTS=1 test exit 0; Checkered Flag 2x box-replication gate
still exact (398/398, 0 bad blocks).

Co-Authored-By: Claude Fable 5 <[email protected]>

* docs: Stage 2 on-screen verification -- what actually reaches the display

The Stage 0 census counted qualifying blit SHAPES (fractional A1 source
walk onto a 16bpp CRY destination).  That is not the same thing as
supersampled pixels arriving on screen, and the gap is large enough that
the A1 table was being quoted as a beneficiary list when it is not one.

Measured every census candidate with test/tools/hires_shot on a clean
836baf6 tree (build-identity guard on, both blitter engines), each scene
screenshot-verified in real gameplay rather than inferred from a frame
count:

  verified: Alien vs Predator 30.63%, Skyhammer 10-22%, Doom 8.68%,
            Missile Command 3D 5.03-5.88%
  zero:     Hover Strike, I-War, Towers II
  unknown:  Iron Soldier 2 (CD) -- gameplay not reachable headlessly

Two corrections and one new rule:

- An earlier hand measurement of "AvP: 0.0000% on both engines" is WRONG.
  AvP is the LARGEST beneficiary measured, above Doom.
- The two zeros are different mechanisms.  Hover Strike produces the
  detail (9.06M of 10.38M accepted blocks carry real sub-samples) and
  then loses it downstream: 89% of its Stage 2 stores land at $100000,
  a region the display never reads -- it renders off-screen and
  plain-copies to the display, and the copy replicates.  I-War never
  produces it: 81% of its qualifying walks magnify (< 1 source texel per
  destination pixel), so the pos+inc/2 sub-sample lands in the same texel.
- Stage 2 recovers information only where the source walk MINIFIES.  A
  fractional walk is necessary but not sufficient.  That single sentence
  is what stops the optimistic list being re-derived.

Also records that phrase-mode is NOT the blocker for any measured title
(Doom's accurate-engine run: 5,071,224 pixel-mode 16bpp destination
writes, zero phrase-mode), and that the harness runs the Fast blitter
unless told otherwise while the shipped default is Accurate.

Names one cheaply-fixable follow-on with evidence -- shadow-aware copy
propagation, which would recover Hover Strike -- but implements nothing.

Instrumentation was a throwaway patch (reverted); only this doc lands.

Co-Authored-By: Claude Fable 5 <[email protected]>

* docs: close gaps in the Stage 2 verification section

Review follow-ups on the §9 verification section, no conclusion changes:

- CRZ Demo is the 9th row of §6's A1 table and had no §9 verdict, which
  is exactly the cross-reference hole §9 exists to close.  Added an
  explicit "not measured" row plus the GOURD-based prediction, marked as
  a prediction rather than a result.
- Doom's minification share is 44%, not 43% (inc_hist buckets 1..7 =
  2,184,437 of 4,936,831 accepted walks).
- Towers II's accurate-engine run sampled 12000/14800; the Frames column
  claimed three frames for both engines.  Split per column.
- Pre-empt the "did you measure a frozen buffer?" objection on AvP: the
  16-frame epoch trusted window means a framebuffer that stopped being
  rewritten decays to 0.0000, so a sustained 30.6% over 800 frames is
  itself proof of continuous re-rendering; the video_stall lines are the
  documented benign 256-pixel-hash class.
- State that the earlier AvP zero could not be reproduced AND its cause
  was not established, rather than implying it was diagnosed.

Co-Authored-By: Claude Fable 5 <[email protected]>

* feat(tom): permanent OP shadow-resolve hit/miss counters (#338 Stage 2)

Hi-res Stage 2 has one failure mode that produces no symptom at all: the
blitter passes every gate and stores every supersampled block correctly,
and the OP resolve then rejects 100% of them at the value+epoch check in
shadow_hires_block() -- 0.0000% supersampled output, no log line, nothing
else wrong.  Production and delivery are separate failure points and only
delivery fails silently.  It has bitten two titles (Doom, fixed by
404cb11; Alien vs Predator, same constant), and both took a full
investigation of the innocent half of the pipeline to diagnose, each time
with throwaway instrumentation that was then deleted.

Make it a permanent counter instead.  shadow_hires_block() now bumps
exactly one of four counters on every exit, so hits + the three miss
buckets equals resolves attempted, and each bucket names a different
subsystem:

  hits       -- block delivered
  missEpoch  -- value matched, rejected for age only (the silent killer)
  missValue  -- entry exists, RAM no longer holds its tag value
  missNoPage -- no shadow page at all (production never got here)

crash_detect.c's existing verbose heartbeat prints them every 600 frames,
but only while shadowHiresActive, so 1x runs stay quiet:

  [CRASH-DETECT] hires_resolve frame=2400 N=2x hits=79588699 misses=68948450
    (epoch=24224010 value=44388198 nopage=336242) rate=53.6% window_rate=98.2%

window_rate covers the last 600 frames; the cumulative rate is diluted by
menus and boot, which on AvP is the first ~1500 frames.  Counters are
64-bit and printed through double (%.0f): C89 has no %llu and MSVC's
unsigned long is 32-bit, which a long session would overflow.

Zero cost at 1x: the counting sites sit inside the resolve path, which
ShadowHiresLineFromRAM only reaches when shadowHiresActive.  Counting is
unconditional there rather than verbose-gated -- a uint64 increment is
noise against the N*N work already being done, and a second global test
per pixel would cost more than it saves.  make benchmark at 1x, best of
three each: 289.9/290.2/288.4 fps with the counters vs 286.3/286.4/284.9
without, i.e. no regression (the difference is host noise).

Counters are added to the test ABI (link-test.T / exports-test.list) so
harnesses can harness_dlsym() and assert on the rate.  The production ABI
is unchanged -- verified with nm -gU on both link modes.

HIRES_EPOCH_WINDOW is deliberately untouched: 16 is measured correct.

Demonstrated on AvP 2x with test/fixtures/avp_reach_gameplay.press,
crash_detect=verbose: window_rate 98.2% / 97.2% in the gameplay windows
(matching the 97.8% measured in #359) with 25.06% non-uniform 2x2 blocks
on screen.  With the epoch window temporarily forced to 2 in the working
tree, the same run reads window_rate=0.0% with the entire miss population
in epoch= and 0.0000% non-uniform blocks -- the counter would have
answered both investigations in one line.

The heartbeat also guards its own window-delta subtraction: the counters
and the previous-heartbeat snapshots reset independently, and no live path
zeroes one without the other today, but the subtraction is unsigned and a
future reordering would print a 19-digit window_rate rather than a number.

* docs: check the OP resolve hit rate before the blit shapes (#338 Stage 2)

The knowledge, not just the counter -- so it survives on builds that
predate the heartbeat line and for anyone reading the docs rather than a
log.

Stated explicitly in all three places a "title X shows no supersampling"
report would land:

- hires-upscaling-design.md section 8, Stage 2: a triage rule with the
  heartbeat line format and a bucket-by-bucket table mapping epoch= /
  value= / nopage= / all-zero to the subsystem each implicates.
- hires-stage0-census.md section 9: a fifth condition (e) alongside the
  four filters that section already lists between "qualifying blit shape"
  and "supersampled pixels on screen" -- the OP resolve has to accept the
  blocks, and unlike (a)-(d) it is not a property of the title at all.
- avp-renderer-analysis.md section 8 item 4, which asked for exactly this
  and can now say it is implemented.

The load-bearing sentence in each: production and delivery are separate
failure points, and the epoch window is the one that fails silently.  Two
titles have hit it -- Doom (404cb11) and Alien vs Predator (#359, OP
resolve hits 42,827,520 -> 0 with blitter-side production bit-identical).

Also records the two traps in reading the number: use window_rate rather
than the cumulative rate (menus and boot dilute it), and do not widen
HIRES_EPOCH_WINDOW reflexively -- 16 is measured to saturate the benefit
and widening trades against the R3 stale-structure class.

* fix(crash-detect): never print a cumulative figure under a window label

Review on #362: `window_rate` is meant to cover the last heartbeat window,
but the `hb_prev_hires_*` baselines are only ever taken *by a heartbeat*,
and heartbeats only run under CRASH_DETECT_VERBOSE.  Switch verbose on
mid-run and the snapshots are still zero while the counters are already in
the millions -- so the first `window_rate` was silently the cumulative
figure wearing a window label.  That is exactly the misreading these
counters exist to prevent: AvP reads 53.6% cumulative against 98.2% live.

Track whether the baseline was ever seeded.  The first heartbeat of a run
seeds it and prints `window_rate=n/a (first window)`; every later line
carries a true window.  The same `n/a` now covers the counters-went-
backwards case, which previously zeroed the baselines and produced the
identical cumulative-from-boot lie rather than a 19-digit number.

Also tighten the test ABI: `shadowHires*` exported `shadowHiresActive`,
`shadowHiresN`, `shadowHiresLineSub` and `shadowHiresLineTag` alongside
the four resolve counters.  Export the four counters explicitly in both
`link-test.T` and `exports-test.list`.

Verified:
- `make TEST_EXPORTS=1 test` exit 0 -- 104 passed, 0 failed, 0 skipped,
  "Skipped checks: none -- every optional check ran".
- `nm -gU` on TEST_EXPORTS=1: exactly four `_shadowHiresResolve*`, no
  other shadow symbol.  On the production ABI: zero shadow symbols.
- AvP 2x + verbose, committed fixture, 3000 frames: every counter value
  reproduces the PR's healthy run byte for byte, with frame=600's
  window_rate now `n/a (first window)`; frames 2400/3000 still read
  98.2% and 97.2%.  1x stays quiet -- ordinary heartbeats, zero
  hires_resolve lines.
- `HIRES_EPOCH_WINDOW` untouched (16).
- c89-lint clean on crash_detect.c, shadowfb.c, blitter.c.

---------

Co-authored-by: Claude Fable 5 <[email protected]>
This commit is contained in:
Joe MattielloandClaude Fable 5 authored and GitHub committed 2026-08-09 03:00:30 -04:00
1 parent 8648a5b573
commit 83d854b0e1
8 files changed
+300 -5

No files matched your search

+20 -4
View File
@@ -509,10 +509,26 @@ silent and total: full production, zero delivery, 0.0000% on screen and no
log line *naming* it (a `video_stall` line may well be present, but it is
unrelated and fires on healthy runs too — see §9). Any future "title X shows
no supersampling" report should check the
OP resolve hit rate **before** investigating blit shapes. A cheap permanent
form of that check — a resolve hit/miss counter behind the existing
`crash_detect` verbose mode, or simply a note in the Stage 2 documentation —
would have turned this investigation into a one-line answer.
OP resolve hit rate **before** investigating blit shapes.
**Implemented.** That check is no longer throwaway instrumentation:
`src/tom/shadowfb.c` now carries permanent OP resolve counters
(`shadowHiresResolveHits` and the `MissEpoch` / `MissValue` / `MissNoPage`
buckets), reported by the existing `crash_detect` verbose heartbeat every 600
frames while hi-res is active:
```
[CRASH-DETECT] hires_resolve frame=2400 N=2x hits=79588699 misses=68948450 \
(epoch=24224010 value=44388198 nopage=336242) rate=53.6% window_rate=98.2%
```
Read `window_rate` (last 600 frames) rather than the cumulative `rate`, which
AvP's ~1500 frames of menus dilute. The same fixture on the same build with
the epoch window forced back to 2 reads `window_rate=0.0%` with the whole miss
population in `epoch=` — i.e. the counter would have answered this
investigation in one line. Bucket-by-bucket triage lives in
`docs/hires-upscaling-design.md` §8, Stage 2; the counters are exported in the
test ABI so harnesses can `dlsym` and assert on them.
---
+30
View File
@@ -400,6 +400,36 @@ And one condition the census could not see at all:
value. A fractional walk is necessary but not sufficient — only
minification carries information the 1x sample threw away.
And one that is not a property of the title at all:
- **(e) the OP resolve has to accept the stored blocks.** Production (the
blitter storing supersampled blocks) and delivery (the OP resolving them
into the Nx line buffer) are **separate failure points, and only delivery
fails silently.** Every block can be stored bit-identically and every one
of them rejected at the value+epoch check in `shadow_hires_block()`
(`src/tom/shadowfb.c`) — 0.0000% on screen, no log line, nothing else
wrong. The **epoch** half is the silent one: a title whose engine takes
more than `HIRES_EPOCH_WINDOW` presented frames per rendered view has
*every* block rejected for age, not merely some.
**So when a title measures `0.0000` here, check the OP resolve hit rate
BEFORE investigating blit shapes, the Stage 2 gate, or the census
predicate.** That check is a permanent counter now, not throwaway
instrumentation: run with `virtualjaguar_crash_detect=verbose` and the
watchdog heartbeat prints `hires_resolve … (epoch=… value=… nopage=…)
rate=… window_rate=…` every 600 frames while hi-res is active. Read
`window_rate` (last 600 frames), not the cumulative `rate`, which menus and
boot dilute; the first line of a run reads `window_rate=n/a (first window)`
because it is seeding that baseline, not reporting one. Healthy AvP gameplay
reads 97–98%, and an `epoch`-dominated
bucket with `window_rate` near zero is the signature. This failure mode has
bitten **two** titles — Doom (fixed by `404cb11`, window 2 → 16) and Alien
vs Predator (same constant; A/B in `docs/avp-renderer-analysis.md` §6 shows
OP resolve hits 42,827,520 → 0 with blitter-side production bit-identical).
Bucket-by-bucket triage is in `docs/hires-upscaling-design.md` §8, Stage 2.
Do not widen `HIRES_EPOCH_WINDOW` reflexively: 16 is measured to saturate
the benefit.
### 9.2 The metric
`hires_shot` reports, for each dumped 2x frame, the **percentage of 2×2
+58
View File
@@ -574,6 +574,64 @@ different entry format (index + palette identity rather than `{value16,
frac16}`) and a supersampled CLUT resolve in the OP, which §6.5 leaves on the
stock path. That is its own stage and its own design.
#### Triage rule — check the OP resolve hit rate BEFORE the blit shapes
**Production and delivery are separate failure points, and only delivery fails
silently.** The blitter can pass every Stage 2 gate and store every
supersampled block bit-identically, and the OP resolve can then reject 100% of
them at the value+epoch check in `shadow_hires_block()`
(`src/tom/shadowfb.c`) — putting **0.0000% supersampled pixels on screen with
no log line and no other symptom**. Nothing about the blit shapes, the gate
predicate, or the stored blocks is wrong in that state; the detail is created
and then thrown away.
The epoch half of that check is the one that fails silently and totally. A
title whose 3D engine takes more than `HIRES_EPOCH_WINDOW` presented frames
per rendered view (slow or double-buffered engines) has *every* block rejected
for age, not merely some. This has now bitten two titles, both diagnosed only
after a full investigation of the innocent half of the pipeline:
- **Doom** (~10–15 Hz double-buffered engine) — fixed by `404cb11`, window
2 → 16.
- **Alien vs Predator** (its 3D buffer is always older than 2 presented
frames) — same root cause, same constant; see
`docs/avp-renderer-analysis.md` §6 for the A/B, which shows OP resolve hits
going 42,827,520 → 0 while the blitter-side production stays bit-identical.
**So: before investigating blit shapes, the Stage 2 gate, or the census
predicate, read the resolve hit rate.** It is a permanent counter now, not
throwaway instrumentation — run with `virtualjaguar_crash_detect=verbose` and
the heartbeat prints, every 600 frames while hi-res is active:
```
[CRASH-DETECT] hires_resolve frame=2400 N=2x hits=79588699 misses=68948450 \
(epoch=24224010 value=44388198 nopage=336242) rate=53.6% window_rate=98.2%
```
Read `window_rate` (the last 600 frames), not `rate` — the cumulative figure is
diluted by menus and boot. Healthy AvP gameplay reads 97–98%.
The **first** heartbeat of a run prints `window_rate=n/a (first window)` and
seeds the baseline instead of reporting one. Only a heartbeat takes that
baseline and only verbose runs heartbeats, so if verbose is switched on
mid-run there is nothing to subtract — a number there would be the cumulative
figure wearing a window label, which is the misreading this line exists to
prevent. The same `n/a` appears if the counters are ever seen to go backwards.
Every subsequent line carries a true window. The bucket names
each point at a different subsystem, so the line localizes the fault as well as
detecting it:
| Bucket | Meaning | Where to look |
|---|---|---|
| `epoch=` dominant, `window_rate` ≈ 0 | Entries match by value and are rejected for age only | **The silent killer.** `HIRES_EPOCH_WINDOW` vs the title's render cadence. Do not raise it reflexively: 16 is measured to saturate the benefit (`404cb11`), and widening it trades against the R3 stale-structure class (§5.4). |
| `value=` dominant | Entries exist but RAM no longer holds the value they were derived from | Normal coherence miss; also every never-written word inside an allocated page. Suspect a write path that bypasses the store site. |
| `nopage=` dominant | No shadow page for the address at all | Production never got here: the blitter store site, the Stage 2 gate, or the allocation cap (`SHADOWFB_HIRES_CAP_BYTES`, §3.3). |
| all zero while `shadowHiresActive` | No resolves attempted | The OP is not reaching the 16bpp resolve site for this title at all (§6.5 leaves several object paths on the stock path). |
The counters (`shadowHiresResolveHits` / `…MissValue` / `…MissEpoch` /
`…MissNoPage`) are also in the test ABI, so a harness can `harness_dlsym()` them
and assert on the rate directly rather than parsing the log.
### Stage 3 — OP scaled-object supersampling (SCBITOBJ)
Sub-sample the source bitmap through a **local** copy of the scaling
+4
View File
@@ -65,3 +65,7 @@ _NVMBios*
_NVM_*
_JGD*
_jgd*
_shadowHiresResolveHits
_shadowHiresResolveMissValue
_shadowHiresResolveMissEpoch
_shadowHiresResolveMissNoPage
+4
View File
@@ -68,5 +68,9 @@
NVM_*;
JGD*;
jgd*;
shadowHiresResolveHits;
shadowHiresResolveMissValue;
shadowHiresResolveMissEpoch;
shadowHiresResolveMissNoPage;
local: *;
};
+117
View File
@@ -5,6 +5,7 @@
#include "../jerry/dsp.h" /* DSPIsRunning() returns bool -- match the canonical decl */
#include "../tom/gpu.h" /* GPUIsRunning() */
#include "../cd/cdrom.h" /* CDROMDiagGetSeekWedgeState(), CDTraceDump() */
#include "../tom/shadowfb.h" /* shadowHiresActive + resolve counters */
#include "settings.h" /* bootConfig.isCDGame */
#include <boolean.h> /* project shim; bool / true / false */
#include <stdint.h>
@@ -93,6 +94,24 @@ static unsigned cd_seek_wedge_frames;
static unsigned next_heartbeat_frame;
/* Previous heartbeat's hi-res resolve counters, so the line can report a
* rate for the last window as well as the cumulative one. Cumulative alone
* is misleading on a title whose menus run for a thousand frames before any
* supersampled content exists.
*
* `hb_hires_seeded` says whether those snapshots were actually taken. They
* are only ever taken by a heartbeat that ran, and heartbeats only run under
* CRASH_DETECT_VERBOSE -- so if verbose is switched on mid-run the snapshots
* are still zero while the counters are not, and a delta against them would
* be cumulative-from-boot wearing a window label. That is precisely the
* misreading these counters exist to prevent (AvP: 53.6% cumulative vs 98.2%
* live), so the first heartbeat seeds instead of reporting. */
static uint64_t hb_prev_hires_hits;
static uint64_t hb_prev_hires_miss_value;
static uint64_t hb_prev_hires_miss_epoch;
static uint64_t hb_prev_hires_miss_nopage;
static int hb_hires_seeded;
/* Last frame at which each signature fired -- prevents log spam. */
static unsigned last_log_gpu_escape;
static unsigned last_log_dsp_escape;
@@ -139,6 +158,98 @@ static uint32_t fb_hash(const uint32_t *fb, unsigned w, unsigned h)
return h32;
}
/* Verbose-heartbeat extension: the OP shadow-resolve hit rate.
*
* Hi-res has one failure mode that produces no other symptom -- the blitter
* stores every supersampled block correctly and the OP resolve then discards
* all of them, putting 0.0000% supersampled pixels on screen with nothing in
* the log. Production and delivery are separate failure points; only
* delivery fails silently. A low `epoch=` share is the signature, and it has
* been the answer twice (Doom, Alien vs Predator).
*
* Emitted only while hi-res is actually active, so 1x runs stay quiet, and
* only under CRASH_DETECT_VERBOSE. Counters are 64-bit and printed through
* double (%.0f): C89 has no %llu, and `unsigned long` is 32-bit under MSVC,
* which a long session would overflow. */
/* Everything up to and including the cumulative rate. The window figure is
* appended by one of two callers below -- a real percentage, or the literal
* "n/a", never a number that means something else. */
#define HIRES_RESOLVE_FMT \
"[CRASH-DETECT] hires_resolve frame=%u N=%dx hits=%.0f misses=%.0f " \
"(epoch=%.0f value=%.0f nopage=%.0f) rate=%.1f%%"
#define HIRES_RESOLVE_ARGS \
frame_no, shadowHiresN, (double)hits, (double)misses, \
(double)m_epoch, (double)m_value, (double)m_nopage, rate
static void hires_resolve_heartbeat(void)
{
uint64_t hits, m_value, m_epoch, m_nopage, misses, total;
uint64_t d_hits, d_misses, d_total;
double rate, window_rate;
int have_window;
if (!shadowHiresActive)
return;
hits = shadowHiresResolveHits;
m_value = shadowHiresResolveMissValue;
m_epoch = shadowHiresResolveMissEpoch;
m_nopage = shadowHiresResolveMissNoPage;
misses = m_value + m_epoch + m_nopage;
total = hits + misses;
rate = total ? (100.0 * (double)hits / (double)total) : 0.0;
/* Two ways the baseline can be untrustworthy:
*
* 1. It was never taken. Only a heartbeat seeds it and only verbose runs
* heartbeats, so switching verbose on mid-run leaves the snapshots at
* zero while the counters are already in the millions -- the delta
* would then be the cumulative figure under a window label, which is
* the exact misreading this line exists to prevent.
* 2. The counters went backwards. They and these snapshots reset
* independently (ShadowHiresShutdown vs CrashDetectReset). No live
* path zeroes one without the other today -- every ShadowHiresShutdown
* call site is in load/unload/deinit and the load path resets the
* watchdog afterwards -- but the subtraction below is unsigned, so a
* future reordering would otherwise print a 19-digit window_rate.
*
* Either way: seed now, say "n/a", and report a true window next time. A
* diagnostic that can lie loudly is worse than one that cannot; one that
* lies quietly is worse than both. */
have_window = hb_hires_seeded
&& hits >= hb_prev_hires_hits
&& m_value >= hb_prev_hires_miss_value
&& m_epoch >= hb_prev_hires_miss_epoch
&& m_nopage >= hb_prev_hires_miss_nopage;
if (have_window)
{
d_hits = hits - hb_prev_hires_hits;
d_misses = (m_value - hb_prev_hires_miss_value)
+ (m_epoch - hb_prev_hires_miss_epoch)
+ (m_nopage - hb_prev_hires_miss_nopage);
d_total = d_hits + d_misses;
window_rate = d_total ? (100.0 * (double)d_hits / (double)d_total) : 0.0;
LOG_INF(HIRES_RESOLVE_FMT " window_rate=%.1f%%\n",
HIRES_RESOLVE_ARGS, window_rate);
}
else
{
LOG_INF(HIRES_RESOLVE_FMT " window_rate=n/a (first window)\n",
HIRES_RESOLVE_ARGS);
}
hb_prev_hires_hits = hits;
hb_prev_hires_miss_value = m_value;
hb_prev_hires_miss_epoch = m_epoch;
hb_prev_hires_miss_nopage = m_nopage;
hb_hires_seeded = 1;
}
static int may_log(unsigned *last_frame)
{
if (frame_no - *last_frame < LOG_REPEAT_FRAMES && *last_frame != 0)
@@ -167,6 +278,11 @@ void CrashDetectReset(void)
last_cd_fifo_drains = 0;
cd_seek_wedge_frames = 0;
next_heartbeat_frame = HEARTBEAT_FRAMES;
hb_prev_hires_hits = 0;
hb_prev_hires_miss_value = 0;
hb_prev_hires_miss_epoch = 0;
hb_prev_hires_miss_nopage = 0;
hb_hires_seeded = 0;
last_log_gpu_escape = 0;
last_log_dsp_escape = 0;
last_log_gpu_wedge = 0;
@@ -312,6 +428,7 @@ void CrashDetectFrameTick(const uint32_t *fb, unsigned w, unsigned h)
"dsp_pc=$%08X dsp_run=%d fb_hash=$%08X\n",
frame_no, cur_gpu_pc, gpu_running,
cur_dsp_pc, dsp_running, cur_fb_hash);
hires_resolve_heartbeat();
next_heartbeat_frame = frame_no + HEARTBEAT_FRAMES;
}
+46 -1
View File
@@ -151,6 +151,23 @@ void ShadowFBShutdown(void)
int shadowHiresActive = 0;
int shadowHiresN = 1;
/* Resolve diagnostics (shadowfb.h). Production (the blitter storing
* supersampled blocks) and delivery (the OP resolving them) are separate
* failure points, and only delivery fails silently: a title can pass every
* Stage 2 gate, store every block bit-identically, and still put 0.0000%
* supersampled pixels on screen because the value+epoch check below rejects
* all of them. That has happened twice (Doom, Alien vs Predator -- both
* epoch-expired). These counters make it a heartbeat line instead of an
* investigation; see crash_detect.c and docs/hires-upscaling-design.md
* section 8. Counting is unconditional inside the resolve path, which the
* caller only reaches when shadowHiresActive: a uint64 increment is noise
* against the N*N work already being done, and a second global test per
* pixel to make it verbose-only would cost more than it saves. */
uint64_t shadowHiresResolveHits = 0;
uint64_t shadowHiresResolveMissValue = 0;
uint64_t shadowHiresResolveMissEpoch = 0;
uint64_t shadowHiresResolveMissNoPage = 0;
shadowfb_sub *shadowHiresLineSub = NULL;
uint32_t shadowHiresLineTag[SHADOWFB_LINE_PIXELS];
@@ -263,25 +280,49 @@ void ShadowHiresStoreCryBlock(uint32_t addr, uint16_t stock16,
| (hiresEpoch << HIRES_TAG_EPOCH_SHIFT);
}
/* Value+epoch-checked block lookup. Returns the N*N block or NULL. */
/* Value+epoch-checked block lookup. Returns the N*N block or NULL.
* Every exit bumps exactly one resolve counter, so hits + the three miss
* buckets always equals the number of resolves attempted, and the bucket
* names each point at a different subsystem:
* nopage -- production never reached this page (nothing stored, or the
* allocation cap stopped it): look at the blitter/store side.
* value -- an entry exists but RAM no longer holds the value it was
* derived from (normal coherence miss; also every word inside
* an allocated page that was never itself written).
* epoch -- the entry matches by value and was rejected only for age.
* This is the silent-total-failure bucket: a slow or
* double-buffered engine can push 100% of its blocks here. */
static const shadowfb_sub *shadow_hires_block(uint32_t addr, uint16_t current16)
{
uint32_t idx, page, word, tag, ep;
addr &= 0xFFFFFF;
if (addr >= 0x800000)
{
shadowHiresResolveMissNoPage++;
return NULL;
}
idx = (addr & 0x1FFFFE) >> 1;
page = idx >> 12;
if (!hiresPageTag[page])
{
shadowHiresResolveMissNoPage++;
return NULL;
}
word = idx & 0xFFF;
tag = hiresPageTag[page][word];
if ((tag & 0x1FFFF) != ((uint32_t)current16 | SHADOWFB_TAG_VALID))
{
shadowHiresResolveMissValue++;
return NULL;
}
ep = (tag >> HIRES_TAG_EPOCH_SHIFT) & 0xFF;
if (((hiresEpoch - ep) & 0xFF) >= HIRES_EPOCH_WINDOW)
{
shadowHiresResolveMissEpoch++;
return NULL;
}
shadowHiresResolveHits++;
return hiresPageSub[page]
+ word * (uint32_t)shadowHiresN * (uint32_t)shadowHiresN;
}
@@ -365,6 +406,10 @@ void ShadowHiresShutdown(void)
hiresAllocStopped = 0;
shadowHiresActive = 0;
shadowHiresN = 1;
shadowHiresResolveHits = 0;
shadowHiresResolveMissValue = 0;
shadowHiresResolveMissEpoch = 0;
shadowHiresResolveMissNoPage = 0;
}
void ShadowHiresSetN(int n)
+21
View File
@@ -121,6 +121,27 @@ typedef struct
/* Nonzero when the option requested N>1 AND allocation succeeded. */
extern int shadowHiresActive;
/* OP resolve hit/miss counters -- permanent diagnostics for the one hi-res
* failure mode that produces no other symptom. The blitter can store every
* supersampled block correctly while the OP resolve rejects 100% of them at
* the value+epoch check, yielding 0.0000% supersampled output with nothing
* in the log: production and delivery are separate failure points and only
* delivery fails silently (Doom and Alien vs Predator have both hit it, both
* epoch-expired). Monotonic since the last ShadowHiresShutdown; only ever
* bumped while shadowHiresActive, so a 1x run leaves them all zero.
*
* hits + missValue + missEpoch + missNoPage == resolves attempted:
* missNoPage -- no shadow page for the address (production never got here)
* missValue -- entry exists, RAM value no longer matches its tag
* missEpoch -- value matched, entry rejected for age only (the silent one)
*
* Reported by crash_detect.c's verbose heartbeat; also exported in the test
* ABI (link-test.T / exports-test.list) so harnesses can dlsym and assert. */
extern uint64_t shadowHiresResolveHits;
extern uint64_t shadowHiresResolveMissValue;
extern uint64_t shadowHiresResolveMissEpoch;
extern uint64_t shadowHiresResolveMissNoPage;
/* Replication factor. 1 whenever hi-res is off, so callers may
* multiply by it unconditionally. */
extern int shadowHiresN;