Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
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
1 change: 1 addition & 0 deletions .github/workflows/compile_lambda_rs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ jobs:
echo "WGPU_BACKEND=vulkan" >> "$GITHUB_ENV"
# Prefer Mesa's software Vulkan (lavapipe) to ensure headless availability
echo "VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/lvp_icd.x86_64.json" >> "$GITHUB_ENV"
echo "LAMBDA_REQUIRE_GPU_ADAPTER=1" >> "$GITHUB_ENV"
vulkaninfo --summary || true

# Windows runners already include the required toolchain for DX12 builds.
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ jobs:
coverage:
name: Generate code coverage with cargo-llvm-cov
runs-on: ubuntu-latest
env:
LAMBDA_REQUIRE_GPU_ADAPTER: "1"

steps:
- name: Checkout Repository
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ jobs:
run: |
echo "WGPU_BACKEND=vulkan" >> "$GITHUB_ENV"
echo "VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/lvp_icd.x86_64.json" >> "$GITHUB_ENV"
echo "LAMBDA_REQUIRE_GPU_ADAPTER=1" >> "$GITHUB_ENV"
vulkaninfo --summary || true

- name: Format check
Expand Down
9 changes: 6 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,10 @@ Temporary Items

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

imgui.ini
imgui.ini

# Planning
docs/plans/
# Planning
docs/plans/

# Coverage reports
coverage/
181 changes: 179 additions & 2 deletions crates/lambda-rs/src/render/bind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,184 @@ impl BindingVisibility {
}

#[cfg(test)]
mod tests {}
mod tests {
use super::*;
use crate::render::{
buffer::{
BufferBuilder,
BufferType,
Properties,
Usage,
},
gpu::create_test_gpu,
texture::{
SamplerBuilder,
TextureBuilder,
TextureFormat,
ViewDimension,
},
};

/// Ensures engine-facing shader stage visibility flags map to the platform
/// wgpu visibility flags.
#[test]
fn binding_visibility_maps_to_platform() {
assert!(matches!(
BindingVisibility::Vertex.to_platform(),
lambda_platform::wgpu::bind::Visibility::Vertex
));
assert!(matches!(
BindingVisibility::Fragment.to_platform(),
lambda_platform::wgpu::bind::Visibility::Fragment
));
assert!(matches!(
BindingVisibility::Compute.to_platform(),
lambda_platform::wgpu::bind::Visibility::Compute
));
assert!(matches!(
BindingVisibility::VertexAndFragment.to_platform(),
lambda_platform::wgpu::bind::Visibility::VertexAndFragment
));
assert!(matches!(
BindingVisibility::All.to_platform(),
lambda_platform::wgpu::bind::Visibility::All
));
}

/// Rejects duplicated binding indices within a single bind group layout in
/// debug builds.
#[test]
#[cfg(debug_assertions)]
fn bind_group_layout_builder_rejects_duplicate_binding() {
let Some(gpu) = create_test_gpu("lambda-bind-test") else {
return;
};

// Duplicate binding index 0 across entries should panic in debug builds.
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _layout = BindGroupLayoutBuilder::new()
.with_uniform(0, BindingVisibility::Vertex)
.with_uniform_dynamic(0, BindingVisibility::Vertex)
.build(&gpu);
}));
assert!(result.is_err());
}

/// Tracks the number of dynamic uniform bindings so callers can validate
/// dynamic offset counts at bind time.
#[test]
fn bind_group_layout_counts_dynamic_uniforms() {
let Some(gpu) = create_test_gpu("lambda-bind-test") else {
return;
};

let layout = BindGroupLayoutBuilder::new()
.with_uniform(0, BindingVisibility::VertexAndFragment)
.with_uniform_dynamic(1, BindingVisibility::VertexAndFragment)
.build(&gpu);

assert_eq!(layout.dynamic_binding_count(), 1);
}

/// Ensures building a bind group without providing a layout fails loudly.
#[test]
fn bind_group_builder_requires_layout() {
let Some(gpu) = create_test_gpu("lambda-bind-test") else {
return;
};

let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _group = BindGroupBuilder::new().build(&gpu);
}));
assert!(result.is_err());
}

/// Ensures a bind group exposes the same dynamic binding count as its layout.
#[test]
fn bind_group_dynamic_binding_count_matches_layout() {
let Some(gpu) = create_test_gpu("lambda-bind-test") else {
return;
};

let layout = BindGroupLayoutBuilder::new()
.with_uniform_dynamic(0, BindingVisibility::VertexAndFragment)
.build(&gpu);

let uniform = BufferBuilder::new()
.with_label("bind-test-uniform")
.with_usage(Usage::UNIFORM)
.with_properties(Properties::CPU_VISIBLE)
.with_buffer_type(BufferType::Uniform)
.build(&gpu, vec![0u32; 4])
.expect("build uniform buffer");

let group = BindGroupBuilder::new()
.with_layout(&layout)
.with_uniform(0, &uniform, 0, None)
.build(&gpu);

assert_eq!(
group.dynamic_binding_count(),
layout.dynamic_binding_count()
);
}

/// Builds a bind group with multiple resource kinds (2D sampled texture, 3D
/// sampled texture, sampler) to validate layout/view dimension compatibility.
#[test]
fn bind_group_supports_textures_and_samplers() {
let Some(gpu) = create_test_gpu("lambda-bind-test") else {
return;
};

let texture_2d = TextureBuilder::new_2d(TextureFormat::Rgba8Unorm)
.with_size(1, 1)
.build(&gpu)
.expect("build 2d texture");
let texture_3d = TextureBuilder::new_3d(TextureFormat::Rgba8Unorm)
.with_size_3d(1, 1, 2)
.build(&gpu)
.expect("build 3d texture");
let sampler = SamplerBuilder::new().linear().build(&gpu);

let layout = BindGroupLayoutBuilder::new()
.with_sampled_texture(0)
.with_sampled_texture_dim(
1,
ViewDimension::D3,
BindingVisibility::Fragment,
)
.with_sampler(2)
.build(&gpu);

let group = BindGroupBuilder::new()
.with_layout(&layout)
.with_texture(0, &texture_2d)
.with_texture(1, &texture_3d)
.with_sampler(2, &sampler)
.build(&gpu);

assert_eq!(group.dynamic_binding_count(), 0);
}

/// Rejects duplicated binding indices even when the duplicates are across
/// different resource kinds (uniform vs sampler) in debug builds.
#[test]
#[cfg(debug_assertions)]
fn bind_group_layout_rejects_duplicate_binding_across_resource_kinds() {
let Some(gpu) = create_test_gpu("lambda-bind-test") else {
return;
};

let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _layout = BindGroupLayoutBuilder::new()
.with_uniform(0, BindingVisibility::Vertex)
.with_sampler(0)
.build(&gpu);
}));
assert!(result.is_err());
}
}

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

/// Bind a 2D texture at the specified binding index.
/// Bind a texture at the specified binding index.
pub fn with_texture(mut self, binding: u32, texture: &'a Texture) -> Self {
self.textures.push((binding, texture.platform_texture()));
return self;
Expand Down
69 changes: 69 additions & 0 deletions crates/lambda-rs/src/render/buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -426,13 +426,15 @@ impl BufferBuilder {
mod tests {
use super::*;

/// Rejects constructing a buffer with a logical length of zero elements.
#[test]
fn resolve_length_rejects_zero() {
let builder = BufferBuilder::new();
let result = builder.resolve_length(std::mem::size_of::<u32>(), 0);
assert!(result.is_err());
}

/// Ensures builder labels are stored for later propagation/debugging.
#[test]
fn label_is_recorded_on_builder() {
let builder = BufferBuilder::new().with_label("buffer-test");
Expand All @@ -441,20 +443,23 @@ mod tests {
assert_eq!(builder.label.as_deref(), Some("buffer-test"));
}

/// Rejects length computations that would overflow `usize`.
#[test]
fn resolve_length_rejects_overflow() {
let builder = BufferBuilder::new();
let result = builder.resolve_length(usize::MAX, 2);
assert!(result.is_err());
}

/// Confirms `value_as_bytes` uses native-endian byte order and size.
#[test]
fn value_as_bytes_matches_native_bytes() {
let value: u32 = 0x1122_3344;
let expected = value.to_ne_bytes();
assert_eq!(value_as_bytes(&value), expected.as_slice());
}

/// Confirms `slice_as_bytes` flattens a typed slice to the native bytes.
#[test]
fn slice_as_bytes_matches_native_bytes() {
let values: [u16; 3] = [0x1122, 0x3344, 0x5566];
Expand All @@ -465,15 +470,79 @@ mod tests {
assert_eq!(slice_as_bytes(&values).unwrap(), expected.as_slice());
}

/// Ensures converting an empty slice to bytes yields an empty output slice.
#[test]
fn slice_as_bytes_empty_is_empty() {
let values: [u32; 0] = [];
assert_eq!(slice_as_bytes(&values).unwrap(), &[]);
}

/// Rejects byte length computations that would overflow `usize`.
#[test]
fn checked_byte_len_rejects_overflow() {
let result = checked_byte_len(usize::MAX, 2);
assert!(result.is_err());
}

/// Validates default flags and bitwise-OR behavior for buffer usage and
/// memory properties.
#[test]
fn usage_and_properties_support_defaults_and_bit_ops() {
let default_usage = Usage::default();
let _ = default_usage.to_platform();

let combined = Usage::VERTEX | Usage::INDEX;
let _ = combined.to_platform();

assert!(Properties::default().cpu_visible());
assert!(!Properties::DEVICE_LOCAL.cpu_visible());
}

/// Confirms `BufferType` stays a small Copy enum and is `Debug`-printable.
#[test]
fn buffer_type_is_copy_and_debug() {
let t = BufferType::Uniform;
let _ = format!("{:?}", t);
let copied = t;
assert!(matches!(copied, BufferType::Uniform));
}

/// Exercises the GPU-backed write helpers to ensure they are callable and
/// wired to the platform API.
#[test]
fn buffer_write_value_and_slice_paths_are_callable() {
let Some(gpu) = crate::render::gpu::create_test_gpu("lambda-buffer-test")
else {
return;
};

let buffer = BufferBuilder::new()
.with_label("lambda-buffer-write-test")
.with_usage(Usage::UNIFORM)
.with_properties(Properties::CPU_VISIBLE)
.with_buffer_type(BufferType::Uniform)
.build(&gpu, vec![0_u32; 16])
.expect("build uniform buffer");

buffer.write_value(&gpu, 0, &0x1122_3344_u32);
buffer
.write_slice(&gpu, 0, &[1_u32, 2_u32, 3_u32])
.expect("write slice");
}

/// Builds a typed uniform buffer wrapper and performs an update write.
#[test]
fn uniform_buffer_wrapper_builds_and_writes() {
let Some(gpu) = crate::render::gpu::create_test_gpu("lambda-buffer-test")
else {
return;
};

let initial = 7_u32;
let ubo =
UniformBuffer::new(&gpu, &initial, Some("lambda-ubo-test")).unwrap();
ubo.write(&gpu, &9_u32);

let _ = ubo.raw();
}
}
Loading
Loading