mirror of
https://github.com/libretro/virtualjaguar-libretro.git
synced 2026-11-07 04:36:00 -10:00
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 clean836baf6tree (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:
1 parent
8648a5b573
commit
83d854b0e1
8 files changed
+300
-5
No files matched your search
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -65,3 +65,7 @@ _NVMBios*
|
||||
_NVM_*
|
||||
_JGD*
|
||||
_jgd*
|
||||
_shadowHiresResolveHits
|
||||
_shadowHiresResolveMissValue
|
||||
_shadowHiresResolveMissEpoch
|
||||
_shadowHiresResolveMissNoPage
|
||||
@@ -68,5 +68,9 @@
|
||||
NVM_*;
|
||||
JGD*;
|
||||
jgd*;
|
||||
shadowHiresResolveHits;
|
||||
shadowHiresResolveMissValue;
|
||||
shadowHiresResolveMissEpoch;
|
||||
shadowHiresResolveMissNoPage;
|
||||
local: *;
|
||||
};
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user