Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
19 changes: 17 additions & 2 deletions .github/workflows/compile_lambda_rs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,23 @@ jobs:
if: ${{ matrix.os == 'ubuntu-latest' }}
run: |
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"
# Prefer Mesa's software Vulkan (lavapipe) to ensure headless availability.
# The exact ICD filename can differ across Ubuntu images, so discover it.
LVP_ICD="$(
if [[ -d /usr/share/vulkan/icd.d ]]; then
find /usr/share/vulkan/icd.d -maxdepth 1 -type f \
\( -name '*lvp_icd*.json' -o -name '*lavapipe*.json' \) \
-print 2>/dev/null | head -n1
fi
)"
if [[ -z "$LVP_ICD" ]]; then
echo "lavapipe Vulkan ICD not found under /usr/share/vulkan/icd.d" >&2
ls -la /usr/share/vulkan/icd.d || true
else
echo "Using lavapipe ICD: $LVP_ICD"
echo "VK_ICD_FILENAMES=$LVP_ICD" >> "$GITHUB_ENV"
fi
echo "LAMBDA_REQUIRE_GPU_ADAPTER=1" >> "$GITHUB_ENV"
vulkaninfo --summary || true

# Windows runners already include the required toolchain for DX12 builds.
Expand Down
20 changes: 18 additions & 2 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 Expand Up @@ -60,8 +62,22 @@ jobs:
- name: Configure Vulkan (Ubuntu)
run: |
echo "WGPU_BACKEND=vulkan" >> "$GITHUB_ENV"
# Prefer Mesa's software Vulkan (lavapipe) for headless availability
echo "VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/lvp_icd.x86_64.json" >> "$GITHUB_ENV"
# Prefer Mesa's software Vulkan (lavapipe) for headless availability.
# The exact ICD filename can differ across Ubuntu images, so discover it.
LVP_ICD="$(
if [[ -d /usr/share/vulkan/icd.d ]]; then
find /usr/share/vulkan/icd.d -maxdepth 1 -type f \
\( -name '*lvp_icd*.json' -o -name '*lavapipe*.json' \) \
-print 2>/dev/null | head -n1
fi
)"
if [[ -z "$LVP_ICD" ]]; then
echo "lavapipe Vulkan ICD not found under /usr/share/vulkan/icd.d" >&2
ls -la /usr/share/vulkan/icd.d || true
else
echo "Using lavapipe ICD: $LVP_ICD"
echo "VK_ICD_FILENAMES=$LVP_ICD" >> "$GITHUB_ENV"
fi
vulkaninfo --summary || true

- name: Generate full coverage JSON
Expand Down
18 changes: 17 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,23 @@ jobs:
- name: Configure Vulkan for headless CI
run: |
echo "WGPU_BACKEND=vulkan" >> "$GITHUB_ENV"
echo "VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/lvp_icd.x86_64.json" >> "$GITHUB_ENV"
# Prefer Mesa's software Vulkan (lavapipe) for headless availability.
# The exact ICD filename can differ across Ubuntu images, so discover it.
LVP_ICD="$(
if [[ -d /usr/share/vulkan/icd.d ]]; then
find /usr/share/vulkan/icd.d -maxdepth 1 -type f \
\( -name '*lvp_icd*.json' -o -name '*lavapipe*.json' \) \
-print 2>/dev/null | head -n1
fi
)"
if [[ -z "$LVP_ICD" ]]; then
echo "lavapipe Vulkan ICD not found under /usr/share/vulkan/icd.d" >&2
ls -la /usr/share/vulkan/icd.d || true
else
echo "Using lavapipe ICD: $LVP_ICD"
echo "VK_ICD_FILENAMES=$LVP_ICD" >> "$GITHUB_ENV"
fi
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
Loading
Loading