Skip to content

Commit 1317c1c

Browse files
authored
feat(lambda-rs): Add Depth tests, stencil tests, and MSAA
2 parents 5d1c36e + 2247841 commit 1317c1c

File tree

21 files changed

+3328
-97
lines changed

21 files changed

+3328
-97
lines changed

crates/lambda-rs-platform/src/wgpu/gpu.rs

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use super::{
44
command::CommandBuffer,
55
instance::Instance,
66
surface::Surface,
7+
texture,
78
};
89

910
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -197,6 +198,24 @@ pub struct Gpu {
197198
}
198199

199200
impl Gpu {
201+
/// Whether the provided surface format supports the sample count for render attachments.
202+
pub fn supports_sample_count_for_surface(
203+
&self,
204+
format: super::surface::SurfaceFormat,
205+
sample_count: u32,
206+
) -> bool {
207+
return self.supports_sample_count(format.to_wgpu(), sample_count);
208+
}
209+
210+
/// Whether the provided depth format supports the sample count for render attachments.
211+
pub fn supports_sample_count_for_depth(
212+
&self,
213+
format: texture::DepthFormat,
214+
sample_count: u32,
215+
) -> bool {
216+
return self.supports_sample_count(format.to_wgpu(), sample_count);
217+
}
218+
200219
/// Borrow the adapter used to create the device.
201220
///
202221
/// Crate-visible to avoid exposing raw `wgpu` to higher layers.
@@ -245,11 +264,50 @@ impl Gpu {
245264
let iter = list.into_iter().map(|cb| cb.into_raw());
246265
self.queue.submit(iter);
247266
}
267+
268+
fn supports_sample_count(
269+
&self,
270+
format: wgpu::TextureFormat,
271+
sample_count: u32,
272+
) -> bool {
273+
if sample_count <= 1 {
274+
return true;
275+
}
276+
277+
let features = self.adapter.get_texture_format_features(format);
278+
if !features
279+
.allowed_usages
280+
.contains(wgpu::TextureUsages::RENDER_ATTACHMENT)
281+
{
282+
return false;
283+
}
284+
285+
match sample_count {
286+
2 => features
287+
.flags
288+
.contains(wgpu::TextureFormatFeatureFlags::MULTISAMPLE_X2),
289+
4 => features
290+
.flags
291+
.contains(wgpu::TextureFormatFeatureFlags::MULTISAMPLE_X4),
292+
8 => features
293+
.flags
294+
.contains(wgpu::TextureFormatFeatureFlags::MULTISAMPLE_X8),
295+
16 => features
296+
.flags
297+
.contains(wgpu::TextureFormatFeatureFlags::MULTISAMPLE_X16),
298+
_ => false,
299+
}
300+
}
248301
}
249302

250303
#[cfg(test)]
251304
mod tests {
252305
use super::*;
306+
use crate::wgpu::{
307+
instance,
308+
surface,
309+
texture,
310+
};
253311

254312
#[test]
255313
fn gpu_build_error_wraps_request_device_error() {
@@ -260,4 +318,121 @@ mod tests {
260318
fn assert_from_impl<T: From<wgpu::RequestDeviceError>>() {}
261319
assert_from_impl::<GpuBuildError>();
262320
}
321+
322+
/// Create an offscreen GPU for sample-count support tests.
323+
///
324+
/// Returns `None` when no compatible adapter is available so tests can be
325+
/// skipped instead of failing.
326+
fn create_test_gpu() -> Option<Gpu> {
327+
let instance = instance::InstanceBuilder::new()
328+
.with_label("gpu-test-instance")
329+
.build();
330+
return GpuBuilder::new()
331+
.with_label("gpu-test-device")
332+
.build(&instance, None)
333+
.ok();
334+
}
335+
336+
/// Accepts zero or single-sample attachments for any format.
337+
#[test]
338+
fn single_sample_always_supported() {
339+
let gpu = match create_test_gpu() {
340+
Some(gpu) => gpu,
341+
None => {
342+
eprintln!(
343+
"Skipping single_sample_always_supported: no compatible GPU adapter"
344+
);
345+
return;
346+
}
347+
};
348+
let surface_format =
349+
surface::SurfaceFormat::from_wgpu(wgpu::TextureFormat::Bgra8UnormSrgb);
350+
let depth_format = texture::DepthFormat::Depth32Float;
351+
352+
assert!(gpu.supports_sample_count_for_surface(surface_format, 1));
353+
assert!(gpu.supports_sample_count_for_surface(surface_format, 0));
354+
assert!(gpu.supports_sample_count_for_depth(depth_format, 1));
355+
assert!(gpu.supports_sample_count_for_depth(depth_format, 0));
356+
}
357+
358+
/// Rejects sample counts that are outside the supported set.
359+
#[test]
360+
fn unsupported_sample_count_rejected() {
361+
let gpu = match create_test_gpu() {
362+
Some(gpu) => gpu,
363+
None => {
364+
eprintln!(
365+
"Skipping unsupported_sample_count_rejected: no compatible GPU adapter"
366+
);
367+
return;
368+
}
369+
};
370+
let surface_format =
371+
surface::SurfaceFormat::from_wgpu(wgpu::TextureFormat::Bgra8Unorm);
372+
let depth_format = texture::DepthFormat::Depth32Float;
373+
374+
assert!(!gpu.supports_sample_count_for_surface(surface_format, 3));
375+
assert!(!gpu.supports_sample_count_for_depth(depth_format, 3));
376+
}
377+
378+
/// Mirrors the adapter's texture feature flags for surface formats.
379+
#[test]
380+
fn surface_support_matches_texture_features() {
381+
let gpu = match create_test_gpu() {
382+
Some(gpu) => gpu,
383+
None => {
384+
eprintln!(
385+
"Skipping surface_support_matches_texture_features: \
386+
no compatible GPU adapter"
387+
);
388+
return;
389+
}
390+
};
391+
let surface_format =
392+
surface::SurfaceFormat::from_wgpu(wgpu::TextureFormat::Bgra8UnormSrgb);
393+
let features = gpu
394+
.adapter
395+
.get_texture_format_features(surface_format.to_wgpu());
396+
let expected = features
397+
.allowed_usages
398+
.contains(wgpu::TextureUsages::RENDER_ATTACHMENT)
399+
&& features
400+
.flags
401+
.contains(wgpu::TextureFormatFeatureFlags::MULTISAMPLE_X4);
402+
403+
assert_eq!(
404+
gpu.supports_sample_count_for_surface(surface_format, 4),
405+
expected
406+
);
407+
}
408+
409+
/// Mirrors the adapter's texture feature flags for depth formats.
410+
#[test]
411+
fn depth_support_matches_texture_features() {
412+
let gpu = match create_test_gpu() {
413+
Some(gpu) => gpu,
414+
None => {
415+
eprintln!(
416+
"Skipping depth_support_matches_texture_features: \
417+
no compatible GPU adapter"
418+
);
419+
return;
420+
}
421+
};
422+
let depth_format = texture::DepthFormat::Depth32Float;
423+
let features = gpu
424+
.adapter
425+
.get_texture_format_features(depth_format.to_wgpu());
426+
let expected = features
427+
.allowed_usages
428+
.contains(wgpu::TextureUsages::RENDER_ATTACHMENT)
429+
&& features
430+
.flags
431+
.contains(wgpu::TextureFormatFeatureFlags::MULTISAMPLE_X4);
432+
433+
assert_eq!(
434+
gpu.supports_sample_count_for_depth(depth_format, 4),
435+
expected
436+
);
437+
}
263438
}

crates/lambda-rs-platform/src/wgpu/pipeline.rs

Lines changed: 149 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,95 @@ pub struct VertexAttributeDesc {
8181
pub format: ColorFormat,
8282
}
8383

84+
/// Compare function used for depth and stencil tests.
85+
#[derive(Clone, Copy, Debug)]
86+
pub enum CompareFunction {
87+
Never,
88+
Less,
89+
LessEqual,
90+
Greater,
91+
GreaterEqual,
92+
Equal,
93+
NotEqual,
94+
Always,
95+
}
96+
97+
impl CompareFunction {
98+
fn to_wgpu(self) -> wgpu::CompareFunction {
99+
match self {
100+
CompareFunction::Never => wgpu::CompareFunction::Never,
101+
CompareFunction::Less => wgpu::CompareFunction::Less,
102+
CompareFunction::LessEqual => wgpu::CompareFunction::LessEqual,
103+
CompareFunction::Greater => wgpu::CompareFunction::Greater,
104+
CompareFunction::GreaterEqual => wgpu::CompareFunction::GreaterEqual,
105+
CompareFunction::Equal => wgpu::CompareFunction::Equal,
106+
CompareFunction::NotEqual => wgpu::CompareFunction::NotEqual,
107+
CompareFunction::Always => wgpu::CompareFunction::Always,
108+
}
109+
}
110+
}
111+
112+
/// Stencil operation applied when the stencil test or depth test passes/fails.
113+
#[derive(Clone, Copy, Debug)]
114+
pub enum StencilOperation {
115+
Keep,
116+
Zero,
117+
Replace,
118+
Invert,
119+
IncrementClamp,
120+
DecrementClamp,
121+
IncrementWrap,
122+
DecrementWrap,
123+
}
124+
125+
impl StencilOperation {
126+
fn to_wgpu(self) -> wgpu::StencilOperation {
127+
match self {
128+
StencilOperation::Keep => wgpu::StencilOperation::Keep,
129+
StencilOperation::Zero => wgpu::StencilOperation::Zero,
130+
StencilOperation::Replace => wgpu::StencilOperation::Replace,
131+
StencilOperation::Invert => wgpu::StencilOperation::Invert,
132+
StencilOperation::IncrementClamp => {
133+
wgpu::StencilOperation::IncrementClamp
134+
}
135+
StencilOperation::DecrementClamp => {
136+
wgpu::StencilOperation::DecrementClamp
137+
}
138+
StencilOperation::IncrementWrap => wgpu::StencilOperation::IncrementWrap,
139+
StencilOperation::DecrementWrap => wgpu::StencilOperation::DecrementWrap,
140+
}
141+
}
142+
}
143+
144+
/// Per-face stencil state.
145+
#[derive(Clone, Copy, Debug)]
146+
pub struct StencilFaceState {
147+
pub compare: CompareFunction,
148+
pub fail_op: StencilOperation,
149+
pub depth_fail_op: StencilOperation,
150+
pub pass_op: StencilOperation,
151+
}
152+
153+
impl StencilFaceState {
154+
fn to_wgpu(self) -> wgpu::StencilFaceState {
155+
wgpu::StencilFaceState {
156+
compare: self.compare.to_wgpu(),
157+
fail_op: self.fail_op.to_wgpu(),
158+
depth_fail_op: self.depth_fail_op.to_wgpu(),
159+
pass_op: self.pass_op.to_wgpu(),
160+
}
161+
}
162+
}
163+
164+
/// Full stencil state (front/back + masks).
165+
#[derive(Clone, Copy, Debug)]
166+
pub struct StencilState {
167+
pub front: StencilFaceState,
168+
pub back: StencilFaceState,
169+
pub read_mask: u32,
170+
pub write_mask: u32,
171+
}
172+
84173
/// Wrapper around `wgpu::ShaderModule` that preserves a label.
85174
#[derive(Debug)]
86175
pub struct ShaderModule {
@@ -202,6 +291,10 @@ impl RenderPipeline {
202291
pub(crate) fn into_raw(self) -> wgpu::RenderPipeline {
203292
return self.raw;
204293
}
294+
/// Pipeline label if provided.
295+
pub fn label(&self) -> Option<&str> {
296+
return self.label.as_deref();
297+
}
205298
}
206299

207300
/// Builder for creating a graphics render pipeline.
@@ -212,6 +305,7 @@ pub struct RenderPipelineBuilder<'a> {
212305
cull_mode: CullingMode,
213306
color_target_format: Option<wgpu::TextureFormat>,
214307
depth_stencil: Option<wgpu::DepthStencilState>,
308+
sample_count: u32,
215309
}
216310

217311
impl<'a> RenderPipelineBuilder<'a> {
@@ -224,6 +318,7 @@ impl<'a> RenderPipelineBuilder<'a> {
224318
cull_mode: CullingMode::Back,
225319
color_target_format: None,
226320
depth_stencil: None,
321+
sample_count: 1,
227322
};
228323
}
229324

@@ -275,6 +370,56 @@ impl<'a> RenderPipelineBuilder<'a> {
275370
return self;
276371
}
277372

373+
/// Set the depth compare function. Requires depth to be enabled.
374+
pub fn with_depth_compare(mut self, compare: CompareFunction) -> Self {
375+
let ds = self.depth_stencil.get_or_insert(wgpu::DepthStencilState {
376+
format: wgpu::TextureFormat::Depth32Float,
377+
depth_write_enabled: true,
378+
depth_compare: wgpu::CompareFunction::Less,
379+
stencil: wgpu::StencilState::default(),
380+
bias: wgpu::DepthBiasState::default(),
381+
});
382+
ds.depth_compare = compare.to_wgpu();
383+
return self;
384+
}
385+
386+
/// Enable or disable depth writes. Requires depth-stencil enabled.
387+
pub fn with_depth_write_enabled(mut self, enabled: bool) -> Self {
388+
let ds = self.depth_stencil.get_or_insert(wgpu::DepthStencilState {
389+
format: wgpu::TextureFormat::Depth32Float,
390+
depth_write_enabled: true,
391+
depth_compare: wgpu::CompareFunction::Less,
392+
stencil: wgpu::StencilState::default(),
393+
bias: wgpu::DepthBiasState::default(),
394+
});
395+
ds.depth_write_enabled = enabled;
396+
return self;
397+
}
398+
399+
/// Configure stencil state (front/back ops and masks). Requires depth-stencil enabled.
400+
pub fn with_stencil(mut self, stencil: StencilState) -> Self {
401+
let ds = self.depth_stencil.get_or_insert(wgpu::DepthStencilState {
402+
format: wgpu::TextureFormat::Depth24PlusStencil8,
403+
depth_write_enabled: true,
404+
depth_compare: wgpu::CompareFunction::Less,
405+
stencil: wgpu::StencilState::default(),
406+
bias: wgpu::DepthBiasState::default(),
407+
});
408+
ds.stencil = wgpu::StencilState {
409+
front: stencil.front.to_wgpu(),
410+
back: stencil.back.to_wgpu(),
411+
read_mask: stencil.read_mask,
412+
write_mask: stencil.write_mask,
413+
};
414+
return self;
415+
}
416+
417+
/// Configure multisampling. Count MUST be >= 1 and supported by the device.
418+
pub fn with_sample_count(mut self, count: u32) -> Self {
419+
self.sample_count = count.max(1);
420+
return self;
421+
}
422+
278423
/// Build the render pipeline from provided shader modules.
279424
pub fn build(
280425
self,
@@ -351,7 +496,10 @@ impl<'a> RenderPipelineBuilder<'a> {
351496
vertex: vertex_state,
352497
primitive: primitive_state,
353498
depth_stencil: self.depth_stencil,
354-
multisample: wgpu::MultisampleState::default(),
499+
multisample: wgpu::MultisampleState {
500+
count: self.sample_count,
501+
..wgpu::MultisampleState::default()
502+
},
355503
fragment,
356504
multiview: None,
357505
cache: None,

0 commit comments

Comments
 (0)