Skip to main content

Module effects

Module effects 

Source
Expand description

Offscreen WGSL fragment-shader effects: the GPU half of the shader-showcase feature.

A self-contained engine that renders user-supplied WGSL fragment shaders into offscreen Rgba8Unorm textures. It owns everything that job needs and nothing else:

  • Lazy per-program pipeline compilation (ShaderEffects::ensure_pipeline), seeded from the surface’s wgpu::PipelineCache so a persisted cache speeds first-frame compilation exactly like the renderer’s own pipelines.
  • Per-(program, quantized size) target state (ShaderEffects::ensure_target): an offscreen texture + view, a 16-byte uniform buffer, and the bind group binding them, keyed by (id, w, h) where w/h are quantized_target_key’s output rather than the raw request. Quantizing before the key means a quad resizing pixel-by-pixel (a window drag, an animated scale) reuses one texture across an entire 256px band instead of minting a fresh one every frame — see quantized_target_key’s own doc for why 256px, the same quantum crate::pool::TexturePool already uses for exactly this reason.
  • Fullscreen-triangle pass encoding (ShaderEffects::encode_pass), confined by wgpu’s viewport to the requested sub-rect of the (possibly larger, quantized) target: the shader always sees frust_u.resolution as the caller’s exact requested extent, never the quantized texture’s true size, so a quad samples back a target sized to itself even while sharing a texture with nearby sizes.
  • Age-based whole-id reap (ShaderEffects::mark_seen/ShaderEffects::reap): a program id absent from every frame’s live id set for MAX_UNSEEN_FRAMES consecutive frames has its pipeline, target(s), and any recorded compile failure dropped, rather than living until surface teardown.
  • Age-based per-target reap, the same ShaderEffects::mark_seen call’s other half: a (id, quantized w, quantized h) key absent from every frame’s live target key set for MAX_UNSEEN_TARGET_FRAMES consecutive frames — shorter than the whole-id window, since a size a still-drawn program has resized away from is a narrower, more frequent event than the program vanishing outright — is dropped on its own, independent of its id’s own age. Replaces an earlier same-frame-scoped eviction policy that reclaimed any other-size same-id target the moment a frame’s live key set no longer named it; that policy fought quantization directly, since two nearby (but not identical) requests that shared one quantized texture would otherwise evict each other every single frame.
  • Churn detection (should_warn_churn, consulted by ShaderEffects::ensure_pipeline): a rate-limited log::warn! once the live compiled-program count crosses CHURN_WARN_THRESHOLD — a detection aid pointing at the ShaderProgram::new cache-once contract, not itself a bound on growth; the age-based reap above is what actually bounds it.
  • Per-id target count bound (MAX_TARGETS_PER_ID): a program id may hold at most this many quantized target keys at once, evicting its own least-recently-seen key immediately — before the age-based reap above ever gets a turn — the moment a new key would push it past the cap. This bounds a single frame’s worth of accumulation (a multi-band resize drag minting one key per band per frame) independent of the age window: the accepted peak per program is MAX_TARGETS_PER_ID × (the largest quantized target area it currently holds) × 4 bytes/texel, e.g. two 1024×1024 Rgba8Unorm targets is 8 MiB, not the unbounded run of bands a fast drag could otherwise mint before any of them aged out.

It is deliberately decoupled from any scene vocabulary: the API speaks (id: u64, wgsl: &str, size, time) primitives, never a display list or a command, so the size-clamp and quad-placement policy that decides which (id, size) pairs a frame asks for lives in the layer above (frust_engine::effects::shader_quad), and only the GPU resources live here.

§Where the rendered target goes

ShaderEffects::target_view is the seam a renderer draws the result through: the target carries TEXTURE_BINDING, so the view it hands back is registered as a scene texture and sampled by the frame’s own passes. The texture itself is reachable through ShaderEffects::target_texture for a read-back — the whole (possibly larger, quantized) attachment, never only the sub-rect actually rendered into it. ShaderEffects::target_extent answers with that sub-rect instead: the requested, device-clamped extent actually rendered this call — the extent a registration must state — never the whole texture’s own (larger, quantized) extent, which is never itself exposed for registration. Nothing is registered with a foreign renderer and nothing is handed back on eviction: the pool owns the texture and a consumer borrows it.

§Target identity

A targets entry also carries a generation (ShaderEffects::target_generation): a per-key counter bumped every time ShaderEffects::ensure_target actually creates a new texture at that key, rather than reusing an existing one. A caller that only compares (id, requested extent) to decide whether to re-register cannot tell a steady-state target apart from one silently recreated behind it — the per-target reap above (or the per-id cap just above it) can drop and recreate a target at the identical requested extent between two frames, and a registration keyed on extent alone would then keep sampling the old (still-alive, now-orphaned) texture forever. Comparing generation too is what lets this crate’s caller (frust_engine::effects::shader_quad::ShaderQuadPass::register) tell the two cases apart and rebind whenever either the extent or the generation changed.

§Alpha

A target is Rgba8Unorm and the pass writes the fragment shader’s output into it unblended, so what the shader returns is what the target holds. The consumer samples that target as premultiplied colour, which is the convention every other engine paint travels in: a shader returning vec4(rgb, a) must have already multiplied rgb by a, and one returning opaque output (a = 1.0) is unaffected either way.

§Shader contract

The caller supplies only the fragment source. It is compiled after a fixed prelude (VERTEX_PRELUDE) that declares the uniform block and the fullscreen-triangle vertex stage, so a fragment shader:

  • defines the fragment entry point fs_main (@fragment fn fs_main(in: FrustVsOut) -> @location(0) vec4<f32>), and
  • reads the uniforms as frust_u.resolution / frust_u.time.

§No panics

Every path here upholds the FFI no-panic invariant (docs/CODE_STANDARDS.md): a shader that fails to compile is recorded in ShaderEffects::failed and skipped (with a rate-limited log::warn!) rather than panicking; the underlying validation error also surfaces through the device’s latched uncaptured-error handler (crate::context::create_device).

Structs§

ShaderEffects
Owns every GPU resource the shader-showcase feature needs: lazily-compiled per-program pipelines and per-(program, quantized size) offscreen targets.

Constants§

CHURN_WARN_THRESHOLD
Distinct-compiled-program-id threshold should_warn_churn compares ShaderEffects::pipelines’s live size against. Crossing it is a detection signal — not itself a bound on growth (see below) — that ShaderProgram::new is likely being called somewhere that re-runs every frame/rebuild (a widget’s paint, or a Component’s build) instead of once, per its documented cache-once contract (frust_scene::ShaderProgram::new’s rustdoc). 32 is chosen with generous headroom over the shader-showcase’s own program count (a handful of fixed shaders) so a legitimate small gallery never trips it, while a per-frame-minting footgun — which compiles a fresh id on every frame and only stops accumulating once MAX_UNSEEN_FRAMES-old entries start reaping — reliably crosses it within about a second.
MAX_TARGETS_PER_ID
How many quantized target keys one program id may hold resident at once, enforced immediately by ShaderEffects::ensure_target the moment a new key would push id past it — evicting id’s own least-recently-seen key ([oldest_target_key_for_id]) before minting the new one, rather than waiting for MAX_UNSEEN_TARGET_FRAMES’s age-based reap to catch up.
MAX_UNSEEN_FRAMES
How many consecutive frames a program id may go unseen (absent from every frame’s live key set — see ShaderEffects::mark_seen) before its compiled pipeline, offscreen target(s), and any recorded compile failure are reaped (ShaderEffects::reap) rather than living until surface teardown. ~120 frames is roughly 1-2s at a 60-120Hz refresh rate: generous enough that a shader drawn intermittently (every-other-frame, or through a brief scene-diff hiccup) is never mistaken for vanished, short enough that navigating away from a shader-showcase screen reclaims its GPU state promptly instead of leaking for the rest of the session.
MAX_UNSEEN_TARGET_FRAMES
How many consecutive frames a target key — (program id, quantized width, quantized height), one entry of ShaderEffects::ensure_target’s own map — may go absent from every frame’s live target-key set (see ShaderEffects::mark_seen) before its GPU resources are dropped on their own, independent of whether the program id itself is still being drawn.
VERTEX_PRELUDE
The fixed WGSL prelude prepended to every fragment source: the 16-byte uniform block at @group(0) @binding(0) and the vertex-buffer-free fullscreen-triangle vertex stage.

Functions§

quantized_target_key
The (width, height) a shader-effect target requested at (w, h) is actually keyed and created at: w/h quantized up to the next multiple of crate::pool::SIZE_QUANTUM (256px, via quantize_extent) and never past ceiling (the caller’s own device-validity floor — ShaderEffects::ensure_target passes the device’s real max_texture_dimension_2d).
should_warn_churn
Whether ShaderEffects::ensure_pipeline should log its churn-detection warning: compiled_ids (the live compiled-pipeline count) has crossed CHURN_WARN_THRESHOLD and fewer than this module’s MAX_CHURN_WARNINGS have already been logged — mirroring should_warn_failure’s shape for a distinct signal. Pure: no GPU/log state touched, so the rate-limited decision is unit-testable on its own.