Skip to content

v2.10.0 "Prism"

Choose a tag to compare

@doublegate doublegate released this 03 Jul 01:23

Rusty2600 v2.10.0 "Prism"

Seventh release of the RustyNES gap-closure arc — grows
rusty2600-gfx-shaders beyond its prior CRT scanline + composite-artifact
approximation with a genuine NTSC composite YIQ decode, hqNx/xBRZ
upscaling, a generalized arbitrary-length shader stack, and a constrained
RetroArch .slangp/.cgp preset importer.

Shipped through PR #20, reviewed by GitHub Copilot and Gemini Code
Assist — 6 findings, all genuine, all fixed, including a real bug both
bots independently caught.

A genuine NTSC composite YIQ decode

PassKind::NtscComposite is adapted — not ported — from the
Bisqwit/Mesen signal-decode technique used in the sibling RustyNES
project, re-derived for the TIA's own 4-bit-hue + 3-bit-luma color
model. The TIA's dot clock runs at exactly the NTSC color-subcarrier
frequency, so unlike the NES there's no per-dot phase drift to
synthesize: a given (hue, luma) pair decodes in isolation to one fixed
point in YIQ space, already captured by the existing measured palette.

The genuinely new physics this pass models is the real, named cause of
2600 "artifact colors": a real composite decoder's chroma (I/Q)
bandwidth is much narrower than its luma (Y) bandwidth (~0.6-1.5 MHz vs
~4.2 MHz). The pass takes the center tap's luma unblended and a
5-tap weighted average of the neighborhood's chroma, then converts back
to RGB — sharp luma detail (a single-dot sprite edge), blended chroma
(the hue-dithering artifact colors 2600 programmers deliberately
exploited).

Verification, stated plainly: a pure-Rust test proves the
forward/inverse YIQ matrix pair is a true mathematical inverse for all
128 real NTSC palette entries — the standard published FCC/SMPTE
coefficients, not fabricated numbers. A second test guards the WGSL's
duplicated palette table against transcription drift from the real
frontend table. NTSC-only: PAL's phase-alternating modulation and
SECAM's absent chroma aren't modeled, so the Settings checkbox is
disabled outside the NTSC region.

Landing this needed threading the raw TIA palette-index byte through
the presentation path (present_buffer::Frame::index,
EmuCore::index_buffer()) purely additively, alongside — never
replacing — the existing RGB conversion every other build path still
uses unchanged.

hqNx and xBRZ upscaling

PassKind::HqNx/Xbrz — independently re-derived WGSL adaptations of
the published edge-directed pixel-art smoothing techniques (Maxim
Stepin's hqx, Hyllian/Zenju's xBR/xBRZ), not ports of any existing
implementation.

A generalized shader stack

ShaderStack no longer assumes exactly 2 built-in passes — the
previous fixed-2-slot design's own doc comment incorrectly claimed a
3rd intermediate texture would be needed for a 3rd pass; a linear chain
only ever needs 2 ping-pong textures regardless of length. render now
loops over an arbitrary pass list.

A constrained RetroArch preset importer

slang_preset.rs matches the sibling RustyNES project's own ADR 0013
scope exactly: parses a .slangp/.cgp preset, maps known community
shader filename stems onto Rusty2600's 5 built-in passes, and reports
anything unrecognized as explicitly unsupported — never silently
dropped, and never any attempt at real GLSL/Slang-to-WGSL translation
(that stays permanently out of scope, same reasoning RustyNES's own ADR
gives). Deliberately never maps a preset entry onto the
position-constrained NtscComposite, since a preset could place it
anywhere in the stack.

PR review, for real

PR #20 was reviewed by GitHub Copilot and Gemini Code Assist — 6
findings, all genuine, all fixed:

  • A real ShaderStack::render bug (both bots, independently): the
    ping-pong parity (i % 2) and is_last check were computed off the
    original pass-list index, even though a position-constrained pass
    found anywhere but index 0 gets defensively skipped. If that skipped
    pass were last in the list, no executed pass would ever see is_last = true — nothing would write the swapchain, a blank frame. If it
    were in the middle, the parity of subsequent original indices would
    no longer match which ping-pong texture actually held the latest
    data — a wrong-texture read. Fixed by filtering to the
    actually-executed pass list first and computing both from that
    list's own indices, with direct unit tests for both failure shapes.
  • Inline comments in a preset value (Gemini Code Assist): shader0 = "path" # note or // note would corrupt parsing. Fixed — a quoted
    value is now kept verbatim, an unquoted value is truncated at the
    first comment marker.
  • Duplicate shaderN keys weren't deduplicated (Gemini Code
    Assist): a repeated key produced duplicate/redundant passes instead
    of the RetroArch-standard last-write-wins override. Fixed via a
    BTreeMap.
  • stock/passthrough stem matching was exact-match only (Gemini
    Code Assist): now a substring match, consistent with every other
    stem check.
  • "(see log)" logged nothing (Copilot): unsupported preset passes
    are now actually printed to stderr.

Test count

374 tests passing on default features (378 with --features test-roms) — up from 349/353, 25 new tests covering the YIQ
round-trip/palette parity, the preset importer, and the
ShaderStack::render bug fixes. Full CI green — Linux/macOS/Windows,
the perf regression gate, and the no_std gate. Independently
re-verified beyond the standard gate: cargo clippy --target wasm32-unknown-unknown --features wasm-winit[,debug-hooks] clean;
--features wasm-canvas (the other wasm build) unaffected.

Honest verification boundary

Live in-browser verification (the new passes actually rendering, the
preset importer actually driving a real Settings dialog) remains
blocked by the same sandboxed-headless-Chromium GPU limitation
documented since v2.5.0 — this sandbox can't render wasm-winit at
all. Everything verifiable without live browser GPU access (unit
tests, WGSL naga validation, wasm32 compile + clippy checks) has been
confirmed.

What's next

v2.11.0 "Field Trip" — Mobile Parity: save-state UI on Android and
iOS (reusing rusty2600-mobile's already-real bridge methods, never
exposed in either UI), a research pass on cloud save-state sync, and
real physical Android hardware verification if available. See
to-dos/ROADMAP.md for the complete v2.4.0 -> v3.0.0 roadmap.