Skip to content

feat(gfx): add CRT/HQx presentation post-filter pipeline (v1.2.0) - #64

Merged
doublegate merged 2 commits into
mainfrom
feat/v1.2.0-crt-shader
Jul 11, 2026
Merged

doublegate merged 2 commits into
mainfrom
feat/v1.2.0-crt-shader

Conversation

@doublegate

Copy link
Copy Markdown
Owner

Summary

  • Adds a PostFilter enum (None/Crt/Hqx, Settings -> Video radio row + per-filter strength
    sliders, plus a View -> Post-filter menu submenu).
  • CRT: scanlines (a parabolic per-source-row brightness profile) + an RGB aperture-grille
    mask (fixed-pitch phosphor-triad tint), each with its own 0.0..=1.0 strength slider.
  • HQx: a single-pass, edge-directed diagonal blend (a 2xSaI/Eagle-family diagonal-similarity
    heuristic) -- an HQ2x-style approximation, not a literal lookup-table port, matching this
    project's fixed-resolution architecture.
  • PostFilter::None (default) is the pre-existing direct blit kept byte-for-byte unchanged:
    Gfx::present's None arm calls the same unmodified Gfx::blit, not a re-derived equivalent.
  • Gfx::letterbox_scale extracted out of blit's own inline math (a pure, behavior-preserving
    refactor) so both filter passes share the exact same letterbox convention.
  • Shaders are inline const &str WGSL in gfx.rs, matching the existing BLIT_WGSL convention.

Test plan

  • naga WGSL-parse+validate tests for both new shaders (crt_wgsl_validates,
    hqx_wgsl_validates)
  • letterbox_scale_matches_known_cases -- hand-computed regression test for the extracted
    letterbox helper
  • Real headless xvfb-run launch of the native binary against a staged ROM with each of
    None/Crt/Hqx set in config.toml -- all three ran clean (zero stderr, no panics)
    against a real wgpu adapter
  • cargo test --workspace (455 tests, 44 suites)
  • cargo test -p rustysnes-test-harness --features test-roms --release (28 tests/17 suites,
    zero regressions -- this change only touches rustysnes-frontend)
  • cargo clippy --workspace --all-targets -- -D warnings across default/flags-off/full
  • cargo fmt --check
  • RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps
  • cargo build -p rustysnes-core --target thumbv7em-none-eabihf --no-default-features (no_std)
  • Both wasm32 frontends (wasm-winit default, wasm-canvas) build clean via trunk build --release

🤖 Generated with Claude Code

Adds a PostFilter enum (None/Crt/Hqx, Settings -> Video radio row +
per-filter strength sliders, plus a View -> Post-filter menu submenu).

- CRT: scanlines (a parabolic per-source-row brightness profile) + an
  RGB aperture-grille mask (fixed-pitch phosphor-triad tint), each with
  its own 0.0..=1.0 strength slider.
- HQx: a single-pass, edge-directed diagonal blend (a 2xSaI/Eagle-family
  diagonal-similarity heuristic) -- an HQ2x-style approximation, not a
  literal lookup-table port, matching this project's fixed-resolution
  architecture (it never renders to a literal 2x intermediate buffer).
- PostFilter::None (default) is the pre-existing direct blit kept
  byte-for-byte unchanged: Gfx::present's None arm calls the same
  unmodified Gfx::blit, not a re-derived equivalent.
- Gfx::letterbox_scale extracted out of blit's own inline math (a pure,
  behavior-preserving refactor) so both filter passes share the exact
  same letterbox convention; regression-tested against hand-computed
  cases for windows wider/narrower/exactly the 4:3 SNES aspect.
- Shaders are inline `const &str` WGSL in gfx.rs, matching the existing
  BLIT_WGSL convention -- not split into a separate shader crate (no
  second consumer to justify it, unlike RustyNES's Android target).

Verified: naga WGSL-parse+validate tests for both new shaders, plus a
real headless xvfb-run launch of the native binary against a staged
ROM with each of None/Crt/Hqx set in config.toml -- all three ran
clean (zero stderr, no panics) against a real wgpu adapter. Full
workspace suite (455 tests), the ROM-oracle battery (28 tests/17
suites, zero regressions), clippy clean across default/flags-off/full,
fmt clean, doc build clean, no_std clean, both wasm32 frontends build
clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 11, 2026 13:10

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request implements the CRT/HQx presentation post-filter pipeline for the v1.2.0 release. It introduces a PostFilter configuration supporting None, Crt (scanlines and aperture-grille mask), and Hqx (edge-directed diagonal blend approximation) options, along with corresponding UI controls in the settings panel. The rendering pipeline in gfx.rs has been updated to build and select these shaders dynamically. Feedback suggests optimizing the hot rendering path in Gfx::present by combining two separate write_buffer calls into a single 32-byte uniform buffer write to reduce CPU and driver overhead.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread crates/rustysnes-frontend/src/gfx.rs Outdated

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 presentation post-filter pipeline to the rustysnes-frontend wgpu render path, enabling CRT-style scanlines/mask and an HQ2x-style edge-directed blend while keeping the default None path structurally identical to the pre-existing blit.

Changes:

  • Introduces PostFilter (None/Crt/Hqx) with config defaults and UI controls (Settings → Video + View → Post-filter).
  • Adds two new wgpu render pipelines (CRT + HQx) and a shared letterbox_scale helper so filtered passes and the baseline blit share identical letterboxing.
  • Documents the feature across roadmap/status/changelog/docs and adds WGSL validation + letterbox regression tests.

Reviewed changes

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

Show a summary per file
File Description
to-dos/VERSION-PLAN.md Marks CRT/HQx post-filter pipeline as completed for v1.2.0 and updates regression gate notes.
to-dos/ROADMAP.md Updates v1.2.0 roadmap narrative to include the post-filter pipeline landing.
README.md Updates v1.2.0 “in progress” section to include CRT/HQx post-filters.
docs/STATUS.md Updates v1.2.0 status to reflect CRT/HQx pipeline completion and remaining release steps.
docs/frontend.md Adds a detailed “Presentation post-filters” section documenting behavior, architecture, and verification.
crates/rustysnes-frontend/src/ui_shell.rs Adds View menu “Post-filter” submenu + Settings UI for filter selection and strength sliders.
crates/rustysnes-frontend/src/gfx.rs Implements present() with CRT/HQx shader pipelines, extracts letterbox_scale, adds WGSL + letterbox tests.
crates/rustysnes-frontend/src/config.rs Adds PostFilter enum and new VideoConfig fields for filter selection and per-filter parameters.
crates/rustysnes-frontend/src/app.rs Switches rendering from direct blit() to present() with live config-driven filter parameters.
CHANGELOG.md Adds a v1.2.0 changelog entry describing the CRT/HQx post-filters and verification steps.

Comment thread crates/rustysnes-frontend/src/config.rs
Comment thread crates/rustysnes-frontend/src/gfx.rs
Comment thread crates/rustysnes-frontend/src/gfx.rs
- present(): combine the two write_buffer calls into one over the
  whole 32-byte uniform -- fewer queue-submission round trips in this
  per-frame hot path.
- present(): clamp crt_scanline/crt_mask (and hqx_strength) to
  0.0..=1.0 before writing them, matching the in-shader clamp Hqx's
  edge_bias already had -- a hand-edited config.toml (or a future UI
  bug) must not be able to push these outside the range the shader's
  brightness-compensation math assumes.
- begin_pass now takes a label param; blit passes its own
  "rustysnes-blit-pass" and each FilterPipeline carries its own label
  ("rustysnes-crt"/"rustysnes-hqx"), so a GPU capture/profiler no
  longer shows a stale blit-pass label when a post-filter is active.
- Reworded PostFilter::None's doc: "round-trip unchanged" overclaimed
  a textual guarantee Config::save doesn't provide (it re-serializes
  the whole struct every save, so an old config.toml does gain new
  fields' TOML text on next save) -- the actual guarantee is behavioral
  (a config predating this field deserializes to this value via
  #[serde(default)], so existing setups behave exactly as before).

Re-verified via a live headless xvfb-run smoke test of both Crt/Hqx
filters (the combined uniform write touches the actual byte layout,
not just naga's static shader validation) -- both still run clean,
zero errors. Full workspace suite, clippy clean, fmt clean, doc build
clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@doublegate
doublegate merged commit e4a96fa into main Jul 11, 2026
6 checks passed
@doublegate
doublegate deleted the feat/v1.2.0-crt-shader branch July 11, 2026 13:26
@doublegate doublegate mentioned this pull request Jul 11, 2026
5 tasks done
doublegate added a commit that referenced this pull request Jul 11, 2026
Version bump + CHANGELOG/docs sync for the v1.2.0 "Phosphor" release,
closing out the Libretro-core + CRT/HQ2x-shader-pipeline arc (PRs #62,
#63, #64).

Also fixes a review finding: reintroduced an empty [Unreleased]
CHANGELOG section (Keep a Changelog, which this project's own header
commits to, expects it to persist above the latest dated release).
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