use oxiui_core::UiError;
use crate::gpu::device::TARGET_FORMAT;
pub const DEPTH_STENCIL_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth24PlusStencil8;
pub struct StencilTarget {
pub texture: wgpu::Texture,
pub view: wgpu::TextureView,
pub width: u32,
pub height: u32,
pub sample_count: u32,
}
impl StencilTarget {
pub fn new(
device: &wgpu::Device,
width: u32,
height: u32,
sample_count: u32,
) -> Result<Self, UiError> {
if width == 0 || height == 0 {
return Err(UiError::Unsupported(
"StencilTarget dimensions must be non-zero".to_string(),
));
}
let sc = sample_count.max(1);
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("oxiui-render-wgpu stencil target"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: sc,
dimension: wgpu::TextureDimension::D2,
format: DEPTH_STENCIL_FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
Ok(Self {
texture,
view,
width,
height,
sample_count: sc,
})
}
pub fn resize(
&mut self,
device: &wgpu::Device,
new_width: u32,
new_height: u32,
) -> Result<(), UiError> {
*self = Self::new(device, new_width, new_height, self.sample_count)?;
Ok(())
}
}
pub struct StencilWritePipeline {
pub write: wgpu::RenderPipeline,
pub clear: wgpu::RenderPipeline,
pub globals_layout: wgpu::BindGroupLayout,
}
impl StencilWritePipeline {
pub fn new(device: &wgpu::Device, sample_count: u32) -> Self {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("oxiui-render-wgpu stencil solid shader"),
source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/solid.wgsl").into()),
});
let globals_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("oxiui-render-wgpu stencil globals layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
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("oxiui-render-wgpu stencil pipeline layout"),
bind_group_layouts: &[Some(&globals_layout)],
immediate_size: 0,
});
let write_stencil_face = wgpu::StencilFaceState {
compare: wgpu::CompareFunction::Always,
fail_op: wgpu::StencilOperation::Keep,
depth_fail_op: wgpu::StencilOperation::Keep,
pass_op: wgpu::StencilOperation::Replace,
};
let attrs = vertex_attrs_56();
let vertex_layout = wgpu::VertexBufferLayout {
array_stride: 56,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &attrs,
};
let write = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("oxiui-render-wgpu stencil write pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: std::slice::from_ref(&vertex_layout),
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: TARGET_FORMAT,
blend: None,
write_mask: wgpu::ColorWrites::empty(), })],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
unclipped_depth: false,
polygon_mode: wgpu::PolygonMode::Fill,
conservative: false,
},
depth_stencil: Some(wgpu::DepthStencilState {
format: DEPTH_STENCIL_FORMAT,
depth_write_enabled: Some(false),
depth_compare: Some(wgpu::CompareFunction::Always),
stencil: wgpu::StencilState {
front: write_stencil_face,
back: write_stencil_face,
read_mask: 0xFF,
write_mask: 0xFF,
},
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState {
count: sample_count,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview_mask: None,
cache: None,
});
let clear_stencil_face = wgpu::StencilFaceState {
compare: wgpu::CompareFunction::Always,
fail_op: wgpu::StencilOperation::Zero,
depth_fail_op: wgpu::StencilOperation::Zero,
pass_op: wgpu::StencilOperation::Zero,
};
let clear = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("oxiui-render-wgpu stencil clear pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[vertex_layout],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: TARGET_FORMAT,
blend: None,
write_mask: wgpu::ColorWrites::empty(),
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
unclipped_depth: false,
polygon_mode: wgpu::PolygonMode::Fill,
conservative: false,
},
depth_stencil: Some(wgpu::DepthStencilState {
format: DEPTH_STENCIL_FORMAT,
depth_write_enabled: Some(false),
depth_compare: Some(wgpu::CompareFunction::Always),
stencil: wgpu::StencilState {
front: clear_stencil_face,
back: clear_stencil_face,
read_mask: 0xFF,
write_mask: 0xFF,
},
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState {
count: sample_count,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview_mask: None,
cache: None,
});
Self {
write,
clear,
globals_layout,
}
}
}
fn vertex_attrs_56() -> [wgpu::VertexAttribute; 7] {
[
wgpu::VertexAttribute {
format: wgpu::VertexFormat::Float32x2,
offset: 0,
shader_location: 0,
},
wgpu::VertexAttribute {
format: wgpu::VertexFormat::Float32x4,
offset: 8,
shader_location: 1,
},
wgpu::VertexAttribute {
format: wgpu::VertexFormat::Float32x2,
offset: 24,
shader_location: 2,
},
wgpu::VertexAttribute {
format: wgpu::VertexFormat::Float32x2,
offset: 32,
shader_location: 3,
},
wgpu::VertexAttribute {
format: wgpu::VertexFormat::Float32,
offset: 40,
shader_location: 4,
},
wgpu::VertexAttribute {
format: wgpu::VertexFormat::Float32,
offset: 44,
shader_location: 5,
},
wgpu::VertexAttribute {
format: wgpu::VertexFormat::Float32x2,
offset: 48,
shader_location: 6,
},
]
}
#[derive(Clone, Debug, Default)]
pub struct StencilClipState {
depth: u8,
}
impl StencilClipState {
pub fn reference(&self) -> u32 {
u32::from(self.depth)
}
pub fn push(&mut self) -> Option<u32> {
if self.depth >= 254 {
return None;
}
self.depth += 1;
Some(u32::from(self.depth))
}
pub fn pop(&mut self) {
self.depth = self.depth.saturating_sub(1);
}
pub fn reset(&mut self) {
self.depth = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
use oxiui_core::UiError;
fn try_device() -> Option<(wgpu::Device, wgpu::Queue)> {
let instance = wgpu::Instance::default();
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::default(),
force_fallback_adapter: false,
compatible_surface: None,
}))
.ok()?;
pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
label: Some("stencil test device"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::downlevel_defaults(),
memory_hints: wgpu::MemoryHints::Performance,
experimental_features: wgpu::ExperimentalFeatures::disabled(),
trace: wgpu::Trace::Off,
}))
.ok()
}
#[test]
fn stencil_clip_state_push_pop() {
let mut s = StencilClipState::default();
assert_eq!(s.reference(), 0);
let r = s.push().expect("first push");
assert_eq!(r, 1);
assert_eq!(s.reference(), 1);
s.pop();
assert_eq!(s.reference(), 0);
}
#[test]
fn stencil_clip_state_max_depth() {
let mut s = StencilClipState::default();
for _ in 0..254 {
assert!(s.push().is_some());
}
assert_eq!(s.reference(), 254);
assert!(s.push().is_none(), "should not exceed 254");
}
#[test]
fn stencil_clip_state_pop_at_zero_is_safe() {
let mut s = StencilClipState::default();
s.pop(); assert_eq!(s.reference(), 0);
}
#[test]
fn stencil_target_zero_dimensions_fail() {
let Some((device, _)) = try_device() else {
return;
};
assert!(matches!(
StencilTarget::new(&device, 0, 64, 1),
Err(UiError::Unsupported(_))
));
}
#[test]
fn stencil_target_creates_ok() {
let Some((device, _)) = try_device() else {
return;
};
let st = StencilTarget::new(&device, 64, 64, 1).expect("create stencil target");
assert_eq!(st.width, 64);
assert_eq!(st.height, 64);
assert_eq!(st.sample_count, 1);
}
#[test]
fn stencil_write_pipeline_compiles() {
let Some((device, _)) = try_device() else {
return;
};
let _pipeline = StencilWritePipeline::new(&device, 1);
}
#[test]
fn stencil_format_is_depth24plus_stencil8() {
assert_eq!(
DEPTH_STENCIL_FORMAT,
wgpu::TextureFormat::Depth24PlusStencil8
);
}
}