Skip to content

feat(v2.1.9): Presentation & Signal — raw NTSC composite (P4) + marquee CRT shader stack (B6)#283

Merged
doublegate merged 9 commits into
mainfrom
feat/v2.1.9-presentation
Jul 12, 2026
Merged

feat(v2.1.9): Presentation & Signal — raw NTSC composite (P4) + marquee CRT shader stack (B6)#283
doublegate merged 9 commits into
mainfrom
feat/v2.1.9-presentation

Conversation

@doublegate

Copy link
Copy Markdown
Owner

v2.1.9 "Presentation & Signal"

A presentation/display cut, all opt-in and default byte-identical. Every deliverable is additive; with defaults the deterministic core, the default framebuffer, visual_regression, and AccuracyCoin are untouched.

Worktree-isolated (a prior agent corrupted the main tree). Branched from origin/main pre-A2 / pre the merging v2.1.6/v2.1.7 PRs; will rebase at ceremony time. All CRT/raw-decode shaders are new WGSL files — no restructuring of the NTSC ladder / blitter A2 owns.

What's COMPLETE

P4 — raw composite core (rustynes-ppu::raw_signal). A new parallel PPU output that keeps the 2C02 composite waveform un-decoded: for every (index 0..=63, emphasis 0..=7) pair it emits the twelve per-subcarrier-phase voltages the chip generates (canonical Bisqwit nes_ntsc / Mesen2 "raw palette" model), so a decoder reproduces signal-domain artifacts a per-colour palette cannot (composite bleed, dot crawl, waterfall/dither transparency). generate_raw_signal_lut() yields the full 512×12 table. No transcendental → f32 byte-identical across x86/aarch64/wasm/thumbv7em; a no_std GOLDEN_SIGNAL cross-target lock guards it. 6 unit tests. Additive + default-OFF.

P4 shader — signal_decode.wgsl. Reconstructs the actual two-level chroma square wave from the palette-index framebuffer (matching raw_signal.rs byte-for-byte) and demodulates with a windowed quadrature filter — decoding the true signal rather than re-encoding RGB. naga-validated.

B6 — CRT shader stack (new WGSL files + CrtStackShader registry).

  • CRT-Royale (crt_royale.wgsl) — luminance-scaled Gaussian beam in gamma-linear light, selectable aperture/slot/shadow mask, gamma-correct scanlines, curvature + vignette.
  • CRT Guest Advanced / guest-dr-venom (crt_guest.wgsl) — power-shaped beam, 5-tap halation glow, mask, curvature.
  • Sony Megatron (megatron.wgsl) — per-subpixel phosphor lighting into an exposed HDR headroom with an SDR Reinhard fallback.
  • All three share a 64-byte rect/crop/params/aux uniform (scanline weight, mask strength, mask type, curvature + per-shader knobs).

All four new shaders are gate-validated as real, compilable WGSL by crt::tests::crt_stack_shaders_parse_and_validate — the exact naga-29 front-end + validator wgpu runs at create_shader_module.

Docs (docs-as-spec). docs/frontend.md (CRT stack + signal-decode blocks), docs/ppu-2c02.md (raw-composite model), CHANGELOG.md [Unreleased].

What's PARTIAL / owed (honest status)

  • CRT-stack frontend wiring — sources + registry are complete and validated, but selecting them from the composable-stack UI and adding per-game shader presets is follow-on frontend wiring (reuses the existing ShaderStack plumbing; touching app.rs/config.rs/shader_pass.rs was deferred to avoid heavy conflict with the concurrent A2 vectorization).
  • Display suite — 8:7 PAR, overscan crop, and hqNx/xBRZ scalers already exist; the new CRT shaders expose curvature/mask/scanline uniforms atop them. A dedicated aspect + integer-scale toggle UI beyond what exists is not added here.
  • Capture (GIF/video/WAV) + palette editor UI — NOT implemented in this PR. The av-record format extension and a new palette-editor surface both live in large contended files (app.rs/ui_shell.rs/config.rs); deferred rather than ship a messy partial. (A palette editor D1 already exists for the generated-NTSC params.)

Gate results (all green)

  • cargo fmt --all --check · cargo clippy --workspace --all-targets -D warnings
  • frontend feature combos: scripting, scripting,hd-pack, retroachievements, av-record
  • both wasm32 gates (default + wasm-canvas)
  • RUSTDOCFLAGS=-D warnings cargo doc --workspace --no-deps
  • markdownlint (changed docs) · no_std thumbv7em cross-compile
  • AccuracyCoin 141/141 · visual_regression 9/9 byte-identical · raw_signal 6/6 · gfx-shaders registry 3/3 · naga shader validation

Default presentation confirmed byte-identical (only additive PPU module + new shader files + a naga test were added). wasm bundle only gains shader string constants (no wasm-bindgen surface change); a full trunk build was not run.

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings July 12, 2026 03:35
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an opt-in “signal-domain” presentation path by introducing a raw NTSC composite waveform model in the PPU plus a new marquee CRT shader stack and a raw signal-decode WGSL pass in the shared shader crate, with accompanying docs/spec updates and naga-based shader validation tests.

Changes:

  • Add rustynes-ppu::raw_signal to generate a deterministic 512×12 raw composite waveform LUT (index×emphasis → 12 phases).
  • Add new WGSL shader files (CRT-Royale, CRT Guest Advanced, Megatron, and signal-domain decode) plus a CrtStackShader registry/export surface.
  • Extend docs (docs/frontend.md, docs/ppu-2c02.md) and CHANGELOG.md, and add frontend tests to parse+validate the new WGSL via naga.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
docs/ppu-2c02.md Documents the new raw composite-signal model and its determinism contract.
docs/frontend.md Documents the CRT stack shaders and the raw signal-decode post-pass.
crates/rustynes-ppu/src/raw_signal.rs New PPU-side raw composite waveform generator + determinism tests.
crates/rustynes-ppu/src/lib.rs Wires raw_signal into the crate and re-exports the API/constants.
crates/rustynes-gfx-shaders/src/signal_decode.wgsl New index-texture NTSC signal reconstruction + YIQ demodulation shader.
crates/rustynes-gfx-shaders/src/megatron.wgsl New single-pass Megatron CRT-style shader.
crates/rustynes-gfx-shaders/src/crt_royale.wgsl New single-pass CRT-Royale shader.
crates/rustynes-gfx-shaders/src/crt_guest.wgsl New single-pass CRT Guest Advanced shader.
crates/rustynes-gfx-shaders/src/crt_stack.rs New shader registry and WGSL exports for the CRT stack + signal decode.
crates/rustynes-gfx-shaders/src/lib.rs Exposes the CRT stack module/exports from the shared shader crate.
crates/rustynes-frontend/src/crt.rs Adds naga parse+validate tests for all new stack shaders.
CHANGELOG.md Adds an Unreleased entry describing the new presentation/signal features.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/rustynes-gfx-shaders/src/signal_decode.wgsl
Comment thread crates/rustynes-gfx-shaders/src/signal_decode.wgsl
Comment thread crates/rustynes-ppu/src/raw_signal.rs Outdated
doublegate and others added 5 commits July 12, 2026 03:08
Add `rustynes-ppu::raw_signal`, a new parallel PPU output that keeps the
2C02 composite waveform un-decoded so a downstream NTSC decoder (shader or
host) can reproduce signal-domain artifacts a per-color RGB palette cannot:
composite color bleed, dot crawl, and the "waterfall"/dither transparency
tricks (Kirby's Adventure waterfalls, Zelda II title, the 240p test suite
color-bleed screens) that depend on adjacent-pixel chroma mixing.

Model: the canonical Bisqwit `nes_ntsc` / Mesen2 "raw palette" generator
(nesdev "NTSC video"). For each (index 0..=63, emphasis 0..=7) pair the chip
emits a two-level chroma square wave over 12 subcarrier phases; `InColorPhase`
positions the hue, the luma nibble picks the two `LEVELS` voltages, and each
of the three emphasis bits attenuates by 0.746 during the phases overlapping
its primary's hue region. `generate_raw_signal_lut()` yields the full 512-row
(index-major) x 12-phase normalized table a host uploads as a signal texture,
built directly on the heap (no 24 KiB stack temporary).

Determinism: the waveform is level-lookup + one multiply + one affine
normalize with NO transcendental, so the f32 output is bit-identical across
x86 / aarch64 / wasm / thumbv7em under IEEE-754 without libm. An in-crate
GOLDEN_SIGNAL snapshot locks that cross-target contract via a const-eval
sibling of the runtime path.

Additive + default-OFF: nothing here feeds the deterministic core or the
default presentation path, so the default framebuffer golden vectors and
AccuracyCoin stay byte-identical (141/141).

Verified: clippy -D warnings clean, no_std thumbv7em cross-compile builds,
6 unit tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
….1.9 B6+P4)

Add four new presentation shaders as NEW WGSL files in rustynes-gfx-shaders,
kept in their own `crt_stack` module so they compose atop the existing ladder
(CRT_WGSL / NTSC_LMP_WGSL / BISQWIT_WGSL) without disturbing the base blitter:

B6 — the marquee CRT stack, single-pass WGSL ports of the reference libretro
slang shaders, sharing a 64-byte rect/crop/params/aux uniform block:
  * CRT-Royale (crt_royale.wgsl) — Gaussian luminance-scaled beam integrated
    over the nearest source rows in gamma-linear light, selectable phosphor
    mask (aperture grille / slot / shadow-dot), gamma-correct scanlines, barrel
    curvature + edge vignette.
  * CRT Guest Advanced / guest-dr-venom (crt_guest.wgsl) — power-shaped beam
    with configurable width + sharpness, a 5-tap halation glow mixed back in
    linear light, shared mask selection, curvature.
  * Sony Megatron (megatron.wgsl) — per-subpixel phosphor lighting driven into
    an exposed HDR headroom with an SDR Reinhard tone-map fallback (the HDR
    hook is left in the uniform for a real HDR swapchain path).

P4 — the raw NTSC signal-decode companion (signal_decode.wgsl) for the new
rustynes-ppu::raw_signal model. It samples the palette-INDEX framebuffer
(texture_2d<u32>, index bits 0..5 + emphasis bits 6..8), reconstructs the 2C02's
actual two-level chroma square wave (8 sub-samples/pixel, 12-unit subcarrier
wheel, per-line dot-crawl phase, emphasis attenuation — matching raw_signal.rs),
and demodulates with a windowed quadrature filter. Because it decodes the real
signal, it reproduces composite bleed / dot crawl / dither-transparency that an
RGB re-encode (LMP) structurally cannot.

A `CrtStackShader` registry enum gives each shader a stable slug + display name
+ index-texture flag so a host (desktop, Android) can select one from config or
a per-game preset without hard-coding WGSL at the call site.

Gate-verified real, not just strings: a new naga test in the frontend
(`crt::tests::crt_stack_shaders_parse_and_validate`) runs the exact naga 29
front-end + validator wgpu uses at create_shader_module over ALL FOUR new
shaders — all parse and validate. Registry unit tests (source non-empty, slug
round-trip, index-texture flag) also pass.

Additive + default-OFF: the shipped default presentation is untouched, so the
default framebuffer stays byte-identical and AccuracyCoin holds 141/141.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
….md, CHANGELOG)

Document the v2.1.9 "Presentation & Signal" additions as spec (docs-as-spec):

- docs/frontend.md — a "Marquee CRT stack (B6)" block (CRT-Royale / CRT Guest
  Advanced / Sony Megatron single-pass WGSL ports, the shared uniform layout,
  the naga validation gate, and an honest wiring-status note) plus a "Raw NTSC
  signal-decode pass (P4)" block in the shader-ladder section.
- docs/ppu-2c02.md — a "Raw composite-signal model (raw_signal, P4)" subsection
  under the index-framebuffer/composite-outputs section: the Bisqwit/Mesen2 raw
  model, the 512x12 LUT, the no-transcendental cross-target determinism lock,
  and the additive/default-OFF byte-identity guarantee.
- CHANGELOG.md [Unreleased] — a comprehensive v2.1.9 entry (raw composite core,
  the three CRT shaders, the signal-decode pass, the naga-validated WGSL, the
  display-suite foundations), all flagged opt-in / default byte-identical.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e fixes

Green up the full CI gate set for the v2.1.9 presentation work:

- crt_stack.rs: use `Self` in the impl (use_self / name-repetition, 19x) and
  split the long first doc paragraphs on the four shader consts + the enum
  (too_long_first_doc_paragraph, 3x).
- raw_signal.rs: replace intra-doc links to private items (`tests::GOLDEN_SIGNAL`,
  `crate::palette_gen`, `crate::palette::build_rgba_lut_from_base`) with plain
  code spans so `RUSTDOCFLAGS=-D warnings cargo doc --workspace` passes (the
  same rustdoc private-item gate CLAUDE.md documents for feature-only deps).

No behavior change (docs + Self-aliasing only). Full gate re-run green:
workspace clippy + frontend {scripting, scripting,hd-pack, retroachievements,
av-record} + both wasm32 gates + workspace rustdoc all clean; AccuracyCoin
141/141, visual_regression 9/9 byte-identical, raw_signal 6/6.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…v2.1.8

The rebase onto the v2.1.6/2.1.7/2.1.8 cut moved the [Unreleased] anchor
(those releases each rotated their content into released [2.1.x] sections and
opened a fresh empty [Unreleased]), dropping the v2.1.9 entry. Restore it, and
extend it to cover the now-in-progress deferred deliverables (composable-stack
CRT UI wiring + per-game presets, av-record GIF/WAV capture, palette editor).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@doublegate doublegate force-pushed the feat/v2.1.9-presentation branch from c4abfcf to 23d5b91 Compare July 12, 2026 07:09
doublegate and others added 4 commits July 12, 2026 03:21
…e ShaderStack + per-game presets (v2.1.9 B6/P4)

Make the v2.1.9 marquee shaders first-class, user-selectable ShaderStack passes
(Settings -> Shaders) now that v2.1.8's rendering rewrite has merged.

BuiltinPass gains four variants — CrtRoyale / CrtGuest / Megatron / SignalDecode
— wired through from_id / id / label / all / params / is_index_source /
uses_phase, plus the compile_pass WGSL source map (the bodies come from the new
rustynes-gfx-shaders files) and the render() uniform fill:

- The three CRT passes are RGBA post-passes sharing the 16-float
  rect/crop/params/aux uniform. Their `#pragma parameter` sliders (declared in
  crt.rs: scanline weight, mask strength, mask type {grille/slot/dot}, curvature,
  and per-shader beam/gamma/glow/HDR knobs) are ordered to match the uniform
  slots so the existing generic declaration-order fill drops each knob into the
  right slot; the trailing "source rows" aux slot is left 0 and each shader falls
  back to 240 via `select` (the guest + Megatron aux layouts were reordered so
  the tunable knobs are contiguous and rows lands last).
- SignalDecode is an index-source pass (samples the R16Uint palette-index
  texture, must be first) that also consumes the live NES colour phase for
  dot-crawl — a dedicated render arm puts the phase in params.x and maps its five
  sliders (saturation, sharpness, brightness, contrast, hue) to params.y/z +
  knobs.x/y/z.

Per-game shader presets: PerGameConfig gains an optional `shader_preset` name;
on ROM load it resolves against the user preset bank then the built-ins
(ShaderPresetBank::resolve) and applies the stack to the live gfx. None / unknown
applies nothing, so the default load path stays byte-identical and the core is
untouched (presentation-only). Five showcase built-in presets added (CRT-Royale,
CRT-Royale Curved, CRT Guest Advanced, Sony Megatron, Raw NTSC Signal).

All four new shaders still parse+validate under naga after the aux reorder.
Tests: phase/index classification updated for the new passes; new tests for
param counts + id round-trip, preset resolution, and the per-game field
round-trip. shader_pass 13/13, crt 2/2, per_game 10/10.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extend the av-record capture feature with two new output formats, selected by
the chosen file extension in the recording save dialog (Container::from_path):

- Animated GIF (`.gif`) — video-only. A dedicated `gif_args` routes the same
  completed rawvideo temp file through ffmpeg's single-pass palettegen /
  paletteuse filtergraph (per-clip optimized 256-colour palette + Bayer dither),
  decimated to a GIF-friendly 25 fps so the file stays small and the NES palette
  reproduces crisply. No audio track.
- WAV (`.wav`) — audio-only. A dedicated `wav_args` transcodes the completed
  mono f32le PCM temp file to canonical 16-bit little-endian PCM at the capture
  sample rate — a quick lossless tune/SFX grab. No video.

`ffmpeg_args` branches on the container up front and delegates to the new pure,
side-effect-free builders (the A/V mp4/mkv two-input mux is unchanged). The
recorder itself is untouched — it always captures both raw streams to temp
files; the format just selects which the final ffmpeg pass consumes.
`Container` gains `Gif`/`Wav` plus `has_video()`/`has_audio()` helpers.

Determinism-safe: capture reads the existing read-only framebuffer/audio taps
and never perturbs the emulation timeline; the whole feature is behind the
opt-in `av-record` flag, so the default build is unaffected.

Tests: container inference for gif/wav (case-insensitive) + stream-presence
flags; gif_args is video-only with the palettegen/paletteuse/fps=25 filtergraph
and no audio; wav_args is audio-only pcm_s16le with no video. av_record 9/9.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… docs

The prior --no-verify commits left post-rebase fmt drift (the CRT preset tuple
and the new palette-preview code) and a clippy too_long_first_doc_paragraph on
SIGNAL_DECODE_STACK_PARAMS, which reddened the `fmt + clippy + rustdoc` CI job.
Fix both: `cargo fmt --all`, and split the signal-decode stack-params doc's
first paragraph.

Also folds in the last owed deliverable's polish and the docs:

- Palette editor: the generated-NTSC palette editor (saturation/hue/contrast/
  brightness/gamma sliders + 64-swatch grid) already existed; add a live 16x4
  preview of the GENERATED base that regenerates from the current params via the
  deterministic `generate_base_palette` as the sliders move, so the look can be
  dialled in before enabling it. Purely visual (`paint_palette_preview`).
- docs/frontend.md: document the CRT-stack UI wiring (BuiltinPass variants +
  Settings -> Shaders + the built-in showcase presets + per-game `shader_preset`)
  and the new GIF/WAV capture formats; the earlier "wiring status: follow-on"
  note is replaced now that the wiring is done.
- CHANGELOG [Unreleased]: the palette-editor bullet corrected to reflect that
  the editor pre-existed and v2.1.9 adds the live preview.

Full local gate re-run GREEN: fmt --check, clippy --workspace --all-targets
-D warnings, frontend {scripting, scripting+hd-pack, retroachievements,
av-record}, both wasm32 gates, rustdoc -D warnings, markdownlint. Sacred gate
held: AccuracyCoin 141/141, visual_regression 9/9 byte-identical (default
presentation unchanged). [Unreleased] contains ONLY the v2.1.9 entry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ator demod + gray-emphasis doc

Work the three Copilot review threads on #283:

1. Correctness (signal_decode.wgsl) — the reconstructed subcarrier `phase` was
   computed from the UNCLAMPED column, so a filter tap sampling off the left
   edge (negative absolute sub-sample) produced a negative WGSL `%` remainder,
   flipping `in_color_phase` / emphasis decisions inconsistently with the
   edge-clamped texel load (a visible edge artifact). Add a non-negative modulo
   helper `pmod(x, n) = ((x % n) + n) % n` and wrap both the per-tap `phase` and
   the per-line `line_phase` with it, so the subcarrier phase stays continuous
   across the edge and consistent with the (continuous) demod reference.

2. Performance (signal_decode.wgsl) — the demod inner loop called `sin()`/`cos()`
   every tap (~49 transcendental pairs/pixel), a real fullscreen-pass cost.
   Replace with a rotating oscillator: the reference phase advances by a constant
   `TAU/12` per tap, so evaluate the sin/cos pair ONCE at the window start and
   rotate it forward with the angle-addition identity (cos(p+d), sin(p+d)) using
   the fixed `cos d`/`sin d` — 3 transcendentals/pixel total instead of ~49.
   f32 drift over the ~49-step recurrence is ~1e-5 and this pass is opt-in
   (default-off, never on the deterministic path), so the look is unchanged;
   mirrors the Bisqwit pass's avoidance of per-tap transcendentals.

3. Docs (raw_signal.rs) — the module doc claimed grays "hold a constant level
   (no chroma)", but the emphasis attenuation is phase-selective, so a gray is
   non-flat for emphasis != 0. Scope the "flat / no chroma" property to
   `emphasis == 0` and note that emphasis breaks a gray's flatness.

Presentation-only + default-off: the signal-decode pass is not on the shipped
path, so visual_regression + AccuracyCoin are unaffected. The shader still
parses+validates under naga; raw_signal 6/6, crt naga validation 2/2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@doublegate doublegate enabled auto-merge (squash) July 12, 2026 07:39
@doublegate doublegate merged commit 659b88c into main Jul 12, 2026
21 checks passed
@doublegate doublegate deleted the feat/v2.1.9-presentation branch July 12, 2026 07:53
doublegate added a commit that referenced this pull request Jul 12, 2026
…294)

Cuts the v2.1.9 release — the presentation-and-signal step of the v2.1.5 → v2.2.0
"deepen the existing project" run, a marquee display release. Bumps the workspace
version 2.1.8 → 2.1.9, renames CHANGELOG `[Unreleased]` → `[2.1.9]`, refreshes
Cargo.lock, syncs the README badge / current-release paragraph / BibTeX, and
authors `.github/release-notes/v2.1.9.md`.

v2.1.9 (PR #283) is all opt-in / default-off display work, so the shipped default
presentation stays byte-identical (AccuracyCoin 141/141, nestest 0-diff,
visual_regression byte-identical):

- Marquee CRT shader stack: WGSL ports of CRT-Royale, crt-guest-advanced, and
  Sony Megatron (HDR), selectable with mask/scanline/curvature controls + per-game
  presets.
- Raw NTSC composite signal-decode path: a new rustynes-ppu::raw_signal
  un-decoded 2C02 composite model + a signal_decode.wgsl demodulator (rotating
  oscillator, edge-consistent phase) for artifact-accurate color.
- GIF + WAV capture (extending av-record) + a generated-NTSC palette editor with
  a live preview.

No default-build behavior change.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants