Repository navigation
feat(gfx): add CRT/HQx presentation post-filter pipeline (v1.2.0) - #64
Conversation
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>
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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_scalehelper 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. |
- 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>
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).
Summary
PostFilterenum (None/Crt/Hqx, Settings -> Video radio row + per-filter strengthsliders, plus a View -> Post-filter menu submenu).
mask (fixed-pitch phosphor-triad tint), each with its own
0.0..=1.0strength slider.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'sNonearm calls the same unmodifiedGfx::blit, not a re-derived equivalent.Gfx::letterbox_scaleextracted out ofblit's own inline math (a pure, behavior-preservingrefactor) so both filter passes share the exact same letterbox convention.
const &strWGSL ingfx.rs, matching the existingBLIT_WGSLconvention.Test plan
nagaWGSL-parse+validate tests for both new shaders (crt_wgsl_validates,hqx_wgsl_validates)letterbox_scale_matches_known_cases-- hand-computed regression test for the extractedletterbox helper
xvfb-runlaunch of the native binary against a staged ROM with each ofNone/Crt/Hqxset inconfig.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 warningsacross default/flags-off/fullcargo fmt --checkRUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-depscargo build -p rustysnes-core --target thumbv7em-none-eabihf --no-default-features(no_std)wasm-winitdefault,wasm-canvas) build clean viatrunk build --release🤖 Generated with Claude Code