Skip to content

Commit a9ecb6d

Browse files
committed
[add] uniform buffer objects, bind groups, and scene math helpers for simplifying camera code.
1 parent 0fdc489 commit a9ecb6d

File tree

11 files changed

+1455
-35
lines changed

11 files changed

+1455
-35
lines changed
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
//! Bind group and bind group layout builders for the platform layer.
2+
//!
3+
//! These types provide a thin, explicit wrapper around `wgpu` bind resources
4+
//! so higher layers can compose layouts and groups without pulling in raw
5+
//! `wgpu` descriptors throughout the codebase.
6+
7+
use std::num::NonZeroU64;
8+
9+
use crate::wgpu::types as wgpu;
10+
11+
#[derive(Debug)]
12+
/// Wrapper around `wgpu::BindGroupLayout` that preserves a label.
13+
pub struct BindGroupLayout {
14+
pub(crate) raw: wgpu::BindGroupLayout,
15+
pub(crate) label: Option<String>,
16+
}
17+
18+
impl BindGroupLayout {
19+
/// Borrow the underlying `wgpu::BindGroupLayout`.
20+
pub fn raw(&self) -> &wgpu::BindGroupLayout {
21+
&self.raw
22+
}
23+
24+
/// Optional debug label used during creation.
25+
pub fn label(&self) -> Option<&str> {
26+
self.label.as_deref()
27+
}
28+
}
29+
30+
#[derive(Debug)]
31+
/// Wrapper around `wgpu::BindGroup` that preserves a label.
32+
pub struct BindGroup {
33+
pub(crate) raw: wgpu::BindGroup,
34+
pub(crate) label: Option<String>,
35+
}
36+
37+
impl BindGroup {
38+
/// Borrow the underlying `wgpu::BindGroup`.
39+
pub fn raw(&self) -> &wgpu::BindGroup {
40+
&self.raw
41+
}
42+
43+
/// Optional debug label used during creation.
44+
pub fn label(&self) -> Option<&str> {
45+
self.label.as_deref()
46+
}
47+
}
48+
49+
#[derive(Clone, Copy, Debug)]
50+
/// Visibility of a binding across shader stages.
51+
pub enum Visibility {
52+
Vertex,
53+
Fragment,
54+
Compute,
55+
VertexAndFragment,
56+
All,
57+
}
58+
59+
impl Visibility {
60+
fn to_wgpu(self) -> wgpu::ShaderStages {
61+
match self {
62+
Visibility::Vertex => wgpu::ShaderStages::VERTEX,
63+
Visibility::Fragment => wgpu::ShaderStages::FRAGMENT,
64+
Visibility::Compute => wgpu::ShaderStages::COMPUTE,
65+
Visibility::VertexAndFragment => {
66+
wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT
67+
}
68+
Visibility::All => wgpu::ShaderStages::all(),
69+
}
70+
}
71+
}
72+
73+
#[derive(Default)]
74+
/// Builder for creating a `wgpu::BindGroupLayout`.
75+
pub struct BindGroupLayoutBuilder {
76+
label: Option<String>,
77+
entries: Vec<wgpu::BindGroupLayoutEntry>,
78+
}
79+
80+
impl BindGroupLayoutBuilder {
81+
/// Create a builder with no entries.
82+
pub fn new() -> Self {
83+
Self {
84+
label: None,
85+
entries: Vec::new(),
86+
}
87+
}
88+
89+
/// Attach a human‑readable label.
90+
pub fn with_label(mut self, label: &str) -> Self {
91+
self.label = Some(label.to_string());
92+
self
93+
}
94+
95+
/// Declare a uniform buffer binding at the provided index.
96+
pub fn with_uniform(mut self, binding: u32, visibility: Visibility) -> Self {
97+
self.entries.push(wgpu::BindGroupLayoutEntry {
98+
binding,
99+
visibility: visibility.to_wgpu(),
100+
ty: wgpu::BindingType::Buffer {
101+
ty: wgpu::BufferBindingType::Uniform,
102+
has_dynamic_offset: false,
103+
min_binding_size: None,
104+
},
105+
count: None,
106+
});
107+
self
108+
}
109+
110+
/// Declare a uniform buffer binding with dynamic offsets at the provided index.
111+
pub fn with_uniform_dynamic(
112+
mut self,
113+
binding: u32,
114+
visibility: Visibility,
115+
) -> Self {
116+
self.entries.push(wgpu::BindGroupLayoutEntry {
117+
binding,
118+
visibility: visibility.to_wgpu(),
119+
ty: wgpu::BindingType::Buffer {
120+
ty: wgpu::BufferBindingType::Uniform,
121+
has_dynamic_offset: true,
122+
min_binding_size: None,
123+
},
124+
count: None,
125+
});
126+
self
127+
}
128+
129+
/// Build the layout using the provided device.
130+
pub fn build(self, device: &wgpu::Device) -> BindGroupLayout {
131+
let raw =
132+
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
133+
label: self.label.as_deref(),
134+
entries: &self.entries,
135+
});
136+
BindGroupLayout {
137+
raw,
138+
label: self.label,
139+
}
140+
}
141+
}
142+
143+
#[derive(Default)]
144+
/// Builder for creating a `wgpu::BindGroup`.
145+
pub struct BindGroupBuilder<'a> {
146+
label: Option<String>,
147+
layout: Option<&'a wgpu::BindGroupLayout>,
148+
entries: Vec<wgpu::BindGroupEntry<'a>>,
149+
}
150+
151+
impl<'a> BindGroupBuilder<'a> {
152+
/// Create a new builder with no layout or entries.
153+
pub fn new() -> Self {
154+
Self {
155+
label: None,
156+
layout: None,
157+
entries: Vec::new(),
158+
}
159+
}
160+
161+
/// Attach a human‑readable label.
162+
pub fn with_label(mut self, label: &str) -> Self {
163+
self.label = Some(label.to_string());
164+
self
165+
}
166+
167+
/// Specify the layout to use for this bind group.
168+
pub fn with_layout(mut self, layout: &'a BindGroupLayout) -> Self {
169+
self.layout = Some(layout.raw());
170+
self
171+
}
172+
173+
/// Bind a uniform buffer at a binding index with optional size slice.
174+
pub fn with_uniform(
175+
mut self,
176+
binding: u32,
177+
buffer: &'a wgpu::Buffer,
178+
offset: u64,
179+
size: Option<NonZeroU64>,
180+
) -> Self {
181+
self.entries.push(wgpu::BindGroupEntry {
182+
binding,
183+
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
184+
buffer,
185+
offset,
186+
size,
187+
}),
188+
});
189+
self
190+
}
191+
192+
/// Build the bind group with the accumulated entries.
193+
pub fn build(self, device: &wgpu::Device) -> BindGroup {
194+
let layout = self
195+
.layout
196+
.expect("BindGroupBuilder requires a layout before build");
197+
let raw = device.create_bind_group(&wgpu::BindGroupDescriptor {
198+
label: self.label.as_deref(),
199+
layout,
200+
entries: &self.entries,
201+
});
202+
BindGroup {
203+
raw,
204+
label: self.label,
205+
}
206+
}
207+
}

crates/lambda-rs-platform/src/wgpu/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ use wgpu::rwh::{
1515

1616
use crate::winit::WindowHandle;
1717

18+
pub mod bind;
19+
1820
#[derive(Debug, Clone)]
1921
/// Builder for creating a `wgpu::Instance` with consistent defaults.
2022
///

crates/lambda-rs/examples/push_constants.rs

Lines changed: 21 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ use lambda::{
2121
RenderPipelineBuilder,
2222
},
2323
render_pass::RenderPassBuilder,
24+
scene_math::{
25+
compute_model_view_projection_matrix_about_pivot,
26+
SimpleCamera,
27+
},
2428
shader::{
2529
Shader,
2630
ShaderBuilder,
@@ -100,23 +104,7 @@ pub fn push_constants_to_bytes(push_constants: &PushConstant) -> &[u32] {
100104
return bytes;
101105
}
102106

103-
fn make_transform(
104-
translate: [f32; 3],
105-
angle: f32,
106-
scale: f32,
107-
) -> [[f32; 4]; 4] {
108-
let c = angle.cos() * scale;
109-
let s = angle.sin() * scale;
110-
111-
let [x, y, z] = translate;
112-
113-
return [
114-
[c, 0.0, s, 0.0],
115-
[0.0, scale, 0.0, 0.0],
116-
[-s, 0.0, c, 0.0],
117-
[x, y, z, 1.0],
118-
];
119-
}
107+
// Model, view, and projection matrix computations are handled by `scene_math`.
120108

121109
// --------------------------------- COMPONENT ---------------------------------
122110

@@ -196,6 +184,7 @@ impl Component<ComponentResult, String> for PushConstantsExample {
196184
logging::trace!("mesh: {:?}", mesh);
197185

198186
let pipeline = RenderPipelineBuilder::new()
187+
.with_culling(lambda::render::pipeline::CullingMode::None)
199188
.with_push_constant(PipelineStage::VERTEX, push_constant_size)
200189
.with_buffer(
201190
BufferBuilder::build_from_mesh(&mesh, render_context)
@@ -253,25 +242,23 @@ impl Component<ComponentResult, String> for PushConstantsExample {
253242
render_context: &mut lambda::render::RenderContext,
254243
) -> Vec<lambda::render::command::RenderCommand> {
255244
self.frame_number += 1;
256-
let camera = [0.0, 0.0, -2.0];
257-
let view: [[f32; 4]; 4] = matrix::translation_matrix(camera);
258-
259-
// Create a projection matrix.
260-
let projection: [[f32; 4]; 4] =
261-
matrix::perspective_matrix(0.25, (4 / 3) as f32, 0.1, 100.0);
262-
263-
// Rotate model.
264-
let model: [[f32; 4]; 4] = matrix::rotate_matrix(
265-
matrix::identity_matrix(4, 4),
245+
let camera = SimpleCamera {
246+
position: [0.0, 0.0, 3.0],
247+
field_of_view_in_turns: 0.25,
248+
near_clipping_plane: 0.1,
249+
far_clipping_plane: 100.0,
250+
};
251+
let mesh_matrix = compute_model_view_projection_matrix_about_pivot(
252+
&camera,
253+
self.width.max(1),
254+
self.height.max(1),
255+
[0.0, -1.0 / 3.0, 0.0],
266256
[0.0, 1.0, 0.0],
267257
0.001 * self.frame_number as f32,
258+
0.5,
259+
[0.0, 1.0 / 3.0, 0.0],
268260
);
269261

270-
// Create render matrix.
271-
let mesh_matrix = projection.multiply(&view).multiply(&model);
272-
let mesh_matrix =
273-
make_transform([0.0, 0.0, 0.5], self.frame_number as f32 * 0.01, 0.5);
274-
275262
// Create viewport.
276263
let viewport =
277264
viewport::ViewportBuilder::new().build(self.width, self.height);
@@ -309,7 +296,8 @@ impl Component<ComponentResult, String> for PushConstantsExample {
309296
offset: 0,
310297
bytes: Vec::from(push_constants_to_bytes(&PushConstant {
311298
data: [0.0, 0.0, 0.0, 0.0],
312-
render_matrix: mesh_matrix,
299+
// Transpose to match GPU's column‑major expectation.
300+
render_matrix: mesh_matrix.transpose(),
313301
})),
314302
},
315303
RenderCommand::Draw {

0 commit comments

Comments
 (0)