Files
Joe MattielloandClaude Fable 5 104ee5d178 docs: AvP renderer analysis — hi-res Stage 2 already reaches it (#338) (#359)
* 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: AvP renderer analysis — Stage 2 already reaches it (#338)

Commissioned to explain a measured 0.0000% supersampled-block result for
Alien vs Predator and to find an extension that would reach it.  The
premise does not hold at branch HEAD: AvP measures 30.62% supersampled
2x2 blocks in a static corridor and 13.01% in a moving scene, on BOTH
blitter engines, producing bit-identical output.

The 0.0000% is reproduced exactly by reverting HIRES_EPOCH_WINDOW 16 -> 2
in src/tom/shadowfb.c -- the constant 404cb11 changed for the identical
symptom in Doom.  A/B with everything else held fixed: gate passes
150,480 blits either way, 13,117,200 Stage 2 blocks stored either way,
6,087,600 of them non-uniform either way -- and OP resolve hits go
42,827,520 -> 0.  The detail was produced and then discarded at the
value+epoch check, 100% of it.  AvP double-buffers and takes more than
one presented frame per rendered view, so the displayed buffer is always
older than a 2-frame window.

Also answers the rest of the brief, which stands regardless:

- Blit census in gameplay: 252 blits/frame, 99.4% SRCSHADE textured spans
  onto a 16bpp CRY destination with a fractional inner source walk; zero
  GOURD, zero Z, zero UPDA1F, zero ADDDSEL.  99.92% of census-qualifying
  blits pass the Stage 2 gate; the only rejects are 120 !SRCEN blits.
  No census-vs-gate predicate gap exists for this title.
- Renderer shape: Doom-style, two families -- vertical wall columns
  (A2 XADD0 + YADD1, source Y fraction) and horizontal floor/ceiling
  spans (A2 XADDPIX, source X fraction), 128px textures onto a 320px
  framebuffer, OUTER==1 on 99.8% of blits.
- R2 for AvP (unmeasured by the Stage 0 census): 100% blitter-rasterized.
  GPU stores 1,256 B/frame into main RAM and 0 B/frame into any buffer
  the OP scans; its role is to program the blitter (151,200 of 151,440
  blits launched with who == GPU).
- No composite copy: the rasterizer writes straight into the displayed
  double-buffered surface at $094000-$0DDFFF; OP resolve hit rate 97.8%.
- GPU disassembly of the rasterizer proves sub-texel information exists:
  it splits 16.16 fixed-point texture coordinates into A1_PIXEL/A1_FPIXEL
  and 16.16 steps into A1_INC/A1_FINC, handing the blitter the full
  16-bit fraction of both start phase and step.
- Honesty check: 52% (static) / 36% (moving) of Stage 2 sub-samples land
  on a different source texel -- real recovery.  The rest are magnified,
  where no N can invent detail.

Verdict: no extension needed, and each hypothesised one is measured
false (GPU-store hook, phrase-mode dwrite, composite-copy propagation).
Transferable finding: check the OP resolve hit rate before investigating
blit shapes on any future "title X shows no supersampling" report --
the epoch-window failure is silent, total, and logs nothing.

Research only.  Instrumentation was a throwaway compile-time patch and
is not part of this commit.

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

* docs: AvP hi-res evidence screenshots + review fixes (#338)

Adds the visual evidence the report was missing and addresses the PR
review threads.

Screenshots (docs/site/pr-avp-hires-*.png, embedded in section 0.1):
matched 1x/2x captures of frame 6000 from the section 9 reproduction
command, plus two 6x-magnified crops.  The 1x panel is nearest-neighbour
replicated to the 2x geometry -- exactly what the Stage 1 path presents --
so every difference between panels is Stage 2 content.  The doc says
plainly that the full-frame difference is subtle and that part of what the
door crop adds is a half-step value rather than new structure.

The measurement holds: 30.6237% non-uniform 2x2 blocks at frame 6000,
reproduced exactly from the section 9 command.

Metric caveats, all measured, in a new "Reading the number" subsection:

- The percentage is 2x-ONLY.  At 1x it compares independent scene pixels
  and reads 90.0051% on the same frame; the tool header claimed 0.000
  there, which was wrong.  Corrected in hires_shot.c.
- A video_stall log line does NOT mean the metric is invalid.  AvP
  re-blits an identical view when the player stands still, so the frame
  freezes while the shadow surface stays fresh: frames 3200/4000/5000/
  5800/6000 of the fixture are byte-identical PPMs and all read 25.0601%.
  The 30.6237% frame sits inside a video_stall window too.
- A real 0.0000% at 2x has four causes besides "Stage 2 missed this
  title": a non-3D scene (AvP's menus measure exactly 0.0000%), zero
  frames dumped (total_blocks=0 prints 0.0000), a Stage-2-less build, and
  the aged-out shadow this report explains.

test/fixtures/avp_reach_gameplay.press gains keep-alive input to frame
~6300.  Without it the scene freezes after frame 3000 -- harmless for the
3000-frame gate, a trap for anything longer.  With it the same frames read
23.59/10.94/22.46/21.13/21.05%: non-zero and varying, i.e. live.  Presses
past the run length never fire, so existing runs are unaffected.

Review fixes:

- blitter.c: drop the duplicated PIXEL_OFFSET_16 comment block.
- hires_shot.c: size the framebuffer with sizeof(*st->fb).
- The report's status line now explains that this branch is stacked on
  #357, so a diff against develop carries that PR's product code.

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

* docs: verify panel alignment + re-measure both AvP figures (#338)

Follow-up evidence for the section 0.1 screenshots.

Panel alignment is now proven rather than asserted: across frame 6000, all
54,280 uniform 2x2 blocks in the 2x capture equal their 1x source pixel
exactly, and so does the top-left sub-pixel of all 23,960 non-uniform ones.
That establishes the two runs are the same machine state at the same frame
(so any panel difference is Stage 2 content) and, separately, that Stage 2
is strictly additive -- it never alters the stock sample.

Both headline figures re-measured from a clean rebuild: static 30.6237% at
frame 6000, moving 13.0138% over frames 5650-6100 with --press 5600:up:400.

Section 8 no longer contradicts the new section 9 note: its 'no log line'
claim is scoped to a line naming the Stage 2 failure, since video_stall
fires on healthy runs too.

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

---------

Co-authored-by: Claude Fable 5 <[email protected]>
2026-08-09 01:46:23 -04:00
..