Skip to content

Commit 709054f

Browse files
committed
[add] features to optionally enable validation checks in production builds.
1 parent 70670f8 commit 709054f

File tree

4 files changed

+112
-53
lines changed

4 files changed

+112
-53
lines changed

crates/lambda-rs/Cargo.toml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,43 @@ with-shaderc-build-from-source=[
3737
"lambda-rs-platform/shader-backend-shaderc-build-from-source",
3838
]
3939

40+
# ------------------------------ RENDER VALIDATION -----------------------------
41+
# Granular, opt-in validation flags for release builds. Debug builds enable
42+
# all validations unconditionally via `debug_assertions`.
43+
44+
# Umbrella features
45+
# - render-validation: enable common, configuration-time checks and logs
46+
# - render-validation-strict: includes render-validation + per-draw checks
47+
# - render-validation-all: enables all validations including device probing
48+
render-validation = [
49+
"render-validation-msaa",
50+
"render-validation-depth",
51+
"render-validation-stencil",
52+
]
53+
render-validation-strict = [
54+
"render-validation",
55+
"render-validation-pass-compat",
56+
"render-validation-encoder",
57+
]
58+
render-validation-all = [
59+
"render-validation-strict",
60+
"render-validation-device",
61+
]
62+
63+
# Granular feature flags
64+
# - msaa: sample count validity and mismatch logging
65+
# - depth: depth clear clamping log and depth advisories
66+
# - stencil: stencil/format upgrade advisories
67+
# - pass-compat: color/depth presence compatibility checks at SetPipeline
68+
# - device: device/format probing advisories (may consult platform)
69+
# - encoder: per-draw/encoder-time checks (most expensive)
70+
render-validation-msaa = []
71+
render-validation-depth = []
72+
render-validation-stencil = []
73+
render-validation-pass-compat = []
74+
render-validation-device = []
75+
render-validation-encoder = []
76+
4077

4178
# ---------------------------- PLATFORM DEPENDENCIES ---------------------------
4279

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

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -471,7 +471,10 @@ impl RenderContext {
471471
&& self.depth_format
472472
!= platform::texture::DepthFormat::Depth24PlusStencil8
473473
{
474-
#[cfg(debug_assertions)]
474+
#[cfg(any(
475+
debug_assertions,
476+
feature = "render-validation-stencil",
477+
))]
475478
logging::error!(
476479
"Render pass has stencil ops but depth format {:?} lacks stencil; upgrading to Depth24PlusStencil8",
477480
self.depth_format
@@ -601,10 +604,18 @@ impl RenderContext {
601604
I: Iterator<Item = RenderCommand>,
602605
{
603606
Self::apply_viewport(pass, &initial_viewport);
604-
// De-duplicate advisories within this pass (debug builds only)
605-
#[cfg(debug_assertions)]
607+
// De-duplicate advisories within this pass
608+
#[cfg(any(
609+
debug_assertions,
610+
feature = "render-validation-depth",
611+
feature = "render-validation-stencil",
612+
))]
606613
let mut warned_no_stencil_for_pipeline: HashSet<usize> = HashSet::new();
607-
#[cfg(debug_assertions)]
614+
#[cfg(any(
615+
debug_assertions,
616+
feature = "render-validation-depth",
617+
feature = "render-validation-stencil",
618+
))]
608619
let mut warned_no_depth_for_pipeline: HashSet<usize> = HashSet::new();
609620

610621
while let Some(command) = commands.next() {
@@ -620,8 +631,12 @@ impl RenderContext {
620631
"Unknown pipeline {pipeline}"
621632
));
622633
})?;
623-
// Validate pass/pipeline compatibility before deferring to the platform (debug only).
624-
#[cfg(debug_assertions)]
634+
// Validate pass/pipeline compatibility before deferring to the platform.
635+
#[cfg(any(
636+
debug_assertions,
637+
feature = "render-validation-pass-compat",
638+
feature = "render-validation-encoder",
639+
))]
625640
{
626641
if !uses_color && pipeline_ref.has_color_targets() {
627642
let label = pipeline_ref.pipeline().label().unwrap_or("unnamed");
@@ -645,8 +660,12 @@ impl RenderContext {
645660
)));
646661
}
647662
}
648-
// Advisory checks to help reason about stencil/depth behavior (debug only).
649-
#[cfg(debug_assertions)]
663+
// Advisory checks to help reason about stencil/depth behavior.
664+
#[cfg(any(
665+
debug_assertions,
666+
feature = "render-validation-depth",
667+
feature = "render-validation-stencil",
668+
))]
650669
{
651670
if pass_has_stencil
652671
&& !pipeline_ref.uses_stencil()

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

Lines changed: 25 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -316,26 +316,20 @@ impl RenderPipelineBuilder {
316316

317317
/// Configure multi-sampling for this pipeline.
318318
pub fn with_multi_sample(mut self, samples: u32) -> Self {
319-
#[cfg(debug_assertions)]
320-
{
321-
match validation::validate_sample_count(samples) {
322-
Ok(()) => {
323-
self.sample_count = samples;
324-
}
325-
Err(msg) => {
319+
// Always apply a cheap validity check; log under feature/debug gates.
320+
if matches!(samples, 1 | 2 | 4 | 8) {
321+
self.sample_count = samples;
322+
} else {
323+
#[cfg(any(debug_assertions, feature = "render-validation-msaa",))]
324+
{
325+
if let Err(msg) = validation::validate_sample_count(samples) {
326326
logging::error!(
327327
"{}; falling back to sample_count=1 for pipeline",
328328
msg
329329
);
330-
self.sample_count = 1;
331330
}
332331
}
333-
}
334-
#[cfg(not(debug_assertions))]
335-
{
336-
// In release builds, accept the provided sample count without extra
337-
// engine-level validation. Platform validation MAY still apply.
338-
self.sample_count = samples;
332+
self.sample_count = 1;
339333
}
340334
return self;
341335
}
@@ -463,7 +457,7 @@ impl RenderPipelineBuilder {
463457
if self.stencil.is_some()
464458
&& dfmt != texture::DepthFormat::Depth24PlusStencil8
465459
{
466-
#[cfg(debug_assertions)]
460+
#[cfg(any(debug_assertions, feature = "render-validation-stencil",))]
467461
logging::error!(
468462
"Stencil configured but depth format {:?} lacks stencil; upgrading to Depth24PlusStencil8",
469463
dfmt
@@ -486,23 +480,24 @@ impl RenderPipelineBuilder {
486480
}
487481

488482
// Apply multi-sampling to the pipeline.
489-
// In debug builds, validate and align with the render pass; in release,
490-
// defer to platform validation without engine-side checks.
483+
// Always align to the pass sample count; gate logs.
491484
let mut pipeline_samples = self.sample_count;
492-
#[cfg(debug_assertions)]
493-
{
494-
let pass_samples = _render_pass.sample_count();
495-
if pipeline_samples != pass_samples {
496-
logging::error!(
497-
"Pipeline sample_count={} does not match pass sample_count={}; aligning to pass",
498-
pipeline_samples,
499-
pass_samples
500-
);
501-
pipeline_samples = pass_samples;
502-
}
503-
if validation::validate_sample_count(pipeline_samples).is_err() {
504-
pipeline_samples = 1;
485+
let pass_samples = _render_pass.sample_count();
486+
if pipeline_samples != pass_samples {
487+
#[cfg(any(debug_assertions, feature = "render-validation-msaa",))]
488+
logging::error!(
489+
"Pipeline sample_count={} does not match pass sample_count={}; aligning to pass",
490+
pipeline_samples,
491+
pass_samples
492+
);
493+
pipeline_samples = pass_samples;
494+
}
495+
if !matches!(pipeline_samples, 1 | 2 | 4 | 8) {
496+
#[cfg(any(debug_assertions, feature = "render-validation-msaa",))]
497+
{
498+
let _ = validation::validate_sample_count(pipeline_samples);
505499
}
500+
pipeline_samples = 1;
506501
}
507502
rp_builder = rp_builder.with_sample_count(pipeline_samples);
508503

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

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -201,8 +201,21 @@ impl RenderPassBuilder {
201201

202202
/// Enable a depth attachment with an explicit clear value.
203203
pub fn with_depth_clear(mut self, clear: f64) -> Self {
204+
// Clamp to the valid range [0.0, 1.0] unconditionally.
205+
let clamped = clear.clamp(0.0, 1.0);
206+
// Optionally log when clamping is applied.
207+
#[cfg(any(debug_assertions, feature = "render-validation-depth",))]
208+
{
209+
if (clamped - clear).abs() > f64::EPSILON {
210+
logging::warn!(
211+
"Depth clear value {} out of range [0,1]; clamped to {}",
212+
clear,
213+
clamped
214+
);
215+
}
216+
}
204217
self.depth_operations = Some(DepthOperations {
205-
load: DepthLoadOp::Clear(clear),
218+
load: DepthLoadOp::Clear(clamped),
206219
store: StoreOp::Store,
207220
});
208221
return self;
@@ -243,26 +256,21 @@ impl RenderPassBuilder {
243256

244257
/// Configure multi-sample anti-aliasing for this pass.
245258
pub fn with_multi_sample(mut self, samples: u32) -> Self {
246-
#[cfg(debug_assertions)]
247-
{
248-
match validation::validate_sample_count(samples) {
249-
Ok(()) => {
250-
self.sample_count = samples;
251-
}
252-
Err(msg) => {
259+
// Always apply a cheap validity check; log under feature/debug gates.
260+
let allowed = matches!(samples, 1 | 2 | 4 | 8);
261+
if allowed {
262+
self.sample_count = samples;
263+
} else {
264+
#[cfg(any(debug_assertions, feature = "render-validation-msaa",))]
265+
{
266+
if let Err(msg) = validation::validate_sample_count(samples) {
253267
logging::error!(
254268
"{}; falling back to sample_count=1 for render pass",
255269
msg
256270
);
257-
self.sample_count = 1;
258271
}
259272
}
260-
}
261-
#[cfg(not(debug_assertions))]
262-
{
263-
// In release builds, accept the provided sample count without engine
264-
// validation; platform validation MAY still apply.
265-
self.sample_count = samples;
273+
self.sample_count = 1;
266274
}
267275
return self;
268276
}

0 commit comments

Comments
 (0)