use std::collections::{HashMap, HashSet};
use crate::pool::{DEFAULT_MAX_UNUSED_FRAMES, quantize_extent};
const UNIFORM_SIZE: u64 = 16;
const MAX_FAILED_WARNINGS: u32 = 8;
pub const MAX_UNSEEN_FRAMES: u64 = 120;
pub const MAX_UNSEEN_TARGET_FRAMES: u64 = DEFAULT_MAX_UNUSED_FRAMES;
pub const MAX_TARGETS_PER_ID: usize = 2;
#[must_use]
pub fn quantized_target_key(w: u32, h: u32, ceiling: u32) -> (u32, u32) {
let ceiling = ceiling.max(1);
let (quantized_w, quantized_h) = quantize_extent(w.max(1), h.max(1), ceiling);
(quantized_w.min(ceiling), quantized_h.min(ceiling))
}
pub const CHURN_WARN_THRESHOLD: usize = 32;
const MAX_CHURN_WARNINGS: u32 = 8;
pub const VERTEX_PRELUDE: &str = r#"// --- frust shader-effect prelude (generated) ---
struct Uniforms {
resolution: vec2<f32>,
time: f32,
_pad: f32,
}
@group(0) @binding(0) var<uniform> frust_u: Uniforms;
struct FrustVsOut {
@builtin(position) position: vec4<f32>,
@location(0) uv: vec2<f32>,
}
// Fullscreen triangle from the vertex index alone — no vertex buffer.
// Vertices 0,1,2 produce uv (0,0),(2,0),(0,2), covering the [-1,1] clip square
// with a single oversized triangle.
@vertex
fn vs_main(@builtin(vertex_index) i: u32) -> FrustVsOut {
var out: FrustVsOut;
let uv = vec2<f32>(f32((i << 1u) & 2u), f32(i & 2u));
out.uv = uv;
out.position = vec4<f32>(uv * 2.0 - 1.0, 0.0, 1.0);
return out;
}
// --- end prelude ---
"#;
struct PipelineEntry {
pipeline: wgpu::RenderPipeline,
bind_layout: wgpu::BindGroupLayout,
}
struct TargetEntry {
texture: wgpu::Texture,
view: wgpu::TextureView,
uniforms: wgpu::Buffer,
bind_group: wgpu::BindGroup,
used_w: u32,
used_h: u32,
generation: u64,
}
pub struct ShaderEffects {
pipelines: HashMap<u64, PipelineEntry>,
targets: HashMap<(u64, u32, u32), TargetEntry>,
pipeline_cache: Option<wgpu::PipelineCache>,
failed: HashSet<u64>,
warned_failures: u32,
warned_churn: u32,
frame: u64,
last_seen: HashMap<u64, u64>,
target_last_seen: HashMap<(u64, u32, u32), u64>,
warned_clamped: HashSet<(u64, u32, u32)>,
next_generation: u64,
}
fn uniform_bytes(width: u32, height: u32, time: f32) -> [u8; UNIFORM_SIZE as usize] {
let mut bytes = [0u8; UNIFORM_SIZE as usize];
bytes[0..4].copy_from_slice(&(width as f32).to_le_bytes());
bytes[4..8].copy_from_slice(&(height as f32).to_le_bytes());
bytes[8..12].copy_from_slice(&time.to_le_bytes());
bytes
}
fn compose_shader(fragment_src: &str) -> String {
format!("{VERTEX_PRELUDE}\n{fragment_src}")
}
fn reapable_target_ages(
target_last_seen: &HashMap<(u64, u32, u32), u64>,
current_frame: u64,
max_age: u64,
) -> Vec<(u64, u32, u32)> {
target_last_seen
.iter()
.filter(|&(_, &seen)| current_frame.saturating_sub(seen) >= max_age)
.map(|(&key, _)| key)
.collect()
}
fn should_warn_failure(prior_warn_count: u32) -> bool {
prior_warn_count < MAX_FAILED_WARNINGS
}
pub fn should_warn_churn(compiled_ids: usize, prior_warn_count: u32) -> bool {
compiled_ids > CHURN_WARN_THRESHOLD && prior_warn_count < MAX_CHURN_WARNINGS
}
fn reapable_ids(last_seen: &HashMap<u64, u64>, current_frame: u64, max_age: u64) -> Vec<u64> {
last_seen
.iter()
.filter(|&(_, &seen)| current_frame.saturating_sub(seen) >= max_age)
.map(|(&id, _)| id)
.collect()
}
fn reapable_target_keys<I>(target_keys: I, stale_ids: &HashSet<u64>) -> Vec<(u64, u32, u32)>
where
I: IntoIterator<Item = (u64, u32, u32)>,
{
target_keys
.into_iter()
.filter(|key| stale_ids.contains(&key.0))
.collect()
}
fn oldest_target_key_for_id(
target_last_seen: &HashMap<(u64, u32, u32), u64>,
id: u64,
) -> Option<(u64, u32, u32)> {
target_last_seen
.iter()
.filter(|&(&(key_id, _, _), _)| key_id == id)
.min_by_key(|&(_, &seen)| seen)
.map(|(&key, _)| key)
}
impl ShaderEffects {
pub fn new(pipeline_cache: Option<wgpu::PipelineCache>) -> Self {
Self {
pipelines: HashMap::new(),
targets: HashMap::new(),
pipeline_cache,
failed: HashSet::new(),
warned_failures: 0,
warned_churn: 0,
frame: 0,
last_seen: HashMap::new(),
target_last_seen: HashMap::new(),
warned_clamped: HashSet::new(),
next_generation: 0,
}
}
pub fn needs_compile(&self, id: u64) -> bool {
!self.pipelines.contains_key(&id) && !self.failed.contains(&id)
}
pub fn ensure_pipeline(&mut self, device: &wgpu::Device, id: u64, wgsl: &str) {
if !self.needs_compile(id) {
return;
}
let source = compose_shader(wgsl);
let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("frust shader-effect module"),
source: wgpu::ShaderSource::Wgsl(source.into()),
});
let bind_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("frust shader-effect binds"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("frust shader-effect layout"),
bind_group_layouts: &[Some(&bind_layout)],
immediate_size: 0,
});
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("frust shader-effect pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &module,
entry_point: Some("vs_main"),
compilation_options: Default::default(),
buffers: &[],
},
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
fragment: Some(wgpu::FragmentState {
module: &module,
entry_point: Some("fs_main"),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format: wgpu::TextureFormat::Rgba8Unorm,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
multiview_mask: None,
cache: self.pipeline_cache.as_ref(),
});
if let Some(error) = drain_error_scope(device, scope) {
self.record_failed(id, &error.to_string());
return;
}
self.pipelines.insert(
id,
PipelineEntry {
pipeline,
bind_layout,
},
);
if should_warn_churn(self.pipelines.len(), self.warned_churn) {
self.warned_churn += 1;
log::warn!(
"frust-gpu: {} distinct shader programs are concurrently compiled for this \
surface (threshold {CHURN_WARN_THRESHOLD}) — if new ShaderProgram instances are \
being minted every frame/rebuild instead of created once and cached (see \
ShaderProgram::new's cache-once contract), this is why; this warning is \
detection-only and does not itself bound the growth",
self.pipelines.len(),
);
}
}
fn record_failed(&mut self, id: u64, message: &str) {
self.failed.insert(id);
if should_warn_failure(self.warned_failures) {
self.warned_failures += 1;
log::warn!("frust-gpu: shader program {id} failed to compile, skipping: {message}");
}
}
pub fn mark_seen(
&mut self,
live_ids: &HashSet<u64>,
live_target_keys: &HashSet<(u64, u32, u32)>,
) -> Vec<u64> {
self.frame += 1;
for &id in live_ids {
self.last_seen.insert(id, self.frame);
}
for &key in live_target_keys {
self.target_last_seen.insert(key, self.frame);
}
for key in
reapable_target_ages(&self.target_last_seen, self.frame, MAX_UNSEEN_TARGET_FRAMES)
{
self.targets.remove(&key);
self.target_last_seen.remove(&key);
}
reapable_ids(&self.last_seen, self.frame, MAX_UNSEEN_FRAMES)
}
pub fn reap(&mut self, stale_ids: &[u64]) {
let stale_id_set: HashSet<u64> = stale_ids.iter().copied().collect();
for key in reapable_target_keys(self.targets.keys().copied(), &stale_id_set) {
self.targets.remove(&key);
}
self.target_last_seen
.retain(|key, _| !stale_id_set.contains(&key.0));
self.warned_clamped
.retain(|key| !stale_id_set.contains(&key.0));
for &id in stale_ids {
self.pipelines.remove(&id);
self.failed.remove(&id);
self.last_seen.remove(&id);
}
}
pub fn ensure_target(&mut self, device: &wgpu::Device, id: u64, w: u32, h: u32) {
let (used_w, used_h, ceiling) = Self::target_key(device, w, h);
let key = (id, used_w, used_h);
if self.targets.contains_key(&key) {
return;
}
let Some(pipeline_entry) = self.pipelines.get(&id) else {
return;
};
if (w > ceiling || h > ceiling) && self.warned_clamped.insert((id, w, h)) {
log::warn!(
"frust-gpu: shader-effect target {id} requested {w}x{h}, clamped to \
{used_w}x{used_h} (the device's max_texture_dimension_2d ceiling {ceiling})",
);
}
let resident_for_id = self.targets.keys().filter(|k| k.0 == id).count();
if resident_for_id >= MAX_TARGETS_PER_ID
&& let Some(evict_key) = oldest_target_key_for_id(&self.target_last_seen, id)
{
self.targets.remove(&evict_key);
self.target_last_seen.remove(&evict_key);
}
let generation = self.next_generation;
self.next_generation += 1;
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("frust shader-effect target"),
size: wgpu::Extent3d {
width: used_w,
height: used_h,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let uniforms = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("frust shader-effect uniforms"),
size: UNIFORM_SIZE,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("frust shader-effect bind group"),
layout: &pipeline_entry.bind_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: uniforms.as_entire_binding(),
}],
});
self.targets.insert(
key,
TargetEntry {
texture,
view,
uniforms,
bind_group,
used_w,
used_h,
generation,
},
);
self.target_last_seen.insert(key, self.frame);
}
pub fn encode_pass(
&self,
encoder: &mut wgpu::CommandEncoder,
queue: &wgpu::Queue,
device: &wgpu::Device,
id: u64,
requested: (u32, u32),
time: f32,
) {
let (req_w, req_h) = (requested.0.max(1), requested.1.max(1));
let (used_w, used_h, _ceiling) = Self::target_key(device, req_w, req_h);
let (Some(pipeline_entry), Some(target)) = (
self.pipelines.get(&id),
self.targets.get(&(id, used_w, used_h)),
) else {
return;
};
let render_w = req_w.min(target.used_w);
let render_h = req_h.min(target.used_h);
queue.write_buffer(
&target.uniforms,
0,
&uniform_bytes(render_w, render_h, time),
);
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("frust shader-effect pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &target.view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_pipeline(&pipeline_entry.pipeline);
pass.set_bind_group(0, &target.bind_group, &[]);
pass.set_viewport(0.0, 0.0, render_w as f32, render_h as f32, 0.0, 1.0);
pass.draw(0..3, 0..1);
}
pub fn target_texture(
&self,
device: &wgpu::Device,
id: u64,
w: u32,
h: u32,
) -> Option<&wgpu::Texture> {
let (used_w, used_h, _ceiling) = Self::target_key(device, w, h);
self.targets
.get(&(id, used_w, used_h))
.map(|entry| &entry.texture)
}
pub fn target_view(
&self,
device: &wgpu::Device,
id: u64,
w: u32,
h: u32,
) -> Option<&wgpu::TextureView> {
let (used_w, used_h, _ceiling) = Self::target_key(device, w, h);
self.targets
.get(&(id, used_w, used_h))
.map(|entry| &entry.view)
}
pub fn target_extent(
&self,
device: &wgpu::Device,
id: u64,
w: u32,
h: u32,
) -> Option<(u32, u32)> {
let (used_w, used_h, _ceiling) = Self::target_key(device, w, h);
let target = self.targets.get(&(id, used_w, used_h))?;
Some((w.max(1).min(target.used_w), h.max(1).min(target.used_h)))
}
#[must_use]
pub fn target_generation(&self, device: &wgpu::Device, id: u64, w: u32, h: u32) -> Option<u64> {
let (used_w, used_h, _ceiling) = Self::target_key(device, w, h);
self.targets
.get(&(id, used_w, used_h))
.map(|entry| entry.generation)
}
fn target_key(device: &wgpu::Device, w: u32, h: u32) -> (u32, u32, u32) {
let ceiling = device.limits().max_texture_dimension_2d;
let (used_w, used_h) = quantized_target_key(w, h, ceiling);
(used_w, used_h, ceiling)
}
}
fn drain_error_scope(device: &wgpu::Device, scope: wgpu::ErrorScopeGuard) -> Option<wgpu::Error> {
use std::task::{Context, Poll, Waker};
let waker = Waker::noop();
let mut cx = Context::from_waker(waker);
let mut future = std::pin::pin!(scope.pop());
loop {
match future.as_mut().poll(&mut cx) {
Poll::Ready(error) => return error,
Poll::Pending => {
let _ = device.poll(wgpu::PollType::wait_indefinitely());
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn uniform_bytes_is_16_and_little_endian() {
let bytes = uniform_bytes(1920, 1080, 2.5);
assert_eq!(bytes.len(), 16);
assert_eq!(f32::from_le_bytes(bytes[0..4].try_into().unwrap()), 1920.0);
assert_eq!(f32::from_le_bytes(bytes[4..8].try_into().unwrap()), 1080.0);
assert_eq!(f32::from_le_bytes(bytes[8..12].try_into().unwrap()), 2.5);
assert_eq!(&bytes[12..16], &[0, 0, 0, 0]);
}
#[test]
fn compose_shader_prepends_prelude_and_keeps_fragment() {
let frag = "@fragment fn fs_main(in: FrustVsOut) -> @location(0) vec4<f32> \
{ return vec4<f32>(frust_u.time, 0.0, 0.0, 1.0); }";
let full = compose_shader(frag);
assert!(full.starts_with(VERTEX_PRELUDE));
assert!(full.contains("fn vs_main"));
assert!(full.contains("var<uniform> frust_u: Uniforms"));
assert!(full.contains(frag));
assert!(full.find("fn vs_main").unwrap() < full.find(frag).unwrap());
}
#[test]
fn needs_compile_tracks_pipeline_and_failed_sets() {
let mut fx = ShaderEffects::new(None);
assert!(fx.needs_compile(7));
fx.record_failed(7, "boom");
assert!(!fx.needs_compile(7));
assert!(fx.failed.contains(&7));
assert!(fx.needs_compile(8));
}
#[test]
fn record_failed_warns_only_up_to_the_rate_limit() {
let mut fx = ShaderEffects::new(None);
for id in 0..(MAX_FAILED_WARNINGS + 5) as u64 {
fx.record_failed(id, "bad shader");
}
assert_eq!(fx.warned_failures, MAX_FAILED_WARNINGS);
assert_eq!(fx.failed.len(), (MAX_FAILED_WARNINGS + 5) as usize);
}
#[test]
fn should_warn_failure_stops_at_cap() {
assert!(should_warn_failure(0));
assert!(should_warn_failure(MAX_FAILED_WARNINGS - 1));
assert!(!should_warn_failure(MAX_FAILED_WARNINGS));
assert!(!should_warn_failure(MAX_FAILED_WARNINGS + 1));
}
#[test]
fn should_warn_churn_fires_only_past_the_distinct_id_threshold() {
assert!(
!should_warn_churn(CHURN_WARN_THRESHOLD, 0),
"at threshold, not over it, must not warn"
);
assert!(should_warn_churn(CHURN_WARN_THRESHOLD + 1, 0));
assert!(should_warn_churn(CHURN_WARN_THRESHOLD + 100, 0));
assert!(
!should_warn_churn(0, 0),
"well under threshold must never warn"
);
}
#[test]
fn should_warn_churn_stops_at_the_rate_limit_cap() {
assert!(should_warn_churn(CHURN_WARN_THRESHOLD + 1, 0));
assert!(should_warn_churn(
CHURN_WARN_THRESHOLD + 1,
MAX_CHURN_WARNINGS - 1
));
assert!(!should_warn_churn(
CHURN_WARN_THRESHOLD + 1,
MAX_CHURN_WARNINGS
));
assert!(!should_warn_churn(
CHURN_WARN_THRESHOLD + 1,
MAX_CHURN_WARNINGS + 1
));
}
#[test]
fn ensure_pipeline_never_warns_below_the_churn_threshold() {
for compiled in 0..=CHURN_WARN_THRESHOLD {
assert!(
!should_warn_churn(compiled, 0),
"compiled={compiled} must not warn (at or under threshold)"
);
}
}
#[test]
fn quantized_target_key_rounds_up_to_the_256px_quantum() {
assert_eq!(quantized_target_key(1, 1, 8192), (256, 256));
assert_eq!(quantized_target_key(256, 256, 8192), (256, 256));
assert_eq!(quantized_target_key(257, 4, 8192), (512, 256));
assert_eq!(quantized_target_key(800, 300, 8192), (1024, 512));
}
#[test]
fn quantized_target_key_never_exceeds_the_ceiling() {
assert_eq!(
quantized_target_key(10_000, 10_000, 2048),
(2048, 2048),
"an already-oversized request is clamped to the ceiling here, unlike quantize_extent alone"
);
assert_eq!(quantized_target_key(2000, 2000, 2000), (2000, 2000));
}
#[test]
fn quantized_target_key_floors_zero_to_one_quantum() {
assert_eq!(quantized_target_key(0, 0, 8192), (256, 256));
}
#[test]
fn quantized_target_key_survives_a_degenerate_ceiling() {
assert_eq!(quantized_target_key(100, 100, 0), (1, 1));
}
#[test]
fn quantized_target_key_collapses_a_resize_drag_onto_one_key() {
let mut keys: Vec<(u32, u32)> = (1..=256u32)
.map(|w| quantized_target_key(w, w, 8192))
.collect();
keys.dedup();
assert_eq!(keys, vec![(256, 256)]);
}
#[test]
fn reapable_target_ages_selects_only_keys_unseen_for_at_least_max_age() {
let target_last_seen: HashMap<(u64, u32, u32), u64> = [
((1, 256, 256), 0),
((2, 256, 256), 50),
((3, 256, 256), 100),
]
.into_iter()
.collect();
let mut stale = reapable_target_ages(&target_last_seen, 120, 60);
stale.sort();
assert_eq!(stale, vec![(1, 256, 256), (2, 256, 256)]);
}
#[test]
fn reapable_target_ages_boundary_is_inclusive() {
let target_last_seen: HashMap<(u64, u32, u32), u64> =
[((1, 256, 256), 0)].into_iter().collect();
assert!(reapable_target_ages(&target_last_seen, 60, 60).contains(&(1, 256, 256)));
assert!(!reapable_target_ages(&target_last_seen, 59, 60).contains(&(1, 256, 256)));
}
#[test]
fn reapable_target_ages_empty_when_every_key_seen_this_frame() {
let mut target_last_seen: HashMap<(u64, u32, u32), u64> = HashMap::new();
for frame in 1..=(MAX_UNSEEN_TARGET_FRAMES * 3) {
target_last_seen.insert((1, 256, 256), frame);
assert!(
reapable_target_ages(&target_last_seen, frame, MAX_UNSEEN_TARGET_FRAMES).is_empty()
);
}
}
#[test]
fn reapable_ids_selects_only_ids_unseen_for_at_least_max_age() {
let last_seen: HashMap<u64, u64> = [(1, 0), (2, 50), (3, 100)].into_iter().collect();
let mut stale = reapable_ids(&last_seen, 120, 60);
stale.sort();
assert_eq!(stale, vec![1, 2]);
}
#[test]
fn reapable_ids_boundary_is_inclusive() {
let last_seen: HashMap<u64, u64> = [(1, 0)].into_iter().collect();
assert!(reapable_ids(&last_seen, 60, 60).contains(&1));
assert!(!reapable_ids(&last_seen, 59, 60).contains(&1));
}
#[test]
fn reapable_ids_empty_when_every_id_seen_this_frame() {
let mut last_seen: HashMap<u64, u64> = HashMap::new();
for frame in 1..=(MAX_UNSEEN_FRAMES * 3) {
last_seen.insert(1, frame);
assert!(reapable_ids(&last_seen, frame, MAX_UNSEEN_FRAMES).is_empty());
}
}
#[test]
fn reapable_target_keys_selects_only_matching_stale_ids() {
let keys = [(1, 100, 100), (1, 200, 200), (2, 100, 100), (3, 50, 50)];
let stale_ids: HashSet<u64> = [1, 3].into_iter().collect();
let mut got = reapable_target_keys(keys.iter().copied(), &stale_ids);
got.sort();
assert_eq!(got, vec![(1, 100, 100), (1, 200, 200), (3, 50, 50)]);
}
#[test]
fn reapable_target_keys_empty_for_no_stale_ids() {
let keys = [(1, 100, 100)];
let stale_ids: HashSet<u64> = HashSet::new();
assert!(reapable_target_keys(keys.iter().copied(), &stale_ids).is_empty());
}
#[test]
fn oldest_target_key_for_id_picks_the_least_recently_seen_of_that_id_alone() {
let target_last_seen: HashMap<(u64, u32, u32), u64> = [
((1, 256, 256), 10),
((1, 512, 512), 5),
((1, 768, 768), 20),
((2, 256, 256), 0),
]
.into_iter()
.collect();
assert_eq!(
oldest_target_key_for_id(&target_last_seen, 1),
Some((1, 512, 512))
);
}
#[test]
fn oldest_target_key_for_id_none_when_the_id_holds_nothing() {
let target_last_seen: HashMap<(u64, u32, u32), u64> =
[((2, 256, 256), 0)].into_iter().collect();
assert_eq!(oldest_target_key_for_id(&target_last_seen, 1), None);
}
#[test]
fn a_sweep_across_five_bands_leaves_at_most_the_cap_resident_for_one_id() {
let id = 1u64;
let mut resident: HashSet<(u64, u32, u32)> = HashSet::new();
let mut target_last_seen: HashMap<(u64, u32, u32), u64> = HashMap::new();
for band in 0..5u32 {
let key = (id, band, band);
if resident.len() >= MAX_TARGETS_PER_ID
&& let Some(evict) = oldest_target_key_for_id(&target_last_seen, id)
{
resident.remove(&evict);
target_last_seen.remove(&evict);
}
resident.insert(key);
target_last_seen.insert(key, u64::from(band));
assert!(
resident.len() <= MAX_TARGETS_PER_ID,
"band {band}: must never exceed the per-id cap"
);
}
assert_eq!(
resident.len(),
MAX_TARGETS_PER_ID,
"exactly the cap remains resident after the sweep"
);
assert!(resident.contains(&(id, 3, 3)));
assert!(resident.contains(&(id, 4, 4)));
}
#[test]
fn mark_seen_bumps_frame_and_reaps_ids_unseen_past_max_age() {
let mut fx = ShaderEffects::new(None);
assert!(
fx.mark_seen(&[1].into_iter().collect(), &HashSet::new())
.is_empty()
);
for _ in 0..(MAX_UNSEEN_FRAMES - 1) {
assert!(fx.mark_seen(&HashSet::new(), &HashSet::new()).is_empty());
}
let reapable = fx.mark_seen(&HashSet::new(), &HashSet::new());
assert_eq!(reapable, vec![1]);
}
#[test]
fn mark_seen_never_reaps_an_id_kept_live_every_frame() {
let mut fx = ShaderEffects::new(None);
for _ in 0..(MAX_UNSEEN_FRAMES * 2) {
assert!(
fx.mark_seen(&[1].into_iter().collect(), &HashSet::new())
.is_empty()
);
}
}
#[test]
fn mark_seen_ages_a_target_key_on_its_own_shorter_clock_than_the_id() {
let mut fx = ShaderEffects::new(None);
let ids: HashSet<u64> = [1].into_iter().collect();
let key = (1u64, 256u32, 256u32);
fx.mark_seen(&ids, &[key].into_iter().collect());
assert!(fx.target_last_seen.contains_key(&key));
for _ in 0..(MAX_UNSEEN_TARGET_FRAMES - 1) {
fx.mark_seen(&ids, &HashSet::new());
assert!(
fx.target_last_seen.contains_key(&key),
"must not age out before its own window elapses"
);
}
fx.mark_seen(&ids, &HashSet::new());
assert!(
!fx.target_last_seen.contains_key(&key),
"ages out on its own clock even though id 1 is still live every frame"
);
}
#[test]
fn mark_seen_keeps_two_target_sizes_of_one_id_live_in_the_same_frame() {
let mut fx = ShaderEffects::new(None);
let ids: HashSet<u64> = [1].into_iter().collect();
let keys: HashSet<(u64, u32, u32)> = [(1, 256, 256), (1, 512, 512)].into_iter().collect();
fx.mark_seen(&ids, &keys);
assert!(fx.target_last_seen.contains_key(&(1, 256, 256)));
assert!(fx.target_last_seen.contains_key(&(1, 512, 512)));
}
#[test]
fn reap_clears_failed_and_last_seen_so_a_redrawn_id_recompiles_cleanly() {
let mut fx = ShaderEffects::new(None);
fx.record_failed(1, "boom");
fx.last_seen.insert(1, 3);
assert!(!fx.needs_compile(1), "a failed id is skipped, not retried");
fx.reap(&[1]);
assert!(!fx.failed.contains(&1));
assert!(!fx.last_seen.contains_key(&1));
assert!(
fx.needs_compile(1),
"a reaped id must be eligible to recompile cleanly, not stuck in `failed`"
);
}
#[test]
fn reap_only_touches_the_stale_ids_given() {
let mut fx = ShaderEffects::new(None);
fx.record_failed(1, "boom");
fx.record_failed(2, "boom");
fx.last_seen.insert(1, 1);
fx.last_seen.insert(2, 1);
fx.reap(&[1]);
assert!(!fx.failed.contains(&1));
assert!(
fx.failed.contains(&2),
"id 2 was not in stale_ids, must survive"
);
assert!(!fx.last_seen.contains_key(&1));
assert!(fx.last_seen.contains_key(&2));
}
#[test]
fn reap_drops_the_stale_ids_target_last_seen_rows_but_not_others() {
let mut fx = ShaderEffects::new(None);
fx.target_last_seen.insert((1, 256, 256), 5);
fx.target_last_seen.insert((2, 256, 256), 5);
fx.reap(&[1]);
assert!(!fx.target_last_seen.contains_key(&(1, 256, 256)));
assert!(fx.target_last_seen.contains_key(&(2, 256, 256)));
}
#[test]
#[ignore = "requires a GPU; run locally with `cargo test -p frust-gpu -- --ignored`"]
fn reap_drops_a_vanished_ids_real_pipeline_and_target() {
pollster::block_on(run());
async fn run() {
let instance = wgpu::Instance::new(
wgpu::InstanceDescriptor::new_without_display_handle_from_env(),
);
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions::default())
.await
.expect("no compatible GPU adapter");
let (device, _queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("frust-gpu effects reap test"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
..Default::default()
})
.await
.expect("failed to create device");
const FRAGMENT: &str = "@fragment fn fs_main(in: FrustVsOut) -> \
@location(0) vec4<f32> { return vec4<f32>(frust_u.time, 0.0, 0.0, 1.0); }";
let mut fx = ShaderEffects::new(None);
assert!(fx.target_texture(&device, 1, 4, 4).is_none());
assert!(
fx.mark_seen(&[1].into_iter().collect(), &HashSet::new())
.is_empty()
);
fx.ensure_pipeline(&device, 1, FRAGMENT);
fx.ensure_target(&device, 1, 4, 4);
assert!(!fx.needs_compile(1), "compile must have succeeded");
assert!(fx.pipelines.contains_key(&1));
assert!(fx.target_texture(&device, 1, 4, 4).is_some());
let mut reapable = Vec::new();
for _ in 0..MAX_UNSEEN_FRAMES {
reapable = fx.mark_seen(&HashSet::new(), &HashSet::new());
}
assert_eq!(reapable, vec![1]);
fx.reap(&reapable);
assert!(
!fx.pipelines.contains_key(&1),
"reap must drop the real compiled pipeline"
);
assert!(
fx.target_texture(&device, 1, 4, 4).is_none(),
"reap must drop the real offscreen target"
);
assert!(fx.needs_compile(1));
assert!(
fx.mark_seen(&[1].into_iter().collect(), &HashSet::new())
.is_empty()
);
fx.ensure_pipeline(&device, 1, FRAGMENT);
assert!(
!fx.needs_compile(1),
"a reaped id must recompile cleanly when redrawn"
);
assert!(fx.pipelines.contains_key(&1));
}
}
#[test]
#[ignore = "requires a GPU; run locally with `cargo test -p frust-gpu -- --ignored`"]
fn a_resize_within_one_quantum_reuses_the_same_target_and_ages_out_once_unseen() {
pollster::block_on(run());
async fn run() {
let instance = wgpu::Instance::new(
wgpu::InstanceDescriptor::new_without_display_handle_from_env(),
);
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions::default())
.await
.expect("no compatible GPU adapter");
let (device, _queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("frust-gpu effects quantization test"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
..Default::default()
})
.await
.expect("failed to create device");
const FRAGMENT: &str = "@fragment fn fs_main(in: FrustVsOut) -> \
@location(0) vec4<f32> { return vec4<f32>(frust_u.time, 0.0, 0.0, 1.0); }";
let mut fx = ShaderEffects::new(None);
fx.ensure_pipeline(&device, 1, FRAGMENT);
fx.ensure_target(&device, 1, 4, 4);
let first = fx.target_texture(&device, 1, 4, 4).expect("created") as *const _;
for size in [5u32, 64, 128, 200, 250] {
fx.ensure_target(&device, 1, size, size);
let texture = fx.target_texture(&device, 1, size, size).expect("reused");
assert_eq!(
texture as *const _, first,
"size {size} must reuse the 256x256 target"
);
}
fx.ensure_target(&device, 1, 300, 300);
let second = fx
.target_texture(&device, 1, 300, 300)
.expect("created past the quantum");
assert_ne!(
second as *const _, first,
"a quantum crossing must create a new target"
);
let live_large: HashSet<(u64, u32, u32)> =
[(1u64, 512u32, 512u32)].into_iter().collect();
for _ in 0..MAX_UNSEEN_TARGET_FRAMES {
fx.mark_seen(&[1].into_iter().collect(), &live_large);
}
assert!(
fx.target_texture(&device, 1, 4, 4).is_none(),
"the small target ages out once unseen"
);
assert!(
fx.target_texture(&device, 1, 300, 300).is_some(),
"the still-live size survives"
);
assert!(
fx.pipelines.contains_key(&1),
"the id itself is untouched by target-level aging"
);
}
}
#[test]
#[ignore = "requires a GPU; run locally with `cargo test -p frust-gpu -- --ignored`"]
fn ensure_target_clamps_an_oversized_request_to_the_device_ceiling() {
pollster::block_on(run());
async fn run() {
let instance = wgpu::Instance::new(
wgpu::InstanceDescriptor::new_without_display_handle_from_env(),
);
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions::default())
.await
.expect("no compatible GPU adapter");
const CEILING: u32 = 256;
let (device, _queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("frust-gpu effects ensure_target clamp test"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits {
max_texture_dimension_2d: CEILING,
..wgpu::Limits::default()
},
..Default::default()
})
.await
.expect("failed to create device");
assert_eq!(device.limits().max_texture_dimension_2d, CEILING);
const FRAGMENT: &str = "@fragment fn fs_main(in: FrustVsOut) -> \
@location(0) vec4<f32> { return vec4<f32>(frust_u.time, 0.0, 0.0, 1.0); }";
let mut fx = ShaderEffects::new(None);
fx.ensure_pipeline(&device, 1, FRAGMENT);
assert!(!fx.needs_compile(1), "compile must have succeeded");
let requested = CEILING * 4;
fx.ensure_target(&device, 1, requested, requested);
let texture = fx
.target_texture(&device, 1, requested, requested)
.expect("an oversized request must still create a (clamped) target");
assert_eq!(texture.width(), CEILING);
assert_eq!(texture.height(), CEILING);
}
}
#[test]
#[ignore = "requires a GPU; run locally with `cargo test -p frust-gpu -- --ignored`"]
fn ensure_target_extent_coherence_oversized_request() {
pollster::block_on(run());
async fn run() {
let instance = wgpu::Instance::new(
wgpu::InstanceDescriptor::new_without_display_handle_from_env(),
);
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions::default())
.await
.expect("no compatible GPU adapter");
const CEILING: u32 = 512;
let (device, _queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("frust-gpu effects extent coherence test"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits {
max_texture_dimension_2d: CEILING,
..wgpu::Limits::default()
},
..Default::default()
})
.await
.expect("failed to create device");
assert_eq!(device.limits().max_texture_dimension_2d, CEILING);
const FRAGMENT: &str = "@fragment fn fs_main(in: FrustVsOut) -> \
@location(0) vec4<f32> { return vec4<f32>(frust_u.time, 0.0, 0.0, 1.0); }";
let mut fx = ShaderEffects::new(None);
fx.ensure_pipeline(&device, 1, FRAGMENT);
assert!(!fx.needs_compile(1), "compile must have succeeded");
let requested = CEILING * 4;
assert!(requested > CEILING, "sanity check: request is oversized");
let quantized_key = (1u64, CEILING, CEILING);
fx.ensure_target(&device, 1, requested, requested);
let entry_1 = fx
.targets
.get(&quantized_key)
.expect("target must be created");
assert_eq!(entry_1.used_w, CEILING, "used_w must match ceiling");
assert_eq!(entry_1.used_h, CEILING, "used_h must match ceiling");
let texture_1 = fx
.target_texture(&device, 1, requested, requested)
.expect("texture must exist");
assert_eq!(
texture_1.width(),
CEILING,
"texture width must match clamped extent"
);
assert_eq!(
texture_1.height(),
CEILING,
"texture height must match clamped extent"
);
let texture_ptr_1 = texture_1 as *const _;
fx.ensure_target(&device, 1, requested, requested);
let texture_2 = fx
.target_texture(&device, 1, requested, requested)
.expect("texture must still exist");
let texture_ptr_2 = texture_2 as *const _;
assert_eq!(
texture_ptr_1, texture_ptr_2,
"repeated request must reuse the same texture"
);
let extent = fx
.target_extent(&device, 1, requested, requested)
.expect("registered");
assert_eq!(extent, (CEILING, CEILING));
assert!(
fx.warned_clamped.contains(&(1, requested, requested)),
"warned_clamped must track the oversized request"
);
for _ in 0..MAX_UNSEEN_TARGET_FRAMES {
fx.mark_seen(&HashSet::new(), &HashSet::new());
}
assert!(
!fx.targets.contains_key(&quantized_key),
"the unseen target ages out"
);
assert!(
fx.warned_clamped.contains(&(1, requested, requested)),
"the warn latch survives target-level aging"
);
fx.ensure_target(&device, 1, requested, requested);
let entry_3 = fx.targets.get(&quantized_key).expect("recreated");
assert_eq!(entry_3.used_w, CEILING, "recreation re-clamps");
assert!(
fx.warned_clamped.contains(&(1, requested, requested)),
"recreation must not disturb the latch"
);
fx.reap(&[1]);
assert!(
!fx.warned_clamped.contains(&(1, requested, requested)),
"reap must drop the id's warn-latch entries"
);
}
}
#[test]
#[ignore = "requires a GPU; run locally with `cargo test -p frust-gpu -- --ignored`"]
fn target_generation_bumps_only_when_a_target_is_actually_recreated() {
pollster::block_on(run());
async fn run() {
let instance = wgpu::Instance::new(
wgpu::InstanceDescriptor::new_without_display_handle_from_env(),
);
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions::default())
.await
.expect("no compatible GPU adapter");
let (device, _queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("frust-gpu effects target-generation test"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
..Default::default()
})
.await
.expect("failed to create device");
const FRAGMENT: &str = "@fragment fn fs_main(in: FrustVsOut) -> \
@location(0) vec4<f32> { return vec4<f32>(frust_u.time, 0.0, 0.0, 1.0); }";
let mut fx = ShaderEffects::new(None);
fx.ensure_pipeline(&device, 1, FRAGMENT);
fx.ensure_target(&device, 1, 4, 4);
let first_generation = fx
.target_generation(&device, 1, 4, 4)
.expect("created target has a generation");
fx.ensure_target(&device, 1, 4, 4);
assert_eq!(
fx.target_generation(&device, 1, 4, 4),
Some(first_generation),
"a cache hit leaves the generation untouched"
);
fx.reap(&[1]);
fx.ensure_pipeline(&device, 1, FRAGMENT);
fx.ensure_target(&device, 1, 4, 4);
let second_generation = fx
.target_generation(&device, 1, 4, 4)
.expect("recreated target has a generation");
assert!(
second_generation > first_generation,
"a target recreated at the same extent must carry a strictly \
greater generation ({second_generation} was not > {first_generation})"
);
}
}
}