Skip to content

Commit a3ce973

Browse files
authored
chore(lambda-rs): Increase code coverage for rendering (#177)
## Summary Increase `lambda-rs` render module test coverage by adding adapter-backed GPU tests, a feature-gated window/surface smoke runner for coverage, and a small render refactor to support offscreen-only rendering paths. ## Related Issues ## Changes - Add comprehensive tests across `crates/lambda-rs/src/render/**` to exercise pipeline/pass/bind-group/buffer/encoder/targets/texture/validation paths. - Update GPU-dependent tests to run automatically when an adapter is available: attempt primary adapter first, then wgpu fallback/virtual adapter; optionally enforce adapter presence via `LAMBDA_REQUIRE_GPU_ADAPTER=1`. - Centralize GPU creation logic for tests in a single helper implementation. - Add rustdoc on all render tests describing what each test validates. - Update CI workflows to enforce adapter-backed tests on Linux Vulkan jobs (Mesa lavapipe) by setting `LAMBDA_REQUIRE_GPU_ADAPTER=1`. - Ignore generated coverage artifacts in `.gitignore`. - Flatten docs paths for specs/tutorials (remove nested area folders), update indices/links, and remove outdated template guidance. ## Type of Change - [ ] Bug fix (non-breaking change that fixes an issue) - [x] Feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [x] Documentation (updates to docs, specs, tutorials, or comments) - [x] Refactor (code change that neither fixes a bug nor adds a feature) - [ ] Performance (change that improves performance) - [x] Test (adding or updating tests) - [x] Build/CI (changes to build process or CI configuration) ## Affected Crates - [x] `lambda-rs` - [ ] `lambda-rs-platform` - [ ] `lambda-rs-args` - [ ] `lambda-rs-logging` - [ ] Other: ## Checklist - [x] Code follows the repository style guidelines (`cargo +nightly fmt --all`) - [ ] Code passes clippy (`cargo clippy --workspace --all-targets -- -D warnings`) - [ ] Tests pass (`cargo test --workspace`) - [x] New code includes appropriate documentation - [ ] Public API changes are documented - [ ] Breaking changes are noted in this PR description ## Testing **Commands run:** ```bash cargo +nightly fmt cargo test -p lambda-rs --lib --tests ``` **Coverage workflow used for adapter-backed + window/surface coverage:** ```bash cargo llvm-cov clean -p lambda-rs cargo llvm-cov -p lambda-rs --lib --tests --no-report -- --include-ignored cargo llvm-cov report -p lambda-rs --json --output-path coverage/lambda_rs_cov.json ``` **Manual verification steps (if applicable):** 1. Confirm render folder per-file line coverage is ≥80% in `coverage/lambda_rs_cov.json`. ## Screenshots/Recordings N/A (no UI changes). ## Platform Testing - [x] macOS - [ ] Windows - [ ] Linux ## Additional Notes - Linux CI is configured to prefer Mesa's Vulkan software implementation (lavapipe) and now enforces adapter-backed render tests via `LAMBDA_REQUIRE_GPU_ADAPTER=1`.
2 parents 0392012 + e62b5c3 commit a3ce973

24 files changed

+2745
-56
lines changed

.github/workflows/compile_lambda_rs.yml

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,23 @@ jobs:
7272
if: ${{ matrix.os == 'ubuntu-latest' }}
7373
run: |
7474
echo "WGPU_BACKEND=vulkan" >> "$GITHUB_ENV"
75-
# Prefer Mesa's software Vulkan (lavapipe) to ensure headless availability
76-
echo "VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/lvp_icd.x86_64.json" >> "$GITHUB_ENV"
75+
# Prefer Mesa's software Vulkan (lavapipe) to ensure headless availability.
76+
# The exact ICD filename can differ across Ubuntu images, so discover it.
77+
LVP_ICD="$(
78+
if [[ -d /usr/share/vulkan/icd.d ]]; then
79+
find /usr/share/vulkan/icd.d -maxdepth 1 -type f \
80+
\( -name '*lvp_icd*.json' -o -name '*lavapipe*.json' \) \
81+
-print 2>/dev/null | head -n1
82+
fi
83+
)"
84+
if [[ -z "$LVP_ICD" ]]; then
85+
echo "lavapipe Vulkan ICD not found under /usr/share/vulkan/icd.d" >&2
86+
ls -la /usr/share/vulkan/icd.d || true
87+
else
88+
echo "Using lavapipe ICD: $LVP_ICD"
89+
echo "VK_ICD_FILENAMES=$LVP_ICD" >> "$GITHUB_ENV"
90+
fi
91+
echo "LAMBDA_REQUIRE_GPU_ADAPTER=1" >> "$GITHUB_ENV"
7792
vulkaninfo --summary || true
7893
7994
# Windows runners already include the required toolchain for DX12 builds.

.github/workflows/coverage.yml

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ jobs:
2323
coverage:
2424
name: Generate code coverage with cargo-llvm-cov
2525
runs-on: ubuntu-latest
26+
env:
27+
LAMBDA_REQUIRE_GPU_ADAPTER: "1"
2628

2729
steps:
2830
- name: Checkout Repository
@@ -60,8 +62,22 @@ jobs:
6062
- name: Configure Vulkan (Ubuntu)
6163
run: |
6264
echo "WGPU_BACKEND=vulkan" >> "$GITHUB_ENV"
63-
# Prefer Mesa's software Vulkan (lavapipe) for headless availability
64-
echo "VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/lvp_icd.x86_64.json" >> "$GITHUB_ENV"
65+
# Prefer Mesa's software Vulkan (lavapipe) for headless availability.
66+
# The exact ICD filename can differ across Ubuntu images, so discover it.
67+
LVP_ICD="$(
68+
if [[ -d /usr/share/vulkan/icd.d ]]; then
69+
find /usr/share/vulkan/icd.d -maxdepth 1 -type f \
70+
\( -name '*lvp_icd*.json' -o -name '*lavapipe*.json' \) \
71+
-print 2>/dev/null | head -n1
72+
fi
73+
)"
74+
if [[ -z "$LVP_ICD" ]]; then
75+
echo "lavapipe Vulkan ICD not found under /usr/share/vulkan/icd.d" >&2
76+
ls -la /usr/share/vulkan/icd.d || true
77+
else
78+
echo "Using lavapipe ICD: $LVP_ICD"
79+
echo "VK_ICD_FILENAMES=$LVP_ICD" >> "$GITHUB_ENV"
80+
fi
6581
vulkaninfo --summary || true
6682
6783
- name: Generate full coverage JSON

.github/workflows/release.yml

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,23 @@ jobs:
5353
- name: Configure Vulkan for headless CI
5454
run: |
5555
echo "WGPU_BACKEND=vulkan" >> "$GITHUB_ENV"
56-
echo "VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/lvp_icd.x86_64.json" >> "$GITHUB_ENV"
56+
# Prefer Mesa's software Vulkan (lavapipe) for headless availability.
57+
# The exact ICD filename can differ across Ubuntu images, so discover it.
58+
LVP_ICD="$(
59+
if [[ -d /usr/share/vulkan/icd.d ]]; then
60+
find /usr/share/vulkan/icd.d -maxdepth 1 -type f \
61+
\( -name '*lvp_icd*.json' -o -name '*lavapipe*.json' \) \
62+
-print 2>/dev/null | head -n1
63+
fi
64+
)"
65+
if [[ -z "$LVP_ICD" ]]; then
66+
echo "lavapipe Vulkan ICD not found under /usr/share/vulkan/icd.d" >&2
67+
ls -la /usr/share/vulkan/icd.d || true
68+
else
69+
echo "Using lavapipe ICD: $LVP_ICD"
70+
echo "VK_ICD_FILENAMES=$LVP_ICD" >> "$GITHUB_ENV"
71+
fi
72+
echo "LAMBDA_REQUIRE_GPU_ADAPTER=1" >> "$GITHUB_ENV"
5773
vulkaninfo --summary || true
5874
5975
- name: Format check

.gitignore

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,10 @@ Temporary Items
146146

147147
# End of https://www.gitignore.io/api/linux,cpp,c,cmake,macos,opengl
148148

149-
imgui.ini
149+
imgui.ini
150150

151-
# Planning
152-
docs/plans/
151+
# Planning
152+
docs/plans/
153+
154+
# Coverage reports
155+
coverage/

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

Lines changed: 179 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,184 @@ impl BindingVisibility {
6464
}
6565

6666
#[cfg(test)]
67-
mod tests {}
67+
mod tests {
68+
use super::*;
69+
use crate::render::{
70+
buffer::{
71+
BufferBuilder,
72+
BufferType,
73+
Properties,
74+
Usage,
75+
},
76+
gpu::create_test_gpu,
77+
texture::{
78+
SamplerBuilder,
79+
TextureBuilder,
80+
TextureFormat,
81+
ViewDimension,
82+
},
83+
};
84+
85+
/// Ensures engine-facing shader stage visibility flags map to the platform
86+
/// wgpu visibility flags.
87+
#[test]
88+
fn binding_visibility_maps_to_platform() {
89+
assert!(matches!(
90+
BindingVisibility::Vertex.to_platform(),
91+
lambda_platform::wgpu::bind::Visibility::Vertex
92+
));
93+
assert!(matches!(
94+
BindingVisibility::Fragment.to_platform(),
95+
lambda_platform::wgpu::bind::Visibility::Fragment
96+
));
97+
assert!(matches!(
98+
BindingVisibility::Compute.to_platform(),
99+
lambda_platform::wgpu::bind::Visibility::Compute
100+
));
101+
assert!(matches!(
102+
BindingVisibility::VertexAndFragment.to_platform(),
103+
lambda_platform::wgpu::bind::Visibility::VertexAndFragment
104+
));
105+
assert!(matches!(
106+
BindingVisibility::All.to_platform(),
107+
lambda_platform::wgpu::bind::Visibility::All
108+
));
109+
}
110+
111+
/// Rejects duplicated binding indices within a single bind group layout in
112+
/// debug builds.
113+
#[test]
114+
#[cfg(debug_assertions)]
115+
fn bind_group_layout_builder_rejects_duplicate_binding() {
116+
let Some(gpu) = create_test_gpu("lambda-bind-test") else {
117+
return;
118+
};
119+
120+
// Duplicate binding index 0 across entries should panic in debug builds.
121+
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
122+
let _layout = BindGroupLayoutBuilder::new()
123+
.with_uniform(0, BindingVisibility::Vertex)
124+
.with_uniform_dynamic(0, BindingVisibility::Vertex)
125+
.build(&gpu);
126+
}));
127+
assert!(result.is_err());
128+
}
129+
130+
/// Tracks the number of dynamic uniform bindings so callers can validate
131+
/// dynamic offset counts at bind time.
132+
#[test]
133+
fn bind_group_layout_counts_dynamic_uniforms() {
134+
let Some(gpu) = create_test_gpu("lambda-bind-test") else {
135+
return;
136+
};
137+
138+
let layout = BindGroupLayoutBuilder::new()
139+
.with_uniform(0, BindingVisibility::VertexAndFragment)
140+
.with_uniform_dynamic(1, BindingVisibility::VertexAndFragment)
141+
.build(&gpu);
142+
143+
assert_eq!(layout.dynamic_binding_count(), 1);
144+
}
145+
146+
/// Ensures building a bind group without providing a layout fails loudly.
147+
#[test]
148+
fn bind_group_builder_requires_layout() {
149+
let Some(gpu) = create_test_gpu("lambda-bind-test") else {
150+
return;
151+
};
152+
153+
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
154+
let _group = BindGroupBuilder::new().build(&gpu);
155+
}));
156+
assert!(result.is_err());
157+
}
158+
159+
/// Ensures a bind group exposes the same dynamic binding count as its layout.
160+
#[test]
161+
fn bind_group_dynamic_binding_count_matches_layout() {
162+
let Some(gpu) = create_test_gpu("lambda-bind-test") else {
163+
return;
164+
};
165+
166+
let layout = BindGroupLayoutBuilder::new()
167+
.with_uniform_dynamic(0, BindingVisibility::VertexAndFragment)
168+
.build(&gpu);
169+
170+
let uniform = BufferBuilder::new()
171+
.with_label("bind-test-uniform")
172+
.with_usage(Usage::UNIFORM)
173+
.with_properties(Properties::CPU_VISIBLE)
174+
.with_buffer_type(BufferType::Uniform)
175+
.build(&gpu, vec![0u32; 4])
176+
.expect("build uniform buffer");
177+
178+
let group = BindGroupBuilder::new()
179+
.with_layout(&layout)
180+
.with_uniform(0, &uniform, 0, None)
181+
.build(&gpu);
182+
183+
assert_eq!(
184+
group.dynamic_binding_count(),
185+
layout.dynamic_binding_count()
186+
);
187+
}
188+
189+
/// Builds a bind group with multiple resource kinds (2D sampled texture, 3D
190+
/// sampled texture, sampler) to validate layout/view dimension compatibility.
191+
#[test]
192+
fn bind_group_supports_textures_and_samplers() {
193+
let Some(gpu) = create_test_gpu("lambda-bind-test") else {
194+
return;
195+
};
196+
197+
let texture_2d = TextureBuilder::new_2d(TextureFormat::Rgba8Unorm)
198+
.with_size(1, 1)
199+
.build(&gpu)
200+
.expect("build 2d texture");
201+
let texture_3d = TextureBuilder::new_3d(TextureFormat::Rgba8Unorm)
202+
.with_size_3d(1, 1, 2)
203+
.build(&gpu)
204+
.expect("build 3d texture");
205+
let sampler = SamplerBuilder::new().linear().build(&gpu);
206+
207+
let layout = BindGroupLayoutBuilder::new()
208+
.with_sampled_texture(0)
209+
.with_sampled_texture_dim(
210+
1,
211+
ViewDimension::D3,
212+
BindingVisibility::Fragment,
213+
)
214+
.with_sampler(2)
215+
.build(&gpu);
216+
217+
let group = BindGroupBuilder::new()
218+
.with_layout(&layout)
219+
.with_texture(0, &texture_2d)
220+
.with_texture(1, &texture_3d)
221+
.with_sampler(2, &sampler)
222+
.build(&gpu);
223+
224+
assert_eq!(group.dynamic_binding_count(), 0);
225+
}
226+
227+
/// Rejects duplicated binding indices even when the duplicates are across
228+
/// different resource kinds (uniform vs sampler) in debug builds.
229+
#[test]
230+
#[cfg(debug_assertions)]
231+
fn bind_group_layout_rejects_duplicate_binding_across_resource_kinds() {
232+
let Some(gpu) = create_test_gpu("lambda-bind-test") else {
233+
return;
234+
};
235+
236+
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
237+
let _layout = BindGroupLayoutBuilder::new()
238+
.with_uniform(0, BindingVisibility::Vertex)
239+
.with_sampler(0)
240+
.build(&gpu);
241+
}));
242+
assert!(result.is_err());
243+
}
244+
}
68245

69246
/// Bind group layout used when creating pipelines and bind groups.
70247
#[derive(Debug, Clone)]
@@ -348,7 +525,7 @@ impl<'a> BindGroupBuilder<'a> {
348525
return self;
349526
}
350527

351-
/// Bind a 2D texture at the specified binding index.
528+
/// Bind a texture at the specified binding index.
352529
pub fn with_texture(mut self, binding: u32, texture: &'a Texture) -> Self {
353530
self.textures.push((binding, texture.platform_texture()));
354531
return self;

0 commit comments

Comments
 (0)