use std::collections::hash_map::DefaultHasher;
use std::hash::Hash;
use std::sync::OnceLock;
#[derive(Debug, Clone, thiserror::Error)]
pub enum EffectError {
#[error("Effect WGSL compilation failed: {0}")]
CompilationFailed(String),
#[error("Effect {0} has not been loaded")]
EffectNotLoaded(u64),
#[error("Node {0} not found in draw tree")]
NodeNotFound(usize),
#[error("Invalid effect parameters: {0}")]
InvalidParams(String),
}
#[derive(Copy, Clone, Debug, Default, PartialEq)]
pub enum BackdropCaptureArea {
#[default]
NodeBounds,
FullScene,
ScreenRect([(f32, f32); 2]),
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct BackdropEffectConfig {
pub capture_area: BackdropCaptureArea,
pub padding: f32,
pub downsample: f32,
}
impl Default for BackdropEffectConfig {
fn default() -> Self {
Self {
capture_area: BackdropCaptureArea::NodeBounds,
padding: 0.0,
downsample: 1.0,
}
}
}
impl BackdropEffectConfig {
pub fn new() -> Self {
Self::default()
}
pub fn capture_area(mut self, capture_area: BackdropCaptureArea) -> Self {
self.capture_area = capture_area;
self
}
pub fn padding(mut self, padding: f32) -> Self {
self.padding = padding;
self
}
pub fn downsample(mut self, downsample: f32) -> Self {
self.downsample = downsample;
self
}
}
pub(crate) const FULLSCREEN_QUAD_VS: &str = r#"
struct QuadOutput {
@builtin(position) position: vec4<f32>,
@location(0) uv: vec2<f32>,
};
@vertex
fn vs_quad(@builtin(vertex_index) vi: u32) -> QuadOutput {
// Fullscreen triangle trick: 3 vertices cover the entire screen
let uv = vec2<f32>(f32((vi << 1u) & 2u), f32(vi & 2u));
var out: QuadOutput;
out.position = vec4<f32>(uv * 2.0 - 1.0, 0.0, 1.0);
out.uv = vec2<f32>(uv.x, 1.0 - uv.y);
return out;
}
"#;
pub(crate) const EFFECT_FS_PREAMBLE: &str = r#"
// -- Provided by the engine (group 0) --
@group(0) @binding(0) var t_input: texture_2d<f32>;
@group(0) @binding(1) var s_input: sampler;
"#;
pub(crate) const COMPOSITE_FS: &str = r#"
@group(0) @binding(0) var t_input: texture_2d<f32>;
@group(0) @binding(1) var s_input: sampler;
@fragment
fn fs_composite(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
return textureSample(t_input, s_input, uv);
}
"#;
pub(crate) struct LoadedEffectPass {
pub pipeline: wgpu::RenderPipeline,
pub has_params: bool,
}
pub(crate) struct LoadedEffect {
pub passes: Vec<LoadedEffectPass>,
pub input_bind_group_layout: wgpu::BindGroupLayout,
pub params_bind_group_layout: Option<wgpu::BindGroupLayout>,
}
pub(crate) struct EffectInstance {
pub effect_id: u64,
pub params: Vec<u8>,
pub params_buffer: Option<wgpu::Buffer>,
pub params_bind_group: Option<wgpu::BindGroup>,
pub backdrop_config: Option<BackdropEffectConfig>,
pub backdrop_material_params_buffer: Option<wgpu::Buffer>,
pub backdrop_texture_bind_group: Option<wgpu::BindGroup>,
pub backdrop_texture_id: Option<u64>,
}
pub(crate) struct PooledTexture {
pub texture_id: u64,
pub color_texture: wgpu::Texture,
pub color_view: wgpu::TextureView,
pub depth_stencil_view: Option<wgpu::TextureView>,
pub resolve_texture: Option<wgpu::Texture>,
pub resolve_view: Option<wgpu::TextureView>,
pub width: u32,
pub height: u32,
pub sample_count: u32,
}
pub(crate) struct OffscreenTexturePool {
available: Vec<PooledTexture>,
next_texture_id: u64,
}
const MAX_POOL_SIZE: usize = 8;
impl OffscreenTexturePool {
pub fn new() -> Self {
Self {
available: Vec::new(),
next_texture_id: 1,
}
}
pub fn recycle(&mut self, textures: &mut Vec<PooledTexture>) {
self.available.append(textures);
self.available.truncate(MAX_POOL_SIZE);
}
pub fn trim(&mut self, width: u32, height: u32, sample_count: u32) {
self.available
.retain(|t| t.width == width && t.height == height && t.sample_count == sample_count);
if self.available.len() > MAX_POOL_SIZE {
self.available.truncate(MAX_POOL_SIZE);
}
}
pub fn acquire_with_depth(
&mut self,
device: &wgpu::Device,
width: u32,
height: u32,
format: wgpu::TextureFormat,
sample_count: u32,
) -> PooledTexture {
self.acquire(device, width, height, format, sample_count, true)
}
pub fn acquire_color_only(
&mut self,
device: &wgpu::Device,
width: u32,
height: u32,
format: wgpu::TextureFormat,
sample_count: u32,
) -> PooledTexture {
self.acquire(device, width, height, format, sample_count, false)
}
fn acquire(
&mut self,
device: &wgpu::Device,
width: u32,
height: u32,
format: wgpu::TextureFormat,
sample_count: u32,
with_depth: bool,
) -> PooledTexture {
let found = self.available.iter().position(|texture| {
texture.width == width
&& texture.height == height
&& texture.sample_count == sample_count
&& texture.depth_stencil_view.is_some() == with_depth
});
if let Some(idx) = found {
self.available.swap_remove(idx)
} else {
self.create_pooled_texture(device, width, height, format, sample_count, with_depth)
}
}
fn create_pooled_texture(
&mut self,
device: &wgpu::Device,
width: u32,
height: u32,
format: wgpu::TextureFormat,
sample_count: u32,
with_depth: bool,
) -> PooledTexture {
let texture_id = self.next_texture_id;
self.next_texture_id += 1;
let color_texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("effect_offscreen_color"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::COPY_SRC
| wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
let color_view = color_texture.create_view(&wgpu::TextureViewDescriptor::default());
let depth_stencil_view = with_depth.then(|| {
let depth_stencil_texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("effect_offscreen_depth_stencil"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Depth24PlusStencil8,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
depth_stencil_texture.create_view(&wgpu::TextureViewDescriptor::default())
});
let (resolve_texture, resolve_view) = if sample_count > 1 {
let resolve_tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("effect_offscreen_resolve"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::COPY_SRC
| wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
let resolve_v = resolve_tex.create_view(&wgpu::TextureViewDescriptor::default());
(Some(resolve_tex), Some(resolve_v))
} else {
(None, None)
};
PooledTexture {
texture_id,
color_texture,
color_view,
depth_stencil_view,
resolve_texture,
resolve_view,
width,
height,
sample_count,
}
}
}
pub(crate) fn create_effect_input_bind_group_layout(
device: &wgpu::Device,
) -> wgpu::BindGroupLayout {
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("effect_input_bgl"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: true },
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
})
}
pub(crate) fn create_effect_params_bind_group_layout(
device: &wgpu::Device,
) -> wgpu::BindGroupLayout {
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("effect_params_bgl"),
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,
}],
})
}
pub(crate) fn build_effect_wgsl(user_fragment_source: &str) -> String {
format!("{FULLSCREEN_QUAD_VS}\n{EFFECT_FS_PREAMBLE}\n{user_fragment_source}")
}
pub(crate) fn build_composite_wgsl() -> String {
format!("{FULLSCREEN_QUAD_VS}\n{COMPOSITE_FS}")
}
pub(crate) fn has_user_params(user_fragment_source: &str) -> bool {
fn block_comment_regex() -> &'static regex::Regex {
static BLOCK_COMMENT_REGEX: OnceLock<regex::Regex> = OnceLock::new();
BLOCK_COMMENT_REGEX.get_or_init(|| regex::Regex::new(r"(?s)/\*.*?\*/").unwrap())
}
fn line_comment_regex() -> &'static regex::Regex {
static LINE_COMMENT_REGEX: OnceLock<regex::Regex> = OnceLock::new();
LINE_COMMENT_REGEX.get_or_init(|| regex::Regex::new(r"//[^\n]*").unwrap())
}
fn user_params_group_regex() -> &'static regex::Regex {
static USER_PARAMS_GROUP_REGEX: OnceLock<regex::Regex> = OnceLock::new();
USER_PARAMS_GROUP_REGEX.get_or_init(|| regex::Regex::new(r"@group\s*\(\s*1\s*\)").unwrap())
}
let no_block = block_comment_regex().replace_all(user_fragment_source, "");
let stripped = line_comment_regex().replace_all(&no_block, "");
user_params_group_regex().is_match(&stripped)
}
pub(crate) fn compile_effect_pipeline(
device: &wgpu::Device,
pass_sources: &[&str],
format: wgpu::TextureFormat,
) -> Result<LoadedEffect, EffectError> {
if pass_sources.is_empty() {
return Err(EffectError::InvalidParams(
"At least one effect pass is required".into(),
));
}
let input_bgl = create_effect_input_bind_group_layout(device);
let any_has_params = pass_sources.iter().any(|s| has_user_params(s));
let params_bgl = if any_has_params {
Some(create_effect_params_bind_group_layout(device))
} else {
None
};
let mut hasher = DefaultHasher::new();
let mut passes = Vec::with_capacity(pass_sources.len());
for (i, &source) in pass_sources.iter().enumerate() {
source.hash(&mut hasher);
let full_wgsl = build_effect_wgsl(source);
let pass_has_params = has_user_params(source);
let shader_label = format!("effect_pass{i}_shader");
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some(&shader_label),
source: wgpu::ShaderSource::Wgsl(full_wgsl.into()),
});
let layout_label = format!("effect_pass{i}_layout");
let pipeline_layout = if pass_has_params {
let bind_group_layouts = [&input_bgl, params_bgl.as_ref().unwrap()];
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some(&layout_label),
bind_group_layouts: &bind_group_layouts,
push_constant_ranges: &[],
})
} else {
let bind_group_layouts = [&input_bgl];
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some(&layout_label),
bind_group_layouts: &bind_group_layouts,
push_constant_ranges: &[],
})
};
let pipeline_label = format!("effect_pass{i}_pipeline");
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(&pipeline_label),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_quad"),
compilation_options: Default::default(),
buffers: &[], },
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("effect_main"),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
}),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
..Default::default()
},
depth_stencil: None, multisample: wgpu::MultisampleState::default(),
multiview: None,
cache: None,
});
passes.push(LoadedEffectPass {
pipeline,
has_params: pass_has_params,
});
}
Ok(LoadedEffect {
passes,
input_bind_group_layout: input_bgl,
params_bind_group_layout: params_bgl,
})
}
pub(crate) fn compile_composite_pipeline(
device: &wgpu::Device,
format: wgpu::TextureFormat,
) -> (wgpu::RenderPipeline, wgpu::BindGroupLayout) {
let wgsl = build_composite_wgsl();
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("composite_shader"),
source: wgpu::ShaderSource::Wgsl(wgsl.into()),
});
let input_bgl = create_effect_input_bind_group_layout(device);
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("composite_pipeline_layout"),
bind_group_layouts: &[&input_bgl],
push_constant_ranges: &[],
});
let stencil_face = wgpu::StencilFaceState {
compare: wgpu::CompareFunction::Equal,
fail_op: wgpu::StencilOperation::Keep,
depth_fail_op: wgpu::StencilOperation::Keep,
pass_op: wgpu::StencilOperation::Keep,
};
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("composite_pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_quad"),
compilation_options: Default::default(),
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_composite"),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
}),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth24PlusStencil8,
depth_write_enabled: false,
depth_compare: wgpu::CompareFunction::Always,
stencil: wgpu::StencilState {
front: stencil_face,
back: stencil_face,
read_mask: 0xff,
write_mask: 0x00, },
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState::default(),
multiview: None,
cache: None,
});
(pipeline, input_bgl)
}
pub(crate) fn compile_texture_blit_pipeline(
device: &wgpu::Device,
format: wgpu::TextureFormat,
input_bind_group_layout: &wgpu::BindGroupLayout,
) -> wgpu::RenderPipeline {
let wgsl = build_composite_wgsl();
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("texture_blit_shader"),
source: wgpu::ShaderSource::Wgsl(wgsl.into()),
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("texture_blit_pipeline_layout"),
bind_group_layouts: &[input_bind_group_layout],
push_constant_ranges: &[],
});
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("texture_blit_pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_quad"),
compilation_options: Default::default(),
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_composite"),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview: None,
cache: None,
})
}
pub(crate) fn create_texture_sample_bind_group(
device: &wgpu::Device,
layout: &wgpu::BindGroupLayout,
texture_view: &wgpu::TextureView,
sampler: &wgpu::Sampler,
label: Option<&str>,
) -> wgpu::BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label,
layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(texture_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(sampler),
},
],
})
}
pub(crate) fn create_backdrop_texture_sample_bind_group(
device: &wgpu::Device,
layout: &wgpu::BindGroupLayout,
material_params_buffer: &wgpu::Buffer,
texture_view: &wgpu::TextureView,
sampler: &wgpu::Sampler,
label: Option<&str>,
) -> wgpu::BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label,
layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: material_params_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::TextureView(texture_view),
},
wgpu::BindGroupEntry {
binding: 4,
resource: wgpu::BindingResource::Sampler(sampler),
},
],
})
}
pub(crate) fn prepare_solid_backdrop_material_params_buffer(
device: &wgpu::Device,
queue: &wgpu::Queue,
backdrop_material_params_buffer: &mut Option<wgpu::Buffer>,
sampling_uniform: crate::pipeline::BackdropSamplingUniform,
) -> wgpu::Buffer {
let material_params =
crate::gradient::gpu::GpuMaterialParams::for_backdrop_sampling(sampling_uniform);
if let Some(existing_buffer) = backdrop_material_params_buffer.as_ref() {
queue.write_buffer(existing_buffer, 0, bytemuck::bytes_of(&material_params));
} else {
*backdrop_material_params_buffer = Some(crate::pipeline::create_buffer_init(
device,
Some("solid_backdrop_material_params_buffer"),
bytemuck::bytes_of(&material_params),
wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
));
}
backdrop_material_params_buffer
.as_ref()
.expect("backdrop material params buffer should be initialized")
.clone()
}
pub(crate) fn create_params_bind_group(
device: &wgpu::Device,
layout: &wgpu::BindGroupLayout,
buffer: &wgpu::Buffer,
) -> wgpu::BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("effect_params_bg"),
layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: buffer.as_entire_binding(),
}],
})
}