Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2602f2c
[add] initial spec.
vmarcella Nov 11, 2025
1ec667d
[add] initial depth/stencil implementation.
vmarcella Nov 11, 2025
21d0a5b
[add] validation for sample counts.
vmarcella Nov 11, 2025
e41042e
[add] stencils and msaa implementations, update specifications.
vmarcella Nov 14, 2025
518cf94
[fix] stencil operations being exposed by lambda-rs.
vmarcella Nov 14, 2025
f566aa7
[fix] windowing issue on macos.
vmarcella Nov 14, 2025
668f72b
[add] the ability for warnings to only be logged once (Instead of per…
vmarcella Nov 14, 2025
294f37c
[add] high level implementations for comparefunction and culling modes.
vmarcella Nov 14, 2025
4172822
[update] depth/stencil/msaa validation so that it only happens for de…
vmarcella Nov 14, 2025
1db6f96
[add] the ability to toggle msaa, depth tests, and stencil tests in t…
vmarcella Nov 16, 2025
ceaf345
[optimize] resource building.
vmarcella Nov 16, 2025
49b393d
[add] high level depth format implementation.
vmarcella Nov 17, 2025
70670f8
[update] specification and add tutorial for the new demo.
vmarcella Nov 17, 2025
709054f
[add] features to optionally enable validation checks in production b…
vmarcella Nov 17, 2025
4c3c4e8
[add] features documentation for all features and update specificatio…
vmarcella Nov 17, 2025
a8ed539
[update] color of floor and winding of the floor quad.
vmarcella Nov 17, 2025
bf0e90a
[update] camera to allow the angle to be adjusted.
vmarcella Nov 19, 2025
deb8aff
[remove] unmasked reflection toggle and update tutorials.
vmarcella Nov 19, 2025
4c07ca3
[add] device validation for sample counts, headless gpu for testing, …
vmarcella Nov 21, 2025
415167f
[update] render context to better handle depth attachment for stencil…
vmarcella Nov 21, 2025
1f91ff4
[update] specification.
vmarcella Nov 21, 2025
4efec32
[update] tutorial.
vmarcella Nov 21, 2025
2730e86
[update] depth format not be global.
vmarcella Nov 21, 2025
2874e82
[fix] depth clear clamp check.
vmarcella Nov 21, 2025
2247841
[fix] warn once logger to ensure that it can't grow too large.
vmarcella Nov 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 175 additions & 0 deletions crates/lambda-rs-platform/src/wgpu/gpu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use super::{
command::CommandBuffer,
instance::Instance,
surface::Surface,
texture,
};

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

impl Gpu {
/// Whether the provided surface format supports the sample count for render attachments.
pub fn supports_sample_count_for_surface(
&self,
format: super::surface::SurfaceFormat,
sample_count: u32,
) -> bool {
return self.supports_sample_count(format.to_wgpu(), sample_count);
}

/// Whether the provided depth format supports the sample count for render attachments.
pub fn supports_sample_count_for_depth(
&self,
format: texture::DepthFormat,
sample_count: u32,
) -> bool {
return self.supports_sample_count(format.to_wgpu(), sample_count);
}

/// Borrow the adapter used to create the device.
///
/// Crate-visible to avoid exposing raw `wgpu` to higher layers.
Expand Down Expand Up @@ -245,11 +264,50 @@ impl Gpu {
let iter = list.into_iter().map(|cb| cb.into_raw());
self.queue.submit(iter);
}

fn supports_sample_count(
&self,
format: wgpu::TextureFormat,
sample_count: u32,
) -> bool {
if sample_count <= 1 {
return true;
}

let features = self.adapter.get_texture_format_features(format);
if !features
.allowed_usages
.contains(wgpu::TextureUsages::RENDER_ATTACHMENT)
{
return false;
}

match sample_count {
2 => features
.flags
.contains(wgpu::TextureFormatFeatureFlags::MULTISAMPLE_X2),
4 => features
.flags
.contains(wgpu::TextureFormatFeatureFlags::MULTISAMPLE_X4),
8 => features
.flags
.contains(wgpu::TextureFormatFeatureFlags::MULTISAMPLE_X8),
16 => features
.flags
.contains(wgpu::TextureFormatFeatureFlags::MULTISAMPLE_X16),
_ => false,
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::wgpu::{
instance,
surface,
texture,
};

#[test]
fn gpu_build_error_wraps_request_device_error() {
Expand All @@ -260,4 +318,121 @@ mod tests {
fn assert_from_impl<T: From<wgpu::RequestDeviceError>>() {}
assert_from_impl::<GpuBuildError>();
}

/// Create an offscreen GPU for sample-count support tests.
///
/// Returns `None` when no compatible adapter is available so tests can be
/// skipped instead of failing.
fn create_test_gpu() -> Option<Gpu> {
let instance = instance::InstanceBuilder::new()
.with_label("gpu-test-instance")
.build();
return GpuBuilder::new()
.with_label("gpu-test-device")
.build(&instance, None)
.ok();
}

/// Accepts zero or single-sample attachments for any format.
#[test]
fn single_sample_always_supported() {
let gpu = match create_test_gpu() {
Some(gpu) => gpu,
None => {
eprintln!(
"Skipping single_sample_always_supported: no compatible GPU adapter"
);
return;
}
};
let surface_format =
surface::SurfaceFormat::from_wgpu(wgpu::TextureFormat::Bgra8UnormSrgb);
let depth_format = texture::DepthFormat::Depth32Float;

assert!(gpu.supports_sample_count_for_surface(surface_format, 1));
assert!(gpu.supports_sample_count_for_surface(surface_format, 0));
assert!(gpu.supports_sample_count_for_depth(depth_format, 1));
assert!(gpu.supports_sample_count_for_depth(depth_format, 0));
}

/// Rejects sample counts that are outside the supported set.
#[test]
fn unsupported_sample_count_rejected() {
let gpu = match create_test_gpu() {
Some(gpu) => gpu,
None => {
eprintln!(
"Skipping unsupported_sample_count_rejected: no compatible GPU adapter"
);
return;
}
};
let surface_format =
surface::SurfaceFormat::from_wgpu(wgpu::TextureFormat::Bgra8Unorm);
let depth_format = texture::DepthFormat::Depth32Float;

assert!(!gpu.supports_sample_count_for_surface(surface_format, 3));
assert!(!gpu.supports_sample_count_for_depth(depth_format, 3));
}

/// Mirrors the adapter's texture feature flags for surface formats.
#[test]
fn surface_support_matches_texture_features() {
let gpu = match create_test_gpu() {
Some(gpu) => gpu,
None => {
eprintln!(
"Skipping surface_support_matches_texture_features: \
no compatible GPU adapter"
);
return;
}
};
let surface_format =
surface::SurfaceFormat::from_wgpu(wgpu::TextureFormat::Bgra8UnormSrgb);
let features = gpu
.adapter
.get_texture_format_features(surface_format.to_wgpu());
let expected = features
.allowed_usages
.contains(wgpu::TextureUsages::RENDER_ATTACHMENT)
&& features
.flags
.contains(wgpu::TextureFormatFeatureFlags::MULTISAMPLE_X4);

assert_eq!(
gpu.supports_sample_count_for_surface(surface_format, 4),
expected
);
}

/// Mirrors the adapter's texture feature flags for depth formats.
#[test]
fn depth_support_matches_texture_features() {
let gpu = match create_test_gpu() {
Some(gpu) => gpu,
None => {
eprintln!(
"Skipping depth_support_matches_texture_features: \
no compatible GPU adapter"
);
return;
}
};
let depth_format = texture::DepthFormat::Depth32Float;
let features = gpu
.adapter
.get_texture_format_features(depth_format.to_wgpu());
let expected = features
.allowed_usages
.contains(wgpu::TextureUsages::RENDER_ATTACHMENT)
&& features
.flags
.contains(wgpu::TextureFormatFeatureFlags::MULTISAMPLE_X4);

assert_eq!(
gpu.supports_sample_count_for_depth(depth_format, 4),
expected
);
}
}
150 changes: 149 additions & 1 deletion crates/lambda-rs-platform/src/wgpu/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,95 @@ pub struct VertexAttributeDesc {
pub format: ColorFormat,
}

/// Compare function used for depth and stencil tests.
#[derive(Clone, Copy, Debug)]
pub enum CompareFunction {
Never,
Less,
LessEqual,
Greater,
GreaterEqual,
Equal,
NotEqual,
Always,
}

impl CompareFunction {
fn to_wgpu(self) -> wgpu::CompareFunction {
match self {
CompareFunction::Never => wgpu::CompareFunction::Never,
CompareFunction::Less => wgpu::CompareFunction::Less,
CompareFunction::LessEqual => wgpu::CompareFunction::LessEqual,
CompareFunction::Greater => wgpu::CompareFunction::Greater,
CompareFunction::GreaterEqual => wgpu::CompareFunction::GreaterEqual,
CompareFunction::Equal => wgpu::CompareFunction::Equal,
CompareFunction::NotEqual => wgpu::CompareFunction::NotEqual,
CompareFunction::Always => wgpu::CompareFunction::Always,
}
}
}

/// Stencil operation applied when the stencil test or depth test passes/fails.
#[derive(Clone, Copy, Debug)]
pub enum StencilOperation {
Keep,
Zero,
Replace,
Invert,
IncrementClamp,
DecrementClamp,
IncrementWrap,
DecrementWrap,
}

impl StencilOperation {
fn to_wgpu(self) -> wgpu::StencilOperation {
match self {
StencilOperation::Keep => wgpu::StencilOperation::Keep,
StencilOperation::Zero => wgpu::StencilOperation::Zero,
StencilOperation::Replace => wgpu::StencilOperation::Replace,
StencilOperation::Invert => wgpu::StencilOperation::Invert,
StencilOperation::IncrementClamp => {
wgpu::StencilOperation::IncrementClamp
}
StencilOperation::DecrementClamp => {
wgpu::StencilOperation::DecrementClamp
}
StencilOperation::IncrementWrap => wgpu::StencilOperation::IncrementWrap,
StencilOperation::DecrementWrap => wgpu::StencilOperation::DecrementWrap,
}
}
}

/// Per-face stencil state.
#[derive(Clone, Copy, Debug)]
pub struct StencilFaceState {
pub compare: CompareFunction,
pub fail_op: StencilOperation,
pub depth_fail_op: StencilOperation,
pub pass_op: StencilOperation,
}

impl StencilFaceState {
fn to_wgpu(self) -> wgpu::StencilFaceState {
wgpu::StencilFaceState {
compare: self.compare.to_wgpu(),
fail_op: self.fail_op.to_wgpu(),
depth_fail_op: self.depth_fail_op.to_wgpu(),
pass_op: self.pass_op.to_wgpu(),
}
}
}

/// Full stencil state (front/back + masks).
#[derive(Clone, Copy, Debug)]
pub struct StencilState {
pub front: StencilFaceState,
pub back: StencilFaceState,
pub read_mask: u32,
pub write_mask: u32,
}

/// Wrapper around `wgpu::ShaderModule` that preserves a label.
#[derive(Debug)]
pub struct ShaderModule {
Expand Down Expand Up @@ -202,6 +291,10 @@ impl RenderPipeline {
pub(crate) fn into_raw(self) -> wgpu::RenderPipeline {
return self.raw;
}
/// Pipeline label if provided.
pub fn label(&self) -> Option<&str> {
return self.label.as_deref();
}
}

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

impl<'a> RenderPipelineBuilder<'a> {
Expand All @@ -224,6 +318,7 @@ impl<'a> RenderPipelineBuilder<'a> {
cull_mode: CullingMode::Back,
color_target_format: None,
depth_stencil: None,
sample_count: 1,
};
}

Expand Down Expand Up @@ -275,6 +370,56 @@ impl<'a> RenderPipelineBuilder<'a> {
return self;
}

/// Set the depth compare function. Requires depth to be enabled.
pub fn with_depth_compare(mut self, compare: CompareFunction) -> Self {
let ds = self.depth_stencil.get_or_insert(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth32Float,
depth_write_enabled: true,
depth_compare: wgpu::CompareFunction::Less,
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
});
ds.depth_compare = compare.to_wgpu();
return self;
}

/// Enable or disable depth writes. Requires depth-stencil enabled.
pub fn with_depth_write_enabled(mut self, enabled: bool) -> Self {
let ds = self.depth_stencil.get_or_insert(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth32Float,
depth_write_enabled: true,
depth_compare: wgpu::CompareFunction::Less,
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
});
ds.depth_write_enabled = enabled;
return self;
}

/// Configure stencil state (front/back ops and masks). Requires depth-stencil enabled.
pub fn with_stencil(mut self, stencil: StencilState) -> Self {
let ds = self.depth_stencil.get_or_insert(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth24PlusStencil8,
depth_write_enabled: true,
depth_compare: wgpu::CompareFunction::Less,
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
});
ds.stencil = wgpu::StencilState {
front: stencil.front.to_wgpu(),
back: stencil.back.to_wgpu(),
read_mask: stencil.read_mask,
write_mask: stencil.write_mask,
};
return self;
}

/// Configure multisampling. Count MUST be >= 1 and supported by the device.
pub fn with_sample_count(mut self, count: u32) -> Self {
self.sample_count = count.max(1);
return self;
}

/// Build the render pipeline from provided shader modules.
pub fn build(
self,
Expand Down Expand Up @@ -351,7 +496,10 @@ impl<'a> RenderPipelineBuilder<'a> {
vertex: vertex_state,
primitive: primitive_state,
depth_stencil: self.depth_stencil,
multisample: wgpu::MultisampleState::default(),
multisample: wgpu::MultisampleState {
count: self.sample_count,
..wgpu::MultisampleState::default()
},
fragment,
multiview: None,
cache: None,
Expand Down
Loading
Loading