Commit Graph
6 Commits
Author SHA1 Message Date
Joseph MattielloandClaude Opus 5 73ce436534 feat(texreplace): RGB16-direct presents, and static tiles keep Nx art (#528)
Both limitations #526 shipped with, deliberately deferred and documented
in its limits section.

1. RGB16-direct Nx renderer was unwired. Wired for BOTH 1x and Nx rather
   than only 2x: tier 1 does not present on the 1x RGB16 path either, so
   wiring only the Nx one would make a pack look different at 2x than at
   1x for a reason unrelated to resolution.

2. Static tiles aged out to flat colour. Nx blocks carry the surface's
   frame epoch and stock supersampled content is trusted for
   HIRES_EPOCH_WINDOW (16) presented frames; pack art is now exempt from
   that age check, while the stock sub block behind an author alpha hole
   still ages. Before this, a tile blitted once and left on screen -- a
   HUD, a menu, a title card -- showed crisp Nx art for ~16 frames and
   then degraded to the 1x representative's flat blocky colours.

   The exemption adds no new failure class: the window bounds stale
   structure (an NxN interior derived from a write RAM no longer holds),
   and for pack art that class is already unbounded and already shipping
   at 1x, where the tier-1 shadow carries no epoch at all and presents
   the same tile's 1x representative at the same pixel on the same value
   check. Refusing the Nx block prevented no wrong-tile artifact; it only
   dropped that artifact's resolution.

Gates (test_texreplace):
  hires_static_persist -- steps REAL presented frames between the battery
    blits and the probe (harness_step drives retro_run drives
    ShadowHiresFrameTick; nothing fakes the epoch), and first asserts the
    destination RAM is unchanged so expiry cannot be confused with a
    coherence miss. 24 frames against a window of 16.
  rgb16_presents -- 1x 574/574 and 2x 575/575 delivered, 0 wrong, 0
    non-pack entries wrongly claimed, and the no-pack run inert
    (shadowFBReplActive == 0, 0 claims).

make TEST_EXPORTS=1 test: 1658 PASS, 0 FAIL, audio pair confirmed by name
(Clipping check: Iron Soldier 2; two presence passes). C89 lint clean.
Rebased onto libretro/develop after #531/#537/#539/#541 and re-verified --
the pre-rebase branch would have deleted test/tools/ir_ab.sh.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 18:44:09 -04:00
Joseph MattielloandClaude Opus 5 8b7ba835c1 test(texreplace): assert the 1x representative, document epoch expiry
Two gaps found in self-review of the tier-3 change.

`expect_1x_hits(t, 1)` was written but never called: gate 7 asserted the
Nx blocks and gate 3 only covered the 1x run, so the "1x representative
is the pack's TOP-LEFT subpixel" rule -- chosen precisely because it is
trivially predictable and therefore assertable -- shipped unasserted.
Storing the bottom-right pixel, an average, or nothing at all would have
passed all eight gates.  Gate 7 now checks rH->hits/wrong_rgb against it
(63 for the alpha-holed tile: the top-left of stock word (0,0) IS the
PNG (0,0) hole) and checks the 1x presentation path at 2x as well.

Documented, not asserted: Nx blocks carry the surface's frame epoch and
are trusted for HIRES_EPOCH_WINDOW (16) presented frames, while the 1x
shadow has no epoch at all.  So a tile blitted once and left on screen
-- static HUD, menu, title card, exactly the content packs are most
attractive for -- shows crisp Nx art for ~16 frames and then ages out to
the 1x representative: flat blocky top-left colours, NOT stock.  Which
is the load-bearing reason the 1x representative is recorded, previously
unstated.  The battery cannot observe this (its blits run after the
frame loop and probe with a current epoch), so it goes in the limits
list rather than into a gate that would have to fake the epoch.

Also records why the shared SHADOWFB_HIRES_CAP_BYTES budget is safe by
arithmetic: at N=2 all 256 base pages are ~20.5 MB and all 256
replacement planes ~16.8 MB against a 64 MB cap, and MAX_N is 2 -- a
pack cannot starve base allocation.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 03:47:48 -04:00
Joseph MattielloandClaude Opus 5 d282498fb8 feat(texreplace): >1x pack art on the Nx shadow surface (#369 tier 3)
Texture packs could only be authored at the dumped pixel dimensions,
and at Internal Resolution Nx they were worse than that: the hi-res
shadow line entry wins wherever it hits, so the 1x replacement surface
only showed through on a resolve MISS -- hi-res masked packs outright.

Mechanism.  A `shadowfb_sub` is {value16, frac16}, a CRY decomposition,
and pack RGB is not a function of the stock word, so it cannot live in
one.  Rather than widen every subpixel entry to 8 bytes for every user
(and change the blit memo's BLIT_MEMO_SH_BLOCK serialization), pack art
rides a PARALLEL per-page plane of N*N uint32 entries plus ONE extra bit
in the same word tag (HIRES_TAG_REPL).  That buys three properties from
machinery that already exists:

  - any ordinary ShadowHiresStoreCry/Block to the word rewrites the tag
    WITHOUT the bit, so stale pack art self-clears -- no invalidation
    hooks, the same argument that justified the tag scheme for true
    colour;
  - the value+epoch coherence check is unchanged and shared;
  - a 1x run, or a 2x run with no pack, allocates nothing: planes are
    allocated per page on first pack write, and both the OP resolve and
    the Nx scanline renderer hoist a single shadowHiresReplActive test.

Entry encoding is SHADOWFB_HIRES_REPL_VALID | RGB888, or 0 for "keep
what is underneath", so an author's alpha hole falls through to the
Stage 2 supersampled content rather than to a flat colour.  Where the
word's existing entry is not live for this value, the hole subpixels are
seeded with box replication first -- otherwise a hole would expose
uninitialised sub content behind the tag being stamped.

Dimension rule becomes exactly 1x or exactly Nx in both axes.  Nothing
between: an arbitrary resize has no defined mapping onto stock pixel
words and would make the per-pixel straight-copy witness meaningless.
1x art is stored into the Nx surface too, replicated N*N, which is what
removes the masking above.  The 1x representative of a word covered by
Nx art is the pack's TOP-LEFT subpixel (not an average -- an average
invents a colour the author never drew), so a hi-res resolve miss
degrades to pack colour instead of stock.

Three details that would each have been a silent bug:

  - ShadowHiresRestampRamPage now preserves HIRES_TAG_REPL.  It is
    called by the blit memo, so dropping the bit there would make pack
    art vanish on exactly the repeated identical blits the memo skips --
    an intermittent "coherence" symptom with a masking cause.
  - the RGB store is deliberately NOT logged to the blit memo (the
    replacement walk re-runs on every launch, memo skips included, and
    the memo's replay deserializes shadowfb_sub blocks -- feeding it RGB
    words would silently reinterpret them).  Same reasoning as
    ShadowFBStoreRGB.
  - replacement-plane allocation failure sets its OWN stopped flag, not
    hiresAllocStopped: running out of budget for pack art must degrade
    the PACK, never the base hi-res feature.

Unchanged: zero savestate fields, zero RAM writes, zero bus-model time.
The plane is a derived cache like every other shadow surface, dropped by
ShadowHiresInvalidate on savestate load and when a pack is unloaded.

Not wired in, deliberately: the RGB16-direct Nx renderer.  Tier 1 does
not present on the 1x RGB16 path either, so wiring only the 2x one would
make a pack look different at 2x than at 1x for reasons unrelated to
resolution.

Still open on #369: tier 2, indexed (<=8bpp) sources.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 03:39:59 -04:00
Joseph MattielloandClaude Opus 5 b093cb3b6d docs: texture replacement pipeline (pack layout, architecture, tiers)
Extends docs/texture-dump.md (one document for both #369 deliverables,
as specced): vj_texpacks layout mirroring the dump tree, the host-only
presentation-riding architecture and its consequences, tier status and
known limits (CRY 16bpp OP path, value-tag coherence edge, hi-res
interaction), the test gates, and a replacement authoring guide.

Part of #369.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-17 23:49:03 -04:00
Joseph MattielloandClaude Opus 5 a4fc611a64 docs(texdump): mark implemented; authoring guide + open-item resolutions
Adds the pack-author section promised by the file plan (directory
layout, filename-is-the-contract, palette caveats, 16bpp preview
note) and records how each open item resolved: B_CMD source-channel
decode verified against docs/jtrm-blitter.md; miniz links cleanly
from outside the unity TU (no wrapper needed, define carried in
LIBCHDR_CFLAGS for the theos_ios path); golden-list population comes
from the synthetic blit battery because no in-tree public ROM is a
meaningful tripwire by itself.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-17 20:59:55 -04:00
Joseph MattielloandClaude Opus 5 ac266288cd docs(texdump): design spec for texture dump mode (#369 deliverable 1)
Capture at the shared blit-launch site from the register-described
source window (engine-independent, pace-neutral by construction);
FNV-1a 64 bytes-only identity contract with a v1 version marker;
PNG via the already-vendored miniz encoder (one build define);
four inertness/determinism test gates including savestate-digest
proof that the emulated machine cannot see the feature.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-17 20:12:43 -04:00