use std::collections::{HashMap, HashSet};
use frust_gpu::effects::quantized_target_key;
use frust_gpu::{SceneTextureId, ShaderEffects};
use frust_scene::{Command, Scene, ShaderProgram};
use kurbo::{Affine, Rect};
use crate::config;
use crate::gpu::atlas::x_y_advances;
use crate::renderer::EngineRenderer;
pub const MAX_TEXTURE_DIM: u32 = 8192;
#[derive(Debug, Clone, Copy)]
pub struct QuadDemand<'a> {
pub program: &'a ShaderProgram,
pub size: (u32, u32),
pub time: f32,
}
fn clamp_size(requested: (u32, u32), adapter_max: u32) -> (u32, u32) {
let cap = MAX_TEXTURE_DIM.min(adapter_max);
let clamp = |v: u32| v.clamp(1, cap.max(1));
(clamp(requested.0), clamp(requested.1))
}
fn requested_size(dest: Rect, transform: Affine, adapter_max: u32) -> Option<(u32, u32)> {
if !dest.is_finite() || !transform.as_coeffs().iter().all(|c| c.is_finite()) {
return None;
}
let (x_advance, y_advance) = x_y_advances(transform);
let width = dest.width().abs() * x_advance.hypot();
let height = dest.height().abs() * y_advance.hypot();
if !width.is_finite() || !height.is_finite() {
return None;
}
let axis = |extent: f64| -> u32 {
let ceiled = extent.ceil();
if ceiled <= 1.0 {
1
} else if ceiled >= f64::from(u32::MAX) {
u32::MAX
} else {
ceiled as u32
}
};
Some(clamp_size((axis(width), axis(height)), adapter_max))
}
fn quad_is_culled(bbox: Rect, target_rect: Option<Rect>, inside_snapshot: bool) -> bool {
if inside_snapshot {
return false;
}
let Some(target_rect) = target_rect else {
return false;
};
bbox.is_finite() && !bbox.overlaps(target_rect)
}
fn frame_demands(
scene: &Scene,
root: Affine,
adapter_max: u32,
target_extent: Option<(u32, u32)>,
) -> Vec<QuadDemand<'_>> {
let mut order: Vec<u64> = Vec::new();
let mut demands: HashMap<u64, QuadDemand<'_>> = HashMap::new();
let target_rect = target_extent.map(|(w, h)| Rect::new(0.0, 0.0, f64::from(w), f64::from(h)));
let mut snapshot_depth: u32 = 0;
for command in scene.commands() {
match command {
Command::PushSnapshot { .. } => {
snapshot_depth += 1;
continue;
}
Command::PopSnapshot => {
snapshot_depth = snapshot_depth.saturating_sub(1);
continue;
}
_ => {}
}
let Command::ShaderQuad {
program,
dest,
transform,
time,
} = command
else {
continue;
};
let device_transform = root * *transform;
let device_bbox = device_transform.transform_rect_bbox(*dest);
if quad_is_culled(device_bbox, target_rect, snapshot_depth > 0) {
continue;
}
let Some(size) = requested_size(*dest, device_transform, adapter_max) else {
continue;
};
match demands.get_mut(&program.id()) {
Some(demand) => {
demand.size = (demand.size.0.max(size.0), demand.size.1.max(size.1));
}
None => {
order.push(program.id());
demands.insert(
program.id(),
QuadDemand {
program,
size,
time: *time,
},
);
}
}
}
order
.into_iter()
.filter_map(|id| demands.remove(&id))
.collect()
}
fn culled_program_ids(
scene: &Scene,
root: Affine,
target_extent: Option<(u32, u32)>,
) -> HashSet<u64> {
let Some((w, h)) = target_extent else {
return HashSet::new();
};
let target_rect = Some(Rect::new(0.0, 0.0, f64::from(w), f64::from(h)));
let mut snapshot_depth: u32 = 0;
let mut seen: HashSet<u64> = HashSet::new();
let mut not_culled: HashSet<u64> = HashSet::new();
for command in scene.commands() {
match command {
Command::PushSnapshot { .. } => {
snapshot_depth += 1;
continue;
}
Command::PopSnapshot => {
snapshot_depth = snapshot_depth.saturating_sub(1);
continue;
}
_ => {}
}
let Command::ShaderQuad {
program,
dest,
transform,
..
} = command
else {
continue;
};
let device_bbox = (root * *transform).transform_rect_bbox(*dest);
seen.insert(program.id());
if !quad_is_culled(device_bbox, target_rect, snapshot_depth > 0) {
not_culled.insert(program.id());
}
}
seen.difference(¬_culled).copied().collect()
}
fn needs_rebind(existing: Option<(u32, u32, u64)>, current: (u32, u32, u64)) -> bool {
existing != Some(current)
}
pub struct ShaderQuadPass {
effects: ShaderEffects,
registered: HashMap<u64, (u32, u32, u64)>,
frame_target_extent: Option<(u32, u32)>,
}
impl ShaderQuadPass {
#[must_use]
pub fn new(pipeline_cache: Option<wgpu::PipelineCache>) -> Self {
Self {
effects: ShaderEffects::new(pipeline_cache),
registered: HashMap::new(),
frame_target_extent: None,
}
}
#[must_use]
pub fn registered_len(&self) -> usize {
self.registered.len()
}
pub fn set_frame_target_extent(&mut self, extent: Option<(u32, u32)>) {
self.frame_target_extent = extent;
}
pub fn prepare(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
scene: &Scene,
root: Affine,
engine: &mut EngineRenderer,
) {
if config::shader_effects_disabled() {
self.unregister_all(engine);
return;
}
let adapter_max = device.limits().max_texture_dimension_2d;
let demands = frame_demands(scene, root, adapter_max, self.frame_target_extent);
let mut live_ids: HashSet<u64> = HashSet::with_capacity(demands.len());
let mut live_keys: HashSet<(u64, u32, u32)> = HashSet::with_capacity(demands.len());
for demand in demands {
let id = demand.program.id();
let (w, h) = demand.size;
let quantized = quantized_target_key(w, h, adapter_max);
live_ids.insert(id);
live_keys.insert((id, quantized.0, quantized.1));
self.effects
.ensure_pipeline(device, id, demand.program.source());
self.effects.ensure_target(device, id, w, h);
self.effects
.encode_pass(encoder, queue, device, id, (w, h), demand.time);
self.register(engine, device, id, (w, h));
}
for id in culled_program_ids(scene, root, self.frame_target_extent) {
self.unregister(engine, id);
}
let stale = self.effects.mark_seen(&live_ids, &live_keys);
self.effects.reap(&stale);
for id in stale {
self.unregister(engine, id);
}
}
fn register(
&mut self,
engine: &mut EngineRenderer,
device: &wgpu::Device,
id: u64,
size: (u32, u32),
) {
let (w, h) = size;
let extent = self.effects.target_extent(device, id, w, h);
let view = self.effects.target_view(device, id, w, h).cloned();
let generation = self.effects.target_generation(device, id, w, h);
let (Some(extent), Some(view), Some(generation)) = (extent, view, generation) else {
self.unregister(engine, id);
return;
};
let current = (extent.0, extent.1, generation);
if !needs_rebind(self.registered.get(&id).copied(), current) {
return;
}
engine.bind_texture(SceneTextureId::for_shader_program(id), extent, view);
self.registered.insert(id, current);
}
fn unregister(&mut self, engine: &mut EngineRenderer, id: u64) {
if self.registered.remove(&id).is_some() {
engine.unbind_texture(SceneTextureId::for_shader_program(id));
}
}
fn unregister_all(&mut self, engine: &mut EngineRenderer) {
for id in self.registered.drain().map(|(id, _)| id) {
engine.unbind_texture(SceneTextureId::for_shader_program(id));
}
}
}
impl std::fmt::Debug for ShaderQuadPass {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ShaderQuadPass")
.field("registered", &self.registered.len())
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::*;
use frust_scene::SceneBuilder;
const SOURCE: &str = "@fragment fn fs_main(in: FrustVsOut) -> @location(0) vec4<f32> \
{ return vec4<f32>(1.0, 0.0, 1.0, 1.0); }";
fn scene_of(record: impl FnOnce(&mut SceneBuilder<'_>)) -> Scene {
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
record(&mut builder);
scene
}
#[test]
fn clamp_size_caps_at_the_policy_bound() {
assert_eq!(clamp_size((10_000, 10_000), u32::MAX), (8192, 8192));
}
#[test]
fn clamp_size_respects_an_adapter_max_below_the_policy_bound() {
assert_eq!(clamp_size((6000, 6000), 4096), (4096, 4096));
}
#[test]
fn clamp_size_floors_zero_to_one() {
assert_eq!(clamp_size((0, 0), 8192), (1, 1));
assert_eq!(clamp_size((0, 512), 8192), (1, 512));
}
#[test]
fn clamp_size_passes_an_in_range_request_through() {
assert_eq!(clamp_size((1290, 2796), 16384), (1290, 2796));
}
#[test]
fn clamp_size_survives_a_degenerate_adapter_max() {
assert_eq!(clamp_size((100, 100), 0), (1, 1));
}
#[test]
fn a_requested_size_is_the_destination_in_device_space() {
assert_eq!(
requested_size(Rect::new(0.0, 0.0, 40.0, 20.0), Affine::IDENTITY, 8192),
Some((40, 20))
);
assert_eq!(
requested_size(Rect::new(0.0, 0.0, 40.0, 20.0), Affine::scale(2.0), 8192),
Some((80, 40))
);
}
#[test]
fn a_requested_size_rounds_a_fractional_extent_up() {
assert_eq!(
requested_size(Rect::new(0.0, 0.0, 40.5, 20.25), Affine::IDENTITY, 8192),
Some((41, 21))
);
}
#[test]
fn a_requested_size_is_never_zero() {
assert_eq!(
requested_size(Rect::new(4.0, 4.0, 4.0, 4.0), Affine::IDENTITY, 8192),
Some((1, 1))
);
}
#[test]
fn a_requested_size_is_clamped_rather_than_wrapped() {
assert_eq!(
requested_size(Rect::new(0.0, 0.0, 1e12, 1e12), Affine::IDENTITY, 8192),
Some((8192, 8192))
);
}
#[test]
fn an_inverted_dest_yields_the_same_target_extent_as_its_normalized_twin() {
let normalized = Rect::new(0.0, 0.0, 40.0, 20.0);
let inverted = Rect::new(40.0, 20.0, 0.0, 0.0);
assert_eq!(
inverted.width(),
-40.0,
"sanity check: kurbo does not normalize"
);
assert_eq!(
requested_size(inverted, Affine::IDENTITY, 8192),
requested_size(normalized, Affine::IDENTITY, 8192)
);
assert_eq!(
requested_size(inverted, Affine::IDENTITY, 8192),
Some((40, 20))
);
}
#[test]
fn a_single_axis_mirrored_dest_yields_the_same_target_extent_as_unmirrored() {
let unmirrored = Rect::new(0.0, 0.0, 40.0, 20.0);
let mirrored_x = Rect::new(40.0, 0.0, 0.0, 20.0);
assert_eq!(
requested_size(mirrored_x, Affine::IDENTITY, 8192),
requested_size(unmirrored, Affine::IDENTITY, 8192)
);
}
#[test]
fn non_finite_geometry_asks_for_no_target() {
assert_eq!(
requested_size(Rect::new(0.0, 0.0, f64::NAN, 8.0), Affine::IDENTITY, 8192),
None
);
assert_eq!(
requested_size(
Rect::new(0.0, 0.0, 8.0, 8.0),
Affine::translate((f64::INFINITY, 0.0)),
8192
),
None
);
}
#[test]
fn a_rotated_quad_sizes_from_dest_dimensions_not_the_axis_aligned_bbox() {
let dest = Rect::new(0.0, 0.0, 100.0, 50.0);
let transform = Affine::rotate(std::f64::consts::FRAC_PI_4);
assert_eq!(requested_size(dest, transform, 8192), Some((100, 50)));
}
#[test]
fn a_scaled_and_rotated_quad_sizes_aspect_true_too() {
let dest = Rect::new(0.0, 0.0, 100.0, 50.0);
let transform = Affine::rotate(std::f64::consts::FRAC_PI_4) * Affine::scale(2.0);
assert_eq!(requested_size(dest, transform, 8192), Some((200, 100)));
}
#[test]
fn a_scene_without_shader_quads_demands_nothing() {
let scene = scene_of(|builder| {
builder.fill_rect(
Rect::new(0.0, 0.0, 8.0, 8.0),
peniko::Brush::Solid(peniko::color::palette::css::RED),
);
});
assert!(frame_demands(&scene, Affine::IDENTITY, 8192, None).is_empty());
}
#[test]
fn each_program_is_demanded_once_in_painter_order() {
let first = ShaderProgram::new(SOURCE);
let second = ShaderProgram::new(SOURCE);
let scene = scene_of(|builder| {
builder.draw_shader(&first, Rect::new(0.0, 0.0, 8.0, 8.0), 0.0);
builder.draw_shader(&second, Rect::new(0.0, 0.0, 4.0, 4.0), 0.0);
builder.draw_shader(&first, Rect::new(0.0, 0.0, 8.0, 8.0), 0.0);
});
let demands = frame_demands(&scene, Affine::IDENTITY, 8192, None);
assert_eq!(demands.len(), 2);
assert_eq!(demands[0].program.id(), first.id());
assert_eq!(demands[1].program.id(), second.id());
}
#[test]
fn a_program_drawn_at_two_sizes_demands_the_larger_on_each_axis() {
let program = ShaderProgram::new(SOURCE);
let scene = scene_of(|builder| {
builder.draw_shader(&program, Rect::new(0.0, 0.0, 40.0, 10.0), 0.0);
builder.draw_shader(&program, Rect::new(0.0, 0.0, 10.0, 30.0), 0.0);
});
let demands = frame_demands(&scene, Affine::IDENTITY, 8192, None);
assert_eq!(demands.len(), 1, "one target serves both quads");
assert_eq!(demands[0].size, (40, 30));
}
#[test]
fn a_program_renders_at_its_first_quads_time() {
let program = ShaderProgram::new(SOURCE);
let scene = scene_of(|builder| {
builder.draw_shader(&program, Rect::new(0.0, 0.0, 8.0, 8.0), 1.5);
builder.draw_shader(&program, Rect::new(0.0, 0.0, 8.0, 8.0), 9.0);
});
let demands = frame_demands(&scene, Affine::IDENTITY, 8192, None);
assert_eq!(demands[0].time, 1.5);
}
#[test]
fn a_quad_whose_geometry_no_target_can_be_sized_from_is_dropped() {
let program = ShaderProgram::new(SOURCE);
let scene = scene_of(|builder| {
builder.draw_shader(&program, Rect::new(0.0, 0.0, f64::NAN, 8.0), 0.0);
});
assert!(frame_demands(&scene, Affine::IDENTITY, 8192, None).is_empty());
}
#[test]
fn the_frame_transform_scales_the_demand() {
let program = ShaderProgram::new(SOURCE);
let scene = scene_of(|builder| {
builder.draw_shader(&program, Rect::new(0.0, 0.0, 100.0, 50.0), 0.0);
});
let demands = frame_demands(&scene, Affine::scale(3.0), 8192, None);
assert_eq!(demands[0].size, (300, 150));
}
#[test]
fn a_demand_is_clamped_by_the_adapter_ceiling() {
let program = ShaderProgram::new(SOURCE);
let scene = scene_of(|builder| {
builder.draw_shader(&program, Rect::new(0.0, 0.0, 6000.0, 6000.0), 0.0);
});
let demands = frame_demands(&scene, Affine::IDENTITY, 4096, None);
assert_eq!(demands[0].size, (4096, 4096));
}
#[test]
fn a_programs_target_id_matches_the_one_the_compiler_derives() {
let program = ShaderProgram::new(SOURCE);
assert_eq!(
SceneTextureId::for_shader_program(program.id()).get(),
crate::compile::shader_quad_texture_id(program.id())
);
}
#[test]
fn with_no_target_extent_a_far_off_screen_quad_still_demands() {
let program = ShaderProgram::new(SOURCE);
let scene = scene_of(|builder| {
builder.draw_shader(
&program,
Rect::new(10_000.0, 10_000.0, 10_008.0, 10_008.0),
0.0,
);
});
assert_eq!(frame_demands(&scene, Affine::IDENTITY, 8192, None).len(), 1);
}
#[test]
fn a_quad_entirely_outside_the_target_extent_demands_nothing() {
let program = ShaderProgram::new(SOURCE);
let scene = scene_of(|builder| {
builder.draw_shader(
&program,
Rect::new(10_000.0, 10_000.0, 10_008.0, 10_008.0),
0.0,
);
});
let demands = frame_demands(&scene, Affine::IDENTITY, 8192, Some((64, 64)));
assert!(demands.is_empty());
}
#[test]
fn a_quad_straddling_the_target_edge_still_demands() {
let program = ShaderProgram::new(SOURCE);
let scene = scene_of(|builder| {
builder.draw_shader(&program, Rect::new(-8.0, -8.0, 8.0, 8.0), 0.0);
});
let demands = frame_demands(&scene, Affine::IDENTITY, 8192, Some((64, 64)));
assert_eq!(
demands.len(),
1,
"a quad straddling the target boundary is still partly visible"
);
}
#[test]
fn a_quad_touching_the_target_edge_still_demands() {
let program = ShaderProgram::new(SOURCE);
let scene = scene_of(|builder| {
builder.draw_shader(&program, Rect::new(64.0, 0.0, 80.0, 16.0), 0.0);
});
let demands = frame_demands(&scene, Affine::IDENTITY, 8192, Some((64, 64)));
assert_eq!(demands.len(), 1);
}
#[test]
fn a_quad_fully_inside_the_target_extent_still_demands() {
let program = ShaderProgram::new(SOURCE);
let scene = scene_of(|builder| {
builder.draw_shader(&program, Rect::new(4.0, 4.0, 12.0, 12.0), 0.0);
});
let demands = frame_demands(&scene, Affine::IDENTITY, 8192, Some((64, 64)));
assert_eq!(demands.len(), 1);
}
#[test]
fn a_quad_with_non_finite_geometry_is_dropped_by_requested_size_not_by_culling() {
let program = ShaderProgram::new(SOURCE);
let scene = scene_of(|builder| {
builder.draw_shader(&program, Rect::new(0.0, 0.0, f64::NAN, 8.0), 0.0);
});
assert!(frame_demands(&scene, Affine::IDENTITY, 8192, Some((64, 64))).is_empty());
}
#[test]
fn set_frame_target_extent_defaults_to_none() {
let pass = ShaderQuadPass::new(None);
assert_eq!(pass.frame_target_extent, None);
}
#[test]
fn needs_rebind_true_with_no_existing_registration() {
assert!(needs_rebind(None, (64, 64, 1)));
}
#[test]
fn needs_rebind_false_when_extent_and_generation_are_unchanged() {
assert!(!needs_rebind(Some((64, 64, 1)), (64, 64, 1)));
}
#[test]
fn needs_rebind_true_when_the_extent_changes() {
assert!(needs_rebind(Some((64, 64, 1)), (96, 64, 1)));
}
#[test]
fn needs_rebind_true_when_the_generation_changes_at_the_same_extent() {
assert!(needs_rebind(Some((64, 64, 1)), (64, 64, 2)));
}
#[test]
fn quad_is_culled_true_for_a_finite_non_overlapping_bbox() {
let target = Rect::new(0.0, 0.0, 64.0, 64.0);
let bbox = Rect::new(1000.0, 1000.0, 1008.0, 1008.0);
assert!(quad_is_culled(bbox, Some(target), false));
}
#[test]
fn quad_is_culled_false_when_overlapping_the_target() {
let target = Rect::new(0.0, 0.0, 64.0, 64.0);
let bbox = Rect::new(0.0, 0.0, 8.0, 8.0);
assert!(!quad_is_culled(bbox, Some(target), false));
}
#[test]
fn quad_is_culled_false_for_a_non_finite_bbox() {
let target = Rect::new(0.0, 0.0, 64.0, 64.0);
let bbox = Rect::new(0.0, 0.0, f64::NAN, 8.0);
assert!(!quad_is_culled(bbox, Some(target), false));
}
#[test]
fn quad_is_culled_false_with_no_target_rect() {
let bbox = Rect::new(1000.0, 1000.0, 1008.0, 1008.0);
assert!(!quad_is_culled(bbox, None, false));
}
#[test]
fn quad_is_culled_false_inside_a_snapshot_even_when_far_outside_the_target() {
let target = Rect::new(0.0, 0.0, 64.0, 64.0);
let bbox = Rect::new(1000.0, 1000.0, 1008.0, 1008.0);
assert!(!quad_is_culled(bbox, Some(target), true));
}
#[test]
fn culled_program_ids_reports_a_program_whose_only_quad_is_culled() {
let program = ShaderProgram::new(SOURCE);
let scene = scene_of(|builder| {
builder.draw_shader(
&program,
Rect::new(10_000.0, 10_000.0, 10_008.0, 10_008.0),
0.0,
);
});
let culled = culled_program_ids(&scene, Affine::IDENTITY, Some((64, 64)));
assert_eq!(culled, [program.id()].into_iter().collect());
}
#[test]
fn culled_program_ids_empty_when_one_of_the_programs_quads_lands_on_target() {
let program = ShaderProgram::new(SOURCE);
let scene = scene_of(|builder| {
builder.draw_shader(
&program,
Rect::new(10_000.0, 10_000.0, 10_008.0, 10_008.0),
0.0,
);
builder.draw_shader(&program, Rect::new(0.0, 0.0, 8.0, 8.0), 0.0);
});
assert!(culled_program_ids(&scene, Affine::IDENTITY, Some((64, 64))).is_empty());
}
#[test]
fn culled_program_ids_empty_with_no_target_extent() {
let program = ShaderProgram::new(SOURCE);
let scene = scene_of(|builder| {
builder.draw_shader(
&program,
Rect::new(10_000.0, 10_000.0, 10_008.0, 10_008.0),
0.0,
);
});
assert!(culled_program_ids(&scene, Affine::IDENTITY, None).is_empty());
}
#[test]
fn culled_program_ids_exempts_a_quad_inside_a_snapshot_bracket() {
let program = ShaderProgram::new(SOURCE);
let far_off_target = Rect::new(10_000.0, 10_000.0, 10_008.0, 10_008.0);
let scene = scene_of(|builder| {
builder.push_snapshot(1, far_off_target, 1.0, 1.0);
builder.draw_shader(&program, far_off_target, 0.0);
builder.pop_snapshot();
});
assert!(
culled_program_ids(&scene, Affine::IDENTITY, Some((64, 64))).is_empty(),
"a quad inside an open PushSnapshot bracket must never be reported culled"
);
assert_eq!(
frame_demands(&scene, Affine::IDENTITY, 8192, Some((64, 64))).len(),
1
);
}
}