Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Scope/Multitap via `RETRO_DEVICE_SUBCLASS`) is a documented follow-up, not yet wired. New
additive `Bus::wram`/`wram_mut`, `Ppu::vram`/`vram_mut`, `Cart::sram_mut` accessors support it.
See `docs/libretro.md`.
- **CRT/HQx presentation post-filters** (Settings → Video / View → Post-filter). `PostFilter::Crt`
adds scanlines + an RGB aperture-grille mask (each with its own strength slider); `PostFilter::Hqx`
adds a single-pass, edge-directed diagonal blend (an HQ2x-style approximation, not a literal
lookup-table port) that softens staircase edges on flat-color pixel art. `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` rather than a re-derived equivalent. Verified via
`naga` WGSL-validity tests for both new shaders plus a real headless `xvfb-run` launch of all
three filter states against a live wgpu adapter (zero errors, no panics). See
`docs/frontend.md` §Presentation post-filters.

## [1.1.0] "Latchkey" - 2026-07-11

Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -383,9 +383,12 @@ refresh timing, and a fractional-timebase-refactor go/no-go assessment) — see
`to-dos/VERSION-PLAN.md`'s `v1.1.0` section for the full breakdown, including what's still open.

**`v1.2.0`** (in progress) relocates the pure `EmuCore` embedding facade into
`rustysnes-core::facade` and lands a real **Libretro core** (`rustysnes-libretro`, loadable by
`rustysnes-core::facade`, lands a real **Libretro core** (`rustysnes-libretro`, loadable by
RetroArch — region-aware NTSC/PAL, cheats, coprocessor firmware auto-resolution, raw memory-map
pointers; see `docs/libretro.md`); a **CRT/HQ2x shader pipeline** is next.
pointers; see `docs/libretro.md`), and a **CRT/HQx presentation post-filter pipeline** (Settings →
Video — scanlines + aperture mask, an HQ2x-style edge-directed blend; the default no-filter path
stays byte-for-byte identical to the pre-filter direct blit; see `docs/frontend.md` §Presentation
post-filters).

**Still deferred:** HD texture packs (the `hd-pack` flag exists in the manifest as a forward
placeholder; the loader itself is a TODO stub) — planned for `v1.3.0`.
Expand Down
13 changes: 11 additions & 2 deletions crates/rustysnes-frontend/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1094,8 +1094,17 @@ impl App {
label: Some("rustysnes-frame"),
});

// --- (3) Blit the framebuffer (clears then draws the fullscreen triangle). ---
active.gfx.blit(&mut encoder, &view);
// --- (3) Present the framebuffer (clears then draws the fullscreen triangle, through
// the active post-filter if any -- `v1.2.0`, `PostFilter::None` is byte-identical to the
// pre-filter-pipeline direct blit). ---
active.gfx.present(
&mut encoder,
&view,
config.video.filter,
config.video.crt_scanline,
config.video.crt_mask,
config.video.hqx_strength,
);

// --- (4) Run the always-on egui shell pass. The shell NEVER touches the emu lock. ---
#[cfg(all(feature = "retroachievements", not(target_arch = "wasm32")))]
Expand Down
52 changes: 52 additions & 0 deletions crates/rustysnes-frontend/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,45 @@ impl Region {
}
}

/// A presentation post-filter (`v1.2.0`). Applied after the plain nearest-sample framebuffer
/// blit, before the always-on egui shell pass — see `crate::gfx`'s module doc for the pipeline.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PostFilter {
/// No post-filter — the plain nearest-sample blit, pixel-identical to a filter-less build
/// (default; a `config.toml` predating this field deserializes to this value via
/// `#[serde(default)]`, so existing setups behave exactly as before. Note this is a BEHAVIOR
/// guarantee, not a textual one — `Config::save` re-serializes the whole struct, so an old
/// config gains this field's TOML text the next time settings are saved, same as any other
/// added field).
#[default]
None,
/// Scanlines + an RGB aperture-grille mask, approximating a CRT's phosphor structure.
Crt,
/// A single-pass, edge-directed diagonal blend that softens staircase edges on flat-color
/// pixel art — an HQ2x-style *approximation* (not a literal `HQ2x` lookup-table port).
Hqx,
}

impl PostFilter {
/// Human-readable label for the Settings radio row.
#[must_use]
pub const fn display_name(self) -> &'static str {
match self {
Self::None => "None",
Self::Crt => "CRT",
Self::Hqx => "HQx",
}
}

/// All filters in display order — the single source of truth the Settings radio row
/// iterates, so it can never drift out of sync with the enum.
#[must_use]
pub const fn all() -> [Self; 3] {
[Self::None, Self::Crt, Self::Hqx]
}
}

/// Video / windowing settings.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
Expand All @@ -133,6 +172,15 @@ pub struct VideoConfig {
pub pacing: PacingMode,
/// Integer-scale the framebuffer (true) or fit-to-window with aspect correction (false).
pub integer_scale: bool,
/// The active presentation post-filter (`v1.2.0`, default `None` — byte-identical to every
/// prior release when unchanged).
pub filter: PostFilter,
/// [`PostFilter::Crt`] scanline intensity, `0.0..=1.0` (0 = no scanlines).
pub crt_scanline: f32,
/// [`PostFilter::Crt`] RGB aperture-mask intensity, `0.0..=1.0` (0 = no mask).
pub crt_mask: f32,
/// [`PostFilter::Hqx`] edge-directed blend strength, `0.0..=1.0` (0 = plain bilinear).
pub hqx_strength: f32,
}

impl Default for VideoConfig {
Expand All @@ -141,6 +189,10 @@ impl Default for VideoConfig {
present_mode: "fifo".into(),
pacing: PacingMode::default(),
integer_scale: false,
filter: PostFilter::default(),
crt_scanline: 0.3,
crt_mask: 0.15,
hqx_strength: 0.6,
}
}
}
Expand Down
Loading