Skip to content

Commit 4c07ca3

Browse files
committed
[add] device validation for sample counts, headless gpu for testing, and tests for the validation.
1 parent deb8aff commit 4c07ca3

File tree

5 files changed

+397
-13
lines changed

5 files changed

+397
-13
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/surface.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ impl std::ops::BitOr for TextureUsages {
9393
pub struct SurfaceFormat(wgpu::TextureFormat);
9494

9595
impl SurfaceFormat {
96+
/// Common sRGB swapchain format used for windowed rendering.
97+
pub const BGRA8_UNORM_SRGB: SurfaceFormat =
98+
SurfaceFormat(wgpu::TextureFormat::Bgra8UnormSrgb);
99+
96100
pub(crate) fn to_wgpu(self) -> wgpu::TextureFormat {
97101
return self.0;
98102
}

crates/lambda-rs/src/render/mod.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,29 @@ impl RenderContext {
347347
return self.config.format;
348348
}
349349

350+
pub(crate) fn depth_format(&self) -> platform::texture::DepthFormat {
351+
return self.depth_format;
352+
}
353+
354+
pub(crate) fn supports_surface_sample_count(
355+
&self,
356+
sample_count: u32,
357+
) -> bool {
358+
return self
359+
.gpu
360+
.supports_sample_count_for_surface(self.config.format, sample_count);
361+
}
362+
363+
pub(crate) fn supports_depth_sample_count(
364+
&self,
365+
format: platform::texture::DepthFormat,
366+
sample_count: u32,
367+
) -> bool {
368+
return self
369+
.gpu
370+
.supports_sample_count_for_depth(format, sample_count);
371+
}
372+
350373
/// Device limit: maximum bytes that can be bound for a single uniform buffer binding.
351374
pub fn limit_max_uniform_buffer_binding_size(&self) -> u64 {
352375
return self.gpu.limits().max_uniform_buffer_binding_size;

0 commit comments

Comments
 (0)