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’swgpu::PipelineCacheso 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)wherew/harequantized_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 — seequantized_target_key’s own doc for why 256px, the same quantumcrate::pool::TexturePoolalready uses for exactly this reason. - Fullscreen-triangle pass encoding (
ShaderEffects::encode_pass), confined bywgpu’s viewport to the requested sub-rect of the (possibly larger, quantized) target: the shader always seesfrust_u.resolutionas 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 forMAX_UNSEEN_FRAMESconsecutive 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_seencall’s other half: a(id, quantized w, quantized h)key absent from every frame’s live target key set forMAX_UNSEEN_TARGET_FRAMESconsecutive 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 byShaderEffects::ensure_pipeline): a rate-limitedlog::warn!once the live compiled-program count crossesCHURN_WARN_THRESHOLD— a detection aid pointing at theShaderProgram::newcache-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 isMAX_TARGETS_PER_ID× (the largest quantized target area it currently holds) × 4 bytes/texel, e.g. two 1024×1024Rgba8Unormtargets 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§
- Shader
Effects - 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_churncomparesShaderEffects::pipelines’s live size against. Crossing it is a detection signal — not itself a bound on growth (see below) — thatShaderProgram::newis likely being called somewhere that re-runs every frame/rebuild (a widget’spaint, or aComponent’sbuild) 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 onceMAX_UNSEEN_FRAMES-old entries start reaping — reliably crosses it within about a second. - MAX_
TARGETS_ PER_ ID - How many quantized target keys one program
idmay hold resident at once, enforced immediately byShaderEffects::ensure_targetthe moment a new key would pushidpast it — evictingid’s own least-recently-seen key ([oldest_target_key_for_id]) before minting the new one, rather than waiting forMAX_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 ofShaderEffects::ensure_target’s own map — may go absent from every frame’s live target-key set (seeShaderEffects::mark_seen) before its GPU resources are dropped on their own, independent of whether the programiditself 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/hquantized up to the next multiple ofcrate::pool::SIZE_QUANTUM(256px, viaquantize_extent) and never pastceiling(the caller’s own device-validity floor —ShaderEffects::ensure_targetpasses the device’s realmax_texture_dimension_2d). - should_
warn_ churn - Whether
ShaderEffects::ensure_pipelineshould log its churn-detection warning:compiled_ids(the live compiled-pipeline count) has crossedCHURN_WARN_THRESHOLDand fewer than this module’sMAX_CHURN_WARNINGShave already been logged — mirroringshould_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.