use crate::frame_graph::{
FrameCommandRecorder, FrameCommandStats, FrameTextureDescriptor, UploadAllocatorId,
UploadAllocatorSpec,
};
use crate::offscreen::{OffscreenPool, OffscreenTarget};
use crate::shader_cache::{RuntimeShaderPipelineMode, ShaderPipelineCache};
use crate::shaders;
use cranpose_ui_graphics::{
BlendMode, RenderEffect, RuntimeShader, TileMode, ROUNDED_ALPHA_MASK_WGSL,
};
use crate::gpu_stats::FrameStats;
use std::cell::Cell;
pub(crate) struct EffectRenderer {
offscreen_pool: OffscreenPool,
pub shader_cache: ShaderPipelineCache,
blur_pipeline: wgpu::RenderPipeline,
blur_rounded_mask_pipeline: wgpu::RenderPipeline,
blur_uniform_bind_group_layout: wgpu::BindGroupLayout,
offset_pipeline: wgpu::RenderPipeline,
offset_uniform_bind_group_layout: wgpu::BindGroupLayout,
blit_pipeline: wgpu::RenderPipeline,
blit_pipeline_dst_out: wgpu::RenderPipeline,
blit_uniform_bind_group_layout: wgpu::BindGroupLayout,
projective_blit_pipeline: wgpu::RenderPipeline,
projective_blit_pipeline_dst_out: wgpu::RenderPipeline,
pub effect_texture_bind_group_layout: wgpu::BindGroupLayout,
pub effect_uniform_bind_group_layout: wgpu::BindGroupLayout,
pub effect_linear_sampler: wgpu::Sampler,
surface_format: wgpu::TextureFormat,
pub(crate) debug_command_stats: Cell<FrameCommandStats>,
pub(crate) debug_blurs: Cell<u32>,
pub(crate) debug_composites: Cell<u32>,
pub(crate) debug_effects: Cell<u32>,
pub(crate) debug_upload_bytes: Cell<u64>,
}
#[derive(Clone, Copy)]
pub(crate) struct ProjectiveSurfaceComposite<'a> {
pub(crate) source: &'a OffscreenTarget,
pub(crate) source_size: (f32, f32),
pub(crate) inverse_matrix: [[f32; 3]; 3],
pub(crate) dest_bounds: [[f32; 2]; 4],
pub(crate) alpha: f32,
pub(crate) load_op: wgpu::LoadOp<wgpu::Color>,
pub(crate) scissor: Option<(u32, u32, u32, u32)>,
pub(crate) blend_mode: BlendMode,
pub(crate) sample_mode: CompositeSampleMode,
}
pub(crate) trait EffectScratchTargetProvider<'target> {
fn next(&mut self) -> Result<&'target OffscreenTarget, String>;
fn assert_consumed(&self) -> Result<(), String>;
}
pub(crate) struct RecordedEffectScratchTargets {
targets: Vec<RecordedEffectScratchTarget>,
}
struct RecordedEffectScratchTarget {
descriptor: FrameTextureDescriptor,
target: OffscreenTarget,
}
impl RecordedEffectScratchTargets {
fn new() -> Self {
Self {
targets: Vec::new(),
}
}
fn push(&mut self, descriptor: FrameTextureDescriptor, target: OffscreenTarget) {
self.targets
.push(RecordedEffectScratchTarget { descriptor, target });
}
pub(crate) fn release_into<C: FrameCommandRecorder>(self, recorder: &mut C) {
for scratch in self.targets {
recorder.release_transient_offscreen(scratch.descriptor, scratch.target);
}
}
pub(crate) fn refs(&self) -> RecordedEffectScratchTargetRefs<'_> {
RecordedEffectScratchTargetRefs {
targets: &self.targets,
next: 0,
}
}
}
pub(crate) struct RecordedEffectScratchTargetRefs<'a> {
targets: &'a [RecordedEffectScratchTarget],
next: usize,
}
impl<'a> EffectScratchTargetProvider<'a> for RecordedEffectScratchTargetRefs<'a> {
fn next(&mut self) -> Result<&'a OffscreenTarget, String> {
let index = self.next;
let target = self
.targets
.get(index)
.map(|scratch| &scratch.target)
.ok_or_else(|| format!("render effect scratch target {index} was not acquired"))?;
self.next += 1;
Ok(target)
}
fn assert_consumed(&self) -> Result<(), String> {
if self.next == self.targets.len() {
Ok(())
} else {
Err(format!(
"render effect acquired {} scratch targets but consumed {}",
self.targets.len(),
self.next
))
}
}
}
fn acquire_recorded_effect_scratch_textures_into<C: FrameCommandRecorder>(
recorder: &mut C,
device: &wgpu::Device,
effect: &RenderEffect,
width: u32,
height: u32,
format: wgpu::TextureFormat,
targets: &mut RecordedEffectScratchTargets,
) {
match effect {
RenderEffect::Blur {
radius_x, radius_y, ..
} => {
if *radius_x > 0.0 || *radius_y > 0.0 {
let descriptor = FrameTextureDescriptor::render_attachment(
"Render Effect Blur Scratch",
width,
height,
format,
);
let target = recorder.acquire_transient_offscreen(device, descriptor);
targets.push(descriptor, target);
}
}
RenderEffect::Offset { .. } | RenderEffect::Shader { .. } => {}
RenderEffect::Chain { first, second } => {
let descriptor = FrameTextureDescriptor::render_attachment(
"Render Effect Chain Scratch",
width,
height,
format,
);
let target = recorder.acquire_transient_offscreen(device, descriptor);
targets.push(descriptor, target);
acquire_recorded_effect_scratch_textures_into(
recorder, device, first, width, height, format, targets,
);
acquire_recorded_effect_scratch_textures_into(
recorder, device, second, width, height, format, targets,
);
}
}
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct BlurUniforms {
direction_and_radius: [f32; 4],
texture_size_and_tile_mode: [f32; 4],
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct BlurRoundedMaskUniforms {
direction_and_radius: [f32; 4],
texture_size_and_tile_mode: [f32; 4],
effect_rect: [f32; 4],
container_and_feather: [f32; 4],
corner_radii: [f32; 4],
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct OffsetUniforms {
offset: [f32; 2],
_padding: [f32; 2],
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct BlitUniforms {
alpha: [f32; 4],
mask_rect: [f32; 4],
mask_radii: [f32; 4],
mask_enabled: [f32; 4],
sampling: [f32; 4],
dest_viewport: [f32; 4],
source_viewport: [f32; 4],
resolve_span: [f32; 4],
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct ProjectiveBlitUniforms {
viewport: [f32; 2],
source_size: [f32; 2],
inverse_row0: [f32; 4],
inverse_row1: [f32; 4],
inverse_row2: [f32; 4],
alpha: [f32; 4],
sampling: [f32; 4],
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct ProjectiveBlitVertex {
position: [f32; 2],
}
fn blur_uniform_spec(horizontal: bool) -> UploadAllocatorSpec {
if horizontal {
UploadAllocatorSpec::uniform(
"Blur Horizontal Uniform Buffer",
"Blur Horizontal Uniform Bind Group",
std::mem::size_of::<BlurUniforms>() as u64,
)
} else {
UploadAllocatorSpec::uniform(
"Blur Vertical Uniform Buffer",
"Blur Vertical Uniform Bind Group",
std::mem::size_of::<BlurUniforms>() as u64,
)
}
}
fn blur_rounded_mask_uniform_spec() -> UploadAllocatorSpec {
UploadAllocatorSpec::uniform(
"Blur Rounded Mask Uniform Buffer",
"Blur Rounded Mask Uniform Bind Group",
std::mem::size_of::<BlurRoundedMaskUniforms>() as u64,
)
}
fn offset_uniform_spec() -> UploadAllocatorSpec {
UploadAllocatorSpec::uniform(
"Offset Uniform Buffer",
"Offset Uniform Bind Group",
std::mem::size_of::<OffsetUniforms>() as u64,
)
}
fn blit_uniform_spec() -> UploadAllocatorSpec {
UploadAllocatorSpec::uniform(
"Blit Uniform Buffer",
"Blit Uniform Bind Group",
std::mem::size_of::<BlitUniforms>() as u64,
)
}
fn projective_blit_uniform_spec() -> UploadAllocatorSpec {
UploadAllocatorSpec::uniform(
"Projective Blit Uniform Buffer",
"Projective Blit Uniform Bind Group",
std::mem::size_of::<ProjectiveBlitUniforms>() as u64,
)
}
fn projective_blit_vertex_spec() -> UploadAllocatorSpec {
UploadAllocatorSpec::vertex(
"Projective Blit Vertex Buffer",
(std::mem::size_of::<ProjectiveBlitVertex>() * 4) as u64,
)
}
fn effect_uniform_spec() -> UploadAllocatorSpec {
UploadAllocatorSpec::uniform(
"Effect Uniform Buffer",
"Effect Uniform Bind Group",
(RuntimeShader::MAX_UNIFORMS * std::mem::size_of::<f32>()) as u64,
)
}
#[derive(Copy, Clone, Debug)]
pub(crate) struct RoundedCompositeMask {
pub rect: [f32; 4],
pub radii: [f32; 4],
}
#[derive(Clone, Copy)]
struct CompositePassOptions {
alpha: f32,
load_op: wgpu::LoadOp<wgpu::Color>,
scissor: Option<(u32, u32, u32, u32)>,
rounded_mask: Option<RoundedCompositeMask>,
blend_mode: BlendMode,
dest_viewport: Option<(f32, f32, f32, f32)>,
source_viewport: Option<(f32, f32, f32, f32)>,
sample_mode: CompositeSampleMode,
}
#[derive(Clone, Copy)]
struct ShaderPassOptions {
load_op: wgpu::LoadOp<wgpu::Color>,
scissor: Option<(u32, u32, u32, u32)>,
dest_viewport: Option<(f32, f32, f32, f32)>,
pipeline_mode: RuntimeShaderPipelineMode,
}
#[derive(Clone, Copy)]
pub(crate) struct ShaderCompositeBatchItem<'a> {
pub(crate) source: &'a OffscreenTarget,
pub(crate) shader: &'a RuntimeShader,
pub(crate) layer_pixel_rect: [f32; 4],
pub(crate) scissor: Option<(u32, u32, u32, u32)>,
pub(crate) dest_viewport: (f32, f32, f32, f32),
}
#[derive(Clone, Copy)]
pub(crate) struct CompositeBatchItem<'a> {
pub(crate) source: &'a OffscreenTarget,
pub(crate) alpha: f32,
pub(crate) scissor: Option<(u32, u32, u32, u32)>,
pub(crate) rounded_mask: Option<RoundedCompositeMask>,
pub(crate) blend_mode: BlendMode,
pub(crate) dest_viewport: Option<(f32, f32, f32, f32)>,
pub(crate) source_viewport: Option<(f32, f32, f32, f32)>,
pub(crate) sample_mode: CompositeSampleMode,
}
pub(crate) struct PreparedCompositeDraw<'a> {
texture_bind_group: &'a wgpu::BindGroup,
uniform_bind_group: wgpu::BindGroup,
scissor: Option<(u32, u32, u32, u32)>,
blend_mode: BlendMode,
}
pub(crate) struct PreparedShaderDraw<'a> {
shader: &'a RuntimeShader,
texture_bind_group: &'a wgpu::BindGroup,
uniform_bind_group: wgpu::BindGroup,
scissor: Option<(u32, u32, u32, u32)>,
dest_viewport: (f32, f32, f32, f32),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum CompositeSampleMode {
Linear,
Box4,
}
impl EffectRenderer {
pub fn new(
device: &wgpu::Device,
surface_format: wgpu::TextureFormat,
adapter_backend: wgpu::Backend,
) -> Self {
let effect_texture_bind_group_layout = OffscreenPool::texture_bind_group_layout(device);
let effect_uniform_bind_group_layout = OffscreenPool::uniform_bind_group_layout(device);
let blur_uniform_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Blur Uniform Bind Group Layout"),
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 offset_uniform_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Offset Uniform Bind Group Layout"),
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 blit_uniform_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Blit Uniform Bind Group Layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
});
let blur_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Blur Shader"),
source: wgpu::ShaderSource::Wgsl(shaders::blur_shader().into()),
});
let blur_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Blur Pipeline Layout"),
bind_group_layouts: &[
Some(&effect_texture_bind_group_layout),
Some(&blur_uniform_bind_group_layout),
],
immediate_size: 0,
});
let blur_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Blur Pipeline"),
layout: Some(&blur_pipeline_layout),
vertex: wgpu::VertexState {
module: &blur_shader,
entry_point: Some("fullscreen_vs"),
buffers: &[],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &blur_shader,
entry_point: Some("blur_fs"),
targets: &[Some(wgpu::ColorTargetState {
format: surface_format,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleStrip,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
let blur_rounded_mask_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Blur Rounded Mask Shader"),
source: wgpu::ShaderSource::Wgsl(shaders::blur_rounded_mask_shader().into()),
});
let blur_rounded_mask_pipeline =
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Blur Rounded Mask Pipeline"),
layout: Some(&blur_pipeline_layout),
vertex: wgpu::VertexState {
module: &blur_rounded_mask_shader,
entry_point: Some("fullscreen_vs"),
buffers: &[],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &blur_rounded_mask_shader,
entry_point: Some("blur_rounded_mask_fs"),
targets: &[Some(wgpu::ColorTargetState {
format: surface_format,
blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleStrip,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
let offset_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Offset Shader"),
source: wgpu::ShaderSource::Wgsl(shaders::offset_shader().into()),
});
let offset_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Offset Pipeline Layout"),
bind_group_layouts: &[
Some(&effect_texture_bind_group_layout),
Some(&offset_uniform_bind_group_layout),
],
immediate_size: 0,
});
let offset_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Offset Pipeline"),
layout: Some(&offset_pipeline_layout),
vertex: wgpu::VertexState {
module: &offset_shader,
entry_point: Some("fullscreen_vs"),
buffers: &[],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &offset_shader,
entry_point: Some("offset_fs"),
targets: &[Some(wgpu::ColorTargetState {
format: surface_format,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleStrip,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
let blit_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Blit Shader"),
source: wgpu::ShaderSource::Wgsl(shaders::blit_shader().into()),
});
let blit_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Blit Pipeline Layout"),
bind_group_layouts: &[
Some(&effect_texture_bind_group_layout),
Some(&blit_uniform_bind_group_layout),
],
immediate_size: 0,
});
let blit_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Blit Pipeline"),
layout: Some(&blit_pipeline_layout),
vertex: wgpu::VertexState {
module: &blit_shader,
entry_point: Some("fullscreen_vs"),
buffers: &[],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &blit_shader,
entry_point: Some("blit_fs"),
targets: &[Some(wgpu::ColorTargetState {
format: surface_format,
blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleStrip,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
let blit_pipeline_dst_out =
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Blit Pipeline DstOut"),
layout: Some(&blit_pipeline_layout),
vertex: wgpu::VertexState {
module: &blit_shader,
entry_point: Some("fullscreen_vs"),
buffers: &[],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &blit_shader,
entry_point: Some("blit_fs"),
targets: &[Some(wgpu::ColorTargetState {
format: surface_format,
blend: Some(wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::Zero,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::Zero,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
}),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleStrip,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
let projective_blit_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Projective Blit Shader"),
source: wgpu::ShaderSource::Wgsl(shaders::projective_blit_shader().into()),
});
let projective_blit_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Projective Blit Pipeline Layout"),
bind_group_layouts: &[
Some(&effect_texture_bind_group_layout),
Some(&blit_uniform_bind_group_layout),
],
immediate_size: 0,
});
let projective_blit_vertex_layout = wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<ProjectiveBlitVertex>() as u64,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[wgpu::VertexAttribute {
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x2,
}],
};
let projective_blit_pipeline =
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Projective Blit Pipeline"),
layout: Some(&projective_blit_pipeline_layout),
vertex: wgpu::VertexState {
module: &projective_blit_shader,
entry_point: Some("projective_blit_vs"),
buffers: std::slice::from_ref(&projective_blit_vertex_layout),
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &projective_blit_shader,
entry_point: Some("projective_blit_fs"),
targets: &[Some(wgpu::ColorTargetState {
format: surface_format,
blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleStrip,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
let projective_blit_pipeline_dst_out =
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Projective Blit Pipeline DstOut"),
layout: Some(&projective_blit_pipeline_layout),
vertex: wgpu::VertexState {
module: &projective_blit_shader,
entry_point: Some("projective_blit_vs"),
buffers: &[projective_blit_vertex_layout],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &projective_blit_shader,
entry_point: Some("projective_blit_fs"),
targets: &[Some(wgpu::ColorTargetState {
format: surface_format,
blend: Some(wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::Zero,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::Zero,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
}),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleStrip,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
let effect_linear_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("Effect Linear Sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
..Default::default()
});
Self {
offscreen_pool: OffscreenPool::new(device, surface_format),
shader_cache: ShaderPipelineCache::new(adapter_backend),
blur_pipeline,
blur_rounded_mask_pipeline,
blur_uniform_bind_group_layout,
offset_pipeline,
offset_uniform_bind_group_layout,
blit_pipeline,
blit_pipeline_dst_out,
blit_uniform_bind_group_layout,
projective_blit_pipeline,
projective_blit_pipeline_dst_out,
effect_texture_bind_group_layout,
effect_uniform_bind_group_layout,
effect_linear_sampler,
surface_format,
debug_command_stats: Cell::new(FrameCommandStats::default()),
debug_blurs: Cell::new(0),
debug_composites: Cell::new(0),
debug_effects: Cell::new(0),
debug_upload_bytes: Cell::new(0),
}
}
fn sampler_for_mode(&self, _sample_mode: CompositeSampleMode) -> &wgpu::Sampler {
&self.effect_linear_sampler
}
pub(crate) fn max_texture_dim(&self) -> u32 {
self.offscreen_pool.max_texture_dim()
}
pub(crate) fn acquire_offscreen(
&mut self,
device: &wgpu::Device,
width: u32,
height: u32,
stats: Option<&FrameStats>,
) -> OffscreenTarget {
self.offscreen_pool.acquire(device, width, height, stats)
}
pub(crate) fn release_offscreen(&mut self, target: OffscreenTarget) {
self.offscreen_pool.release(target);
}
pub(crate) fn retained_offscreen_count(&self) -> usize {
self.offscreen_pool.pool_size()
}
pub(crate) fn retained_offscreen_bytes(&self) -> usize {
self.offscreen_pool.estimated_bytes()
}
pub(crate) fn merge_and_reset_debug_counters(&mut self, stats: &FrameStats) {
stats.record_command_stats(self.debug_command_stats.get());
stats
.blur_passes
.set(stats.blur_passes.get() + self.debug_blurs.get());
stats
.composite_passes
.set(stats.composite_passes.get() + self.debug_composites.get());
stats
.effect_applies
.set(stats.effect_applies.get() + self.debug_effects.get());
stats.record_upload_bytes(self.debug_upload_bytes.get());
self.debug_command_stats.set(FrameCommandStats::default());
self.debug_blurs.set(0);
self.debug_composites.set(0);
self.debug_effects.set(0);
self.debug_upload_bytes.set(0);
}
pub(crate) fn record_blur_pass(&self) {
self.debug_blurs.set(self.debug_blurs.get() + 1);
}
pub(crate) fn record_composite_pass(&self) {
self.debug_composites.set(self.debug_composites.get() + 1);
}
pub(crate) fn acquire_recorded_effect_scratch_targets<C: FrameCommandRecorder>(
&mut self,
recorder: &mut C,
device: &wgpu::Device,
effect: &RenderEffect,
width: u32,
height: u32,
format: wgpu::TextureFormat,
) -> RecordedEffectScratchTargets {
let mut targets = RecordedEffectScratchTargets::new();
acquire_recorded_effect_scratch_textures_into(
recorder,
device,
effect,
width,
height,
format,
&mut targets,
);
targets
}
#[allow(clippy::too_many_arguments)]
fn encode_blur_axis_pass<C: FrameCommandRecorder>(
&mut self,
recorder: &mut C,
device: &wgpu::Device,
source: &OffscreenTarget,
dest_view: &wgpu::TextureView,
uniforms: BlurUniforms,
horizontal: bool,
load_op: wgpu::LoadOp<wgpu::Color>,
scissor: Option<(u32, u32, u32, u32)>,
) {
let source_bind_group = source.get_or_create_bind_group(
device,
&self.effect_texture_bind_group_layout,
&self.effect_linear_sampler,
);
let uniform_bind_group = recorder.upload_uniform(
if horizontal {
UploadAllocatorId::BlurHorizontal
} else {
UploadAllocatorId::BlurVertical
},
blur_uniform_spec(horizontal),
device,
&self.blur_uniform_bind_group_layout,
bytemuck::bytes_of(&uniforms),
&self.debug_upload_bytes,
);
let mut pass = recorder
.encoder()
.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some(if horizontal {
"Blur Horizontal Pass"
} else {
"Blur Vertical Pass"
}),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: dest_view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: load_op,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
..Default::default()
});
pass.set_pipeline(&self.blur_pipeline);
pass.set_bind_group(0, source_bind_group, &[]);
pass.set_bind_group(1, &uniform_bind_group, &[]);
if let Some((x, y, w, h)) = scissor {
pass.set_scissor_rect(x, y, w, h);
}
pass.draw(0..4, 0..1);
}
fn blur_uniforms(
horizontal: bool,
width: u32,
height: u32,
radius_x: f32,
radius_y: f32,
tile_mode: TileMode,
) -> BlurUniforms {
let direction = if horizontal { [1.0, 0.0] } else { [0.0, 1.0] };
BlurUniforms {
direction_and_radius: [direction[0], direction[1], radius_x, radius_y],
texture_size_and_tile_mode: [
width as f32,
height as f32,
tile_mode_uniform_value(tile_mode),
0.0,
],
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn encode_blur_scissored_ping_pong_passes<C: FrameCommandRecorder>(
&mut self,
recorder: &mut C,
device: &wgpu::Device,
source: &OffscreenTarget,
scratch: &OffscreenTarget,
dest_view: &wgpu::TextureView,
radius_x: f32,
radius_y: f32,
tile_mode: TileMode,
scissor: Option<(u32, u32, u32, u32)>,
) {
debug_assert!(
radius_x > 0.0 || radius_y > 0.0,
"zero-radius blur should use the composite fast path"
);
debug_assert_eq!(scratch.width, source.width);
debug_assert_eq!(scratch.height, source.height);
self.encode_blur_axis_pass(
recorder,
device,
source,
&scratch.view,
Self::blur_uniforms(
true,
source.width,
source.height,
radius_x,
radius_y,
tile_mode,
),
true,
wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
scissor,
);
self.encode_blur_axis_pass(
recorder,
device,
scratch,
dest_view,
Self::blur_uniforms(
false,
source.width,
source.height,
radius_x,
radius_y,
tile_mode,
),
false,
wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
scissor,
);
}
fn rounded_mask_uniforms(
shader: &RuntimeShader,
layer_pixel_rect: [f32; 4],
width: u32,
height: u32,
radius_x: f32,
radius_y: f32,
tile_mode: TileMode,
) -> Option<BlurRoundedMaskUniforms> {
if shader.source() != ROUNDED_ALPHA_MASK_WGSL {
return None;
}
let uniforms = shader.uniforms();
let get = |index: usize| uniforms.get(index).copied().unwrap_or(0.0);
Some(BlurRoundedMaskUniforms {
direction_and_radius: [0.0, 1.0, radius_x, radius_y],
texture_size_and_tile_mode: [
width as f32,
height as f32,
tile_mode_uniform_value(tile_mode),
0.0,
],
effect_rect: layer_pixel_rect,
container_and_feather: [get(0).max(1.0), get(1).max(1.0), get(2).max(0.0), 0.0],
corner_radii: [
get(3).max(0.0),
get(4).max(0.0),
get(5).max(0.0),
get(6).max(0.0),
],
})
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn encode_blur_then_rounded_mask_src_over_to_view<C: FrameCommandRecorder>(
&mut self,
recorder: &mut C,
device: &wgpu::Device,
source: &OffscreenTarget,
scratch: &OffscreenTarget,
dest_view: &wgpu::TextureView,
radius_x: f32,
radius_y: f32,
tile_mode: TileMode,
shader: &RuntimeShader,
layer_pixel_rect: [f32; 4],
load_op: wgpu::LoadOp<wgpu::Color>,
scissor: Option<(u32, u32, u32, u32)>,
dest_viewport: (f32, f32, f32, f32),
) -> bool {
if dest_viewport.2 <= 0.0 || dest_viewport.3 <= 0.0 {
return false;
}
let Some(mask_uniforms) = Self::rounded_mask_uniforms(
shader,
layer_pixel_rect,
source.width,
source.height,
radius_x,
radius_y,
tile_mode,
) else {
return false;
};
debug_assert_eq!(scratch.width, source.width);
debug_assert_eq!(scratch.height, source.height);
self.encode_blur_axis_pass(
recorder,
device,
source,
&scratch.view,
Self::blur_uniforms(
true,
source.width,
source.height,
radius_x,
radius_y,
tile_mode,
),
true,
wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
None,
);
let scratch_bind_group = scratch.get_or_create_bind_group(
device,
&self.effect_texture_bind_group_layout,
&self.effect_linear_sampler,
);
let uniform_bind_group = recorder.upload_uniform(
UploadAllocatorId::BlurRoundedMask,
blur_rounded_mask_uniform_spec(),
device,
&self.blur_uniform_bind_group_layout,
bytemuck::bytes_of(&mask_uniforms),
&self.debug_upload_bytes,
);
let mut pass = recorder
.encoder()
.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Blur Rounded Mask Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: dest_view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: load_op,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
..Default::default()
});
pass.set_pipeline(&self.blur_rounded_mask_pipeline);
pass.set_bind_group(0, scratch_bind_group, &[]);
pass.set_bind_group(1, &uniform_bind_group, &[]);
let (x, y, width, height) = dest_viewport;
pass.set_viewport(x, y, width, height, 0.0, 1.0);
if let Some((x, y, w, h)) = scissor {
pass.set_scissor_rect(x, y, w, h);
}
pass.draw(0..4, 0..1);
true
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn encode_offset<C: FrameCommandRecorder>(
&mut self,
recorder: &mut C,
device: &wgpu::Device,
source: &OffscreenTarget,
dest_view: &wgpu::TextureView,
offset_x: f32,
offset_y: f32,
) {
let uniforms = OffsetUniforms {
offset: [offset_x, offset_y],
_padding: [0.0; 2],
};
let uniform_bind_group = recorder.upload_uniform(
UploadAllocatorId::Offset,
offset_uniform_spec(),
device,
&self.offset_uniform_bind_group_layout,
bytemuck::bytes_of(&uniforms),
&self.debug_upload_bytes,
);
let texture_bind_group = source.get_or_create_bind_group(
device,
&self.effect_texture_bind_group_layout,
&self.effect_linear_sampler,
);
let mut pass = recorder
.encoder()
.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Offset Effect Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: dest_view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
..Default::default()
});
pass.set_pipeline(&self.offset_pipeline);
pass.set_bind_group(0, texture_bind_group, &[]);
pass.set_bind_group(1, &uniform_bind_group, &[]);
pass.draw(0..4, 0..1);
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn encode_shader<C: FrameCommandRecorder>(
&mut self,
recorder: &mut C,
device: &wgpu::Device,
source: &OffscreenTarget,
dest_view: &wgpu::TextureView,
shader: &RuntimeShader,
layer_pixel_rect: [f32; 4],
) -> bool {
self.encode_shader_pass(
recorder,
device,
source,
dest_view,
shader,
layer_pixel_rect,
ShaderPassOptions {
load_op: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
scissor: None,
dest_viewport: None,
pipeline_mode: RuntimeShaderPipelineMode::Replace,
},
)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn encode_shader_src_over_to_view<C: FrameCommandRecorder>(
&mut self,
recorder: &mut C,
device: &wgpu::Device,
source: &OffscreenTarget,
dest_view: &wgpu::TextureView,
shader: &RuntimeShader,
layer_pixel_rect: [f32; 4],
load_op: wgpu::LoadOp<wgpu::Color>,
scissor: Option<(u32, u32, u32, u32)>,
dest_viewport: (f32, f32, f32, f32),
) -> bool {
if dest_viewport.2 <= 0.0 || dest_viewport.3 <= 0.0 {
return false;
}
self.encode_shader_pass(
recorder,
device,
source,
dest_view,
shader,
layer_pixel_rect,
ShaderPassOptions {
load_op,
scissor,
dest_viewport: Some(dest_viewport),
pipeline_mode: RuntimeShaderPipelineMode::PremultipliedSrcOver,
},
)
}
pub(crate) fn encode_shader_batch_src_over_to_view<C: FrameCommandRecorder>(
&mut self,
recorder: &mut C,
device: &wgpu::Device,
dest_view: &wgpu::TextureView,
viewport: (u32, u32),
load_op: wgpu::LoadOp<wgpu::Color>,
items: &[ShaderCompositeBatchItem<'_>],
) -> bool {
if items.is_empty() {
return true;
}
let Some(prepared) = self.prepare_shader_batch_draws(recorder, device, items) else {
return false;
};
let mut pass = recorder
.encoder()
.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Batched Shader Effect Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: dest_view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: load_op,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
..Default::default()
});
for draw in &prepared {
self.draw_prepared_shader_src_over(device, &mut pass, viewport, draw);
}
true
}
pub(crate) fn prepare_shader_batch_draws<'a, C: FrameCommandRecorder>(
&mut self,
recorder: &mut C,
device: &wgpu::Device,
items: &'a [ShaderCompositeBatchItem<'a>],
) -> Option<Vec<PreparedShaderDraw<'a>>> {
let mut prepared = Vec::with_capacity(items.len());
for item in items {
self.shader_cache.get_or_create(
device,
item.shader,
self.surface_format,
&self.effect_texture_bind_group_layout,
&self.effect_uniform_bind_group_layout,
RuntimeShaderPipelineMode::PremultipliedSrcOver,
)?;
let mut padded = item.shader.uniforms_padded();
let slot = RuntimeShader::RESERVED_UNIFORM_START;
padded[slot] = item.layer_pixel_rect[0];
padded[slot + 1] = item.layer_pixel_rect[1];
padded[slot + 2] = item.layer_pixel_rect[2];
padded[slot + 3] = item.layer_pixel_rect[3];
let uniform_bind_group = recorder.upload_uniform(
UploadAllocatorId::EffectUniform,
effect_uniform_spec(),
device,
&self.effect_uniform_bind_group_layout,
bytemuck::cast_slice(&padded),
&self.debug_upload_bytes,
);
let texture_bind_group = item.source.get_or_create_bind_group(
device,
&self.effect_texture_bind_group_layout,
&self.effect_linear_sampler,
);
prepared.push(PreparedShaderDraw {
shader: item.shader,
texture_bind_group,
uniform_bind_group,
scissor: item.scissor,
dest_viewport: item.dest_viewport,
});
}
Some(prepared)
}
pub(crate) fn draw_prepared_shader_src_over(
&mut self,
device: &wgpu::Device,
pass: &mut wgpu::RenderPass<'_>,
viewport: (u32, u32),
draw: &PreparedShaderDraw<'_>,
) {
let pipeline = self
.shader_cache
.get_or_create(
device,
draw.shader,
self.surface_format,
&self.effect_texture_bind_group_layout,
&self.effect_uniform_bind_group_layout,
RuntimeShaderPipelineMode::PremultipliedSrcOver,
)
.expect("shader batch pipeline was prevalidated");
pass.set_pipeline(pipeline);
pass.set_bind_group(0, draw.texture_bind_group, &[]);
pass.set_bind_group(1, &draw.uniform_bind_group, &[]);
let (x, y, width, height) = draw.dest_viewport;
pass.set_viewport(x, y, width, height, 0.0, 1.0);
if let Some((x, y, width, height)) = draw.scissor {
pass.set_scissor_rect(x, y, width, height);
} else {
pass.set_scissor_rect(0, 0, viewport.0, viewport.1);
}
pass.draw(0..4, 0..1);
}
#[allow(clippy::too_many_arguments)]
fn encode_shader_pass<C: FrameCommandRecorder>(
&mut self,
recorder: &mut C,
device: &wgpu::Device,
source: &OffscreenTarget,
dest_view: &wgpu::TextureView,
shader: &RuntimeShader,
layer_pixel_rect: [f32; 4],
options: ShaderPassOptions,
) -> bool {
let mut padded = shader.uniforms_padded();
let slot = RuntimeShader::RESERVED_UNIFORM_START;
padded[slot] = layer_pixel_rect[0];
padded[slot + 1] = layer_pixel_rect[1];
padded[slot + 2] = layer_pixel_rect[2];
padded[slot + 3] = layer_pixel_rect[3];
let uniform_bind_group = recorder.upload_uniform(
UploadAllocatorId::EffectUniform,
effect_uniform_spec(),
device,
&self.effect_uniform_bind_group_layout,
bytemuck::cast_slice(&padded),
&self.debug_upload_bytes,
);
let Some(pipeline) = self.shader_cache.get_or_create(
device,
shader,
self.surface_format,
&self.effect_texture_bind_group_layout,
&self.effect_uniform_bind_group_layout,
options.pipeline_mode,
) else {
return false;
};
let texture_bind_group = source.get_or_create_bind_group(
device,
&self.effect_texture_bind_group_layout,
&self.effect_linear_sampler,
);
let mut pass = recorder
.encoder()
.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Shader Effect Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: dest_view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: options.load_op,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
..Default::default()
});
pass.set_pipeline(pipeline);
pass.set_bind_group(0, texture_bind_group, &[]);
pass.set_bind_group(1, &uniform_bind_group, &[]);
if let Some((x, y, width, height)) = options.dest_viewport {
pass.set_viewport(x, y, width, height, 0.0, 1.0);
}
if let Some((x, y, width, height)) = options.scissor {
pass.set_scissor_rect(x, y, width, height);
}
pass.draw(0..4, 0..1);
true
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn encode_effect<'scratch, C: FrameCommandRecorder>(
&mut self,
recorder: &mut C,
device: &wgpu::Device,
source: &OffscreenTarget,
dest_view: &wgpu::TextureView,
effect: &RenderEffect,
layer_pixel_rect: [f32; 4],
scratch_targets: &mut impl EffectScratchTargetProvider<'scratch>,
) -> Result<u32, String> {
match effect {
RenderEffect::Blur {
radius_x,
radius_y,
edge_treatment,
} => {
if *radius_x <= 0.0 && *radius_y <= 0.0 {
self.encode_composite_to_view_pass(
recorder,
device,
source,
dest_view,
CompositePassOptions {
alpha: 1.0,
load_op: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
scissor: None,
rounded_mask: None,
blend_mode: BlendMode::SrcOver,
dest_viewport: None,
source_viewport: None,
sample_mode: CompositeSampleMode::Linear,
},
);
self.record_composite_pass();
return Ok(1);
}
let intermediate = scratch_targets.next()?;
self.encode_blur_scissored_ping_pong_passes(
recorder,
device,
source,
intermediate,
dest_view,
*radius_x,
*radius_y,
*edge_treatment,
None,
);
self.record_blur_pass();
Ok(2)
}
RenderEffect::Offset { offset_x, offset_y } => {
self.encode_offset(recorder, device, source, dest_view, *offset_x, *offset_y);
self.debug_effects.set(self.debug_effects.get() + 1);
Ok(1)
}
RenderEffect::Shader { shader } => {
if self.encode_shader(
recorder,
device,
source,
dest_view,
shader,
layer_pixel_rect,
) {
self.debug_effects.set(self.debug_effects.get() + 1);
Ok(1)
} else {
self.encode_composite_to_view_pass(
recorder,
device,
source,
dest_view,
CompositePassOptions {
alpha: 1.0,
load_op: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
scissor: None,
rounded_mask: None,
blend_mode: BlendMode::SrcOver,
dest_viewport: None,
source_viewport: None,
sample_mode: CompositeSampleMode::Linear,
},
);
self.record_composite_pass();
Ok(1)
}
}
RenderEffect::Chain { first, second } => {
let intermediate = scratch_targets.next()?;
let first_passes = self.encode_effect(
recorder,
device,
source,
&intermediate.view,
first,
layer_pixel_rect,
scratch_targets,
)?;
let second_passes = self.encode_effect(
recorder,
device,
intermediate,
dest_view,
second,
layer_pixel_rect,
scratch_targets,
)?;
Ok(first_passes.saturating_add(second_passes))
}
}
}
#[allow(clippy::too_many_arguments)]
fn composite_pass_uniforms(
source: &OffscreenTarget,
options: CompositePassOptions,
) -> BlitUniforms {
let (mask_rect, mask_radii, mask_enabled) = if let Some(mask) = options.rounded_mask {
(mask.rect, mask.radii, [1.0, 0.0, 0.0, 0.0])
} else {
(
[0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0],
)
};
let dest_viewport_uniform = options.dest_viewport.unwrap_or((0.0, 0.0, 0.0, 0.0));
let source_viewport_uniform = options.source_viewport.unwrap_or((0.0, 0.0, 0.0, 0.0));
let source_span = options
.source_viewport
.filter(|(_, _, width, height)| *width > 0.0 && *height > 0.0)
.map(|(_, _, width, height)| (width, height))
.unwrap_or((source.width as f32, source.height as f32));
let resolve_span = options
.dest_viewport
.filter(|(_, _, width, height)| *width > 0.0 && *height > 0.0)
.map(|(_, _, width, height)| (source_span.0 / width, source_span.1 / height))
.unwrap_or((0.0, 0.0));
BlitUniforms {
alpha: [options.alpha.clamp(0.0, 1.0), 0.0, 0.0, 0.0],
mask_rect,
mask_radii,
mask_enabled,
sampling: [
composite_sampling_mode_value(options.sample_mode),
0.0,
0.0,
0.0,
],
dest_viewport: [
dest_viewport_uniform.0,
dest_viewport_uniform.1,
dest_viewport_uniform.2,
dest_viewport_uniform.3,
],
source_viewport: [
source_viewport_uniform.0,
source_viewport_uniform.1,
source_viewport_uniform.2,
source_viewport_uniform.3,
],
resolve_span: [resolve_span.0, resolve_span.1, 0.0, 0.0],
}
}
#[allow(clippy::too_many_arguments)]
fn encode_composite_to_view_pass<C: FrameCommandRecorder>(
&mut self,
recorder: &mut C,
device: &wgpu::Device,
source: &OffscreenTarget,
dest_view: &wgpu::TextureView,
options: CompositePassOptions,
) {
let uniforms = Self::composite_pass_uniforms(source, options);
let sampler = self.sampler_for_mode(options.sample_mode);
let texture_bind_group = source.get_or_create_bind_group(
device,
&self.effect_texture_bind_group_layout,
sampler,
);
let uniform_bind_group = recorder.upload_uniform(
UploadAllocatorId::Blit,
blit_uniform_spec(),
device,
&self.blit_uniform_bind_group_layout,
bytemuck::bytes_of(&uniforms),
&self.debug_upload_bytes,
);
let mut pass = recorder
.encoder()
.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Blit Composite Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: dest_view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: options.load_op,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
..Default::default()
});
pass.set_pipeline(match options.blend_mode {
BlendMode::DstOut => &self.blit_pipeline_dst_out,
_ => &self.blit_pipeline,
});
pass.set_bind_group(0, texture_bind_group, &[]);
pass.set_bind_group(1, &uniform_bind_group, &[]);
if let Some((x, y, w, h)) = options.scissor {
pass.set_scissor_rect(x, y, w, h);
}
pass.draw(0..4, 0..1);
}
pub(crate) fn encode_composite_batch_to_view_pass<C: FrameCommandRecorder>(
&mut self,
recorder: &mut C,
device: &wgpu::Device,
dest_view: &wgpu::TextureView,
viewport: (u32, u32),
load_op: wgpu::LoadOp<wgpu::Color>,
items: &[CompositeBatchItem<'_>],
) {
if items.is_empty() {
return;
}
let prepared = self.prepare_composite_batch_draws(recorder, device, load_op, items);
let mut pass = recorder
.encoder()
.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Batched Blit Composite Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: dest_view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: load_op,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
..Default::default()
});
for draw in &prepared {
self.draw_prepared_composite(&mut pass, viewport, draw);
}
}
pub(crate) fn prepare_composite_batch_draws<'a, C: FrameCommandRecorder>(
&mut self,
recorder: &mut C,
device: &wgpu::Device,
load_op: wgpu::LoadOp<wgpu::Color>,
items: &[CompositeBatchItem<'a>],
) -> Vec<PreparedCompositeDraw<'a>> {
let mut prepared = Vec::with_capacity(items.len());
for item in items {
let options = CompositePassOptions {
alpha: item.alpha,
load_op,
scissor: item.scissor,
rounded_mask: item.rounded_mask,
blend_mode: item.blend_mode,
dest_viewport: item.dest_viewport,
source_viewport: item.source_viewport,
sample_mode: item.sample_mode,
};
let uniforms = Self::composite_pass_uniforms(item.source, options);
let sampler = self.sampler_for_mode(item.sample_mode);
let texture_bind_group = item.source.get_or_create_bind_group(
device,
&self.effect_texture_bind_group_layout,
sampler,
);
let uniform_bind_group = recorder.upload_uniform(
UploadAllocatorId::Blit,
blit_uniform_spec(),
device,
&self.blit_uniform_bind_group_layout,
bytemuck::bytes_of(&uniforms),
&self.debug_upload_bytes,
);
prepared.push(PreparedCompositeDraw {
texture_bind_group,
uniform_bind_group,
scissor: item.scissor,
blend_mode: item.blend_mode,
});
}
prepared
}
pub(crate) fn draw_prepared_composite(
&self,
pass: &mut wgpu::RenderPass<'_>,
viewport: (u32, u32),
draw: &PreparedCompositeDraw<'_>,
) {
pass.set_pipeline(match draw.blend_mode {
BlendMode::DstOut => &self.blit_pipeline_dst_out,
_ => &self.blit_pipeline,
});
pass.set_bind_group(0, draw.texture_bind_group, &[]);
pass.set_bind_group(1, &draw.uniform_bind_group, &[]);
if let Some((x, y, w, h)) = draw.scissor {
pass.set_scissor_rect(x, y, w, h);
} else {
pass.set_scissor_rect(0, 0, viewport.0, viewport.1);
}
pass.draw(0..4, 0..1);
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode<
C: FrameCommandRecorder,
>(
&mut self,
recorder: &mut C,
device: &wgpu::Device,
source: &OffscreenTarget,
dest_view: &wgpu::TextureView,
alpha: f32,
load_op: wgpu::LoadOp<wgpu::Color>,
scissor: Option<(u32, u32, u32, u32)>,
rounded_mask: Option<RoundedCompositeMask>,
blend_mode: BlendMode,
dest_viewport: Option<(f32, f32, f32, f32)>,
sample_mode: CompositeSampleMode,
) {
self.encode_composite_to_view_pass(
recorder,
device,
source,
dest_view,
CompositePassOptions {
alpha,
load_op,
scissor,
rounded_mask,
blend_mode,
dest_viewport,
source_viewport: None,
sample_mode,
},
);
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn encode_composite_to_view_projective<C: FrameCommandRecorder>(
&self,
recorder: &mut C,
device: &wgpu::Device,
source: &OffscreenTarget,
dest_view: &wgpu::TextureView,
viewport: (u32, u32),
source_size: (f32, f32),
inverse_matrix: [[f32; 3]; 3],
dest_bounds: [[f32; 2]; 4],
alpha: f32,
load_op: wgpu::LoadOp<wgpu::Color>,
scissor: Option<(u32, u32, u32, u32)>,
blend_mode: BlendMode,
sample_mode: CompositeSampleMode,
) -> bool {
let Some((min_x, min_y, max_x, max_y)) = projective_dest_bounds_rect(dest_bounds) else {
return false;
};
let vertices = [
ProjectiveBlitVertex {
position: [min_x, min_y],
},
ProjectiveBlitVertex {
position: [max_x, min_y],
},
ProjectiveBlitVertex {
position: [min_x, max_y],
},
ProjectiveBlitVertex {
position: [max_x, max_y],
},
];
let uniforms = ProjectiveBlitUniforms {
viewport: [viewport.0 as f32, viewport.1 as f32],
source_size: [source_size.0.max(0.0), source_size.1.max(0.0)],
inverse_row0: [
inverse_matrix[0][0],
inverse_matrix[0][1],
inverse_matrix[0][2],
0.0,
],
inverse_row1: [
inverse_matrix[1][0],
inverse_matrix[1][1],
inverse_matrix[1][2],
0.0,
],
inverse_row2: [
inverse_matrix[2][0],
inverse_matrix[2][1],
inverse_matrix[2][2],
0.0,
],
alpha: [alpha.clamp(0.0, 1.0), 0.0, 0.0, 0.0],
sampling: [composite_sampling_mode_value(sample_mode), 0.0, 0.0, 0.0],
};
let sampler = self.sampler_for_mode(sample_mode);
let texture_bind_group = source.get_or_create_bind_group(
device,
&self.effect_texture_bind_group_layout,
sampler,
);
let vertex_buffer = recorder.upload_vertex(
UploadAllocatorId::ProjectiveBlitVertex,
projective_blit_vertex_spec(),
device,
bytemuck::cast_slice(&vertices),
&self.debug_upload_bytes,
);
let uniform_bind_group = recorder.upload_uniform(
UploadAllocatorId::ProjectiveBlitUniform,
projective_blit_uniform_spec(),
device,
&self.blit_uniform_bind_group_layout,
bytemuck::bytes_of(&uniforms),
&self.debug_upload_bytes,
);
let mut pass = recorder
.encoder()
.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Projective Blit Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: dest_view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: load_op,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
..Default::default()
});
pass.set_pipeline(match blend_mode {
BlendMode::DstOut => &self.projective_blit_pipeline_dst_out,
_ => &self.projective_blit_pipeline,
});
pass.set_bind_group(0, texture_bind_group, &[]);
pass.set_bind_group(1, &uniform_bind_group, &[]);
pass.set_vertex_buffer(0, vertex_buffer.slice(..));
if let Some((x, y, w, h)) = scissor {
pass.set_scissor_rect(x, y, w, h);
}
pass.draw(0..4, 0..1);
true
}
}
pub(crate) fn projective_dest_bounds_rect(
dest_bounds: [[f32; 2]; 4],
) -> Option<(f32, f32, f32, f32)> {
let min_x = dest_bounds
.iter()
.map(|point| point[0])
.fold(f32::INFINITY, f32::min);
let min_y = dest_bounds
.iter()
.map(|point| point[1])
.fold(f32::INFINITY, f32::min);
let max_x = dest_bounds
.iter()
.map(|point| point[0])
.fold(f32::NEG_INFINITY, f32::max);
let max_y = dest_bounds
.iter()
.map(|point| point[1])
.fold(f32::NEG_INFINITY, f32::max);
(min_x.is_finite()
&& min_y.is_finite()
&& max_x.is_finite()
&& max_y.is_finite()
&& max_x > min_x
&& max_y > min_y)
.then_some((min_x, min_y, max_x, max_y))
}
fn tile_mode_uniform_value(tile_mode: TileMode) -> f32 {
match tile_mode {
TileMode::Clamp => 0.0,
TileMode::Repeated => 1.0,
TileMode::Mirror => 2.0,
TileMode::Decal => 3.0,
}
}
fn composite_sampling_mode_value(sample_mode: CompositeSampleMode) -> f32 {
match sample_mode {
CompositeSampleMode::Linear => 0.0,
CompositeSampleMode::Box4 => 1.0,
}
}
#[cfg(test)]
mod tests {
use super::{projective_dest_bounds_rect, BlurUniforms};
#[test]
fn blur_uniforms_use_vec4_packing_for_gl_backends() {
assert_eq!(std::mem::size_of::<BlurUniforms>(), 32);
assert_eq!(std::mem::offset_of!(BlurUniforms, direction_and_radius), 0);
assert_eq!(
std::mem::offset_of!(BlurUniforms, texture_size_and_tile_mode),
16
);
}
#[test]
fn projective_dest_bounds_reject_degenerate_quads() {
assert_eq!(
projective_dest_bounds_rect([[2.0, 3.0], [2.0, 3.0], [2.0, 3.0], [2.0, 3.0]]),
None
);
assert_eq!(
projective_dest_bounds_rect([[1.0, 2.0], [5.0, 2.0], [1.0, 7.0], [5.0, 7.0]]),
Some((1.0, 2.0, 5.0, 7.0))
);
}
}