Skip to content

Commit 0bfe374

Browse files
committed
[add] more unit tests for buffer and render pass abstraction.
1 parent 6c18fc6 commit 0bfe374

File tree

3 files changed

+170
-33
lines changed

3 files changed

+170
-33
lines changed

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

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,117 @@ impl Frame {
343343
}
344344
}
345345

346+
// ---------------------- Command Encoding Abstractions -----------------------
347+
348+
#[derive(Debug)]
349+
/// Thin wrapper around `wgpu::CommandEncoder` with convenience helpers.
350+
pub struct CommandEncoder {
351+
raw: wgpu::CommandEncoder,
352+
}
353+
354+
impl CommandEncoder {
355+
/// Create a new command encoder with an optional label.
356+
pub fn new(device: &wgpu::Device, label: Option<&str>) -> Self {
357+
let raw =
358+
device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label });
359+
return Self { raw };
360+
}
361+
362+
/// Begin a render pass targeting a single color attachment with the provided
363+
/// load/store operations. Depth/stencil is not attached by this helper.
364+
pub fn begin_render_pass<'view>(
365+
&'view mut self,
366+
label: Option<&str>,
367+
view: &'view wgpu::TextureView,
368+
ops: wgpu::Operations<wgpu::Color>,
369+
) -> RenderPass<'view> {
370+
let color_attachment = wgpu::RenderPassColorAttachment {
371+
view,
372+
resolve_target: None,
373+
depth_slice: None,
374+
ops,
375+
};
376+
let color_attachments = [Some(color_attachment)];
377+
let pass = self.raw.begin_render_pass(&wgpu::RenderPassDescriptor {
378+
label,
379+
color_attachments: &color_attachments,
380+
depth_stencil_attachment: None,
381+
timestamp_writes: None,
382+
occlusion_query_set: None,
383+
});
384+
return RenderPass { raw: pass };
385+
}
386+
387+
/// Finish recording and return the command buffer.
388+
pub fn finish(self) -> wgpu::CommandBuffer {
389+
return self.raw.finish();
390+
}
391+
}
392+
393+
#[derive(Debug)]
394+
/// Wrapper around `wgpu::RenderPass<'_>` exposing the operations needed by the
395+
/// Lambda renderer without leaking raw `wgpu` types at the call sites.
396+
pub struct RenderPass<'a> {
397+
raw: wgpu::RenderPass<'a>,
398+
}
399+
400+
impl<'a> RenderPass<'a> {
401+
/// Set the active render pipeline.
402+
pub fn set_pipeline(&mut self, pipeline: &wgpu::RenderPipeline) {
403+
self.raw.set_pipeline(pipeline);
404+
}
405+
406+
/// Apply viewport state.
407+
pub fn set_viewport(
408+
&mut self,
409+
x: f32,
410+
y: f32,
411+
width: f32,
412+
height: f32,
413+
min_depth: f32,
414+
max_depth: f32,
415+
) {
416+
self
417+
.raw
418+
.set_viewport(x, y, width, height, min_depth, max_depth);
419+
}
420+
421+
/// Apply scissor rectangle.
422+
pub fn set_scissor_rect(&mut self, x: u32, y: u32, width: u32, height: u32) {
423+
self.raw.set_scissor_rect(x, y, width, height);
424+
}
425+
426+
/// Bind a group with optional dynamic offsets.
427+
pub fn set_bind_group(
428+
&mut self,
429+
set: u32,
430+
group: &wgpu::BindGroup,
431+
dynamic_offsets: &[u32],
432+
) {
433+
self.raw.set_bind_group(set, group, dynamic_offsets);
434+
}
435+
436+
/// Bind a vertex buffer slot.
437+
pub fn set_vertex_buffer(&mut self, slot: u32, buffer: &wgpu::Buffer) {
438+
self.raw.set_vertex_buffer(slot, buffer.slice(..));
439+
}
440+
441+
/// Upload push constants.
442+
pub fn set_push_constants(
443+
&mut self,
444+
stages: wgpu::ShaderStages,
445+
offset: u32,
446+
data: &[u8],
447+
) {
448+
self.raw.set_push_constants(stages, offset, data);
449+
}
450+
451+
/// Issue a non-indexed draw over a vertex range.
452+
pub fn draw(&mut self, vertices: std::ops::Range<u32>) {
453+
self.raw.draw(vertices, 0..1);
454+
}
455+
}
456+
346457
#[derive(Debug, Clone)]
347458
/// Builder for a `Gpu` (adapter, device, queue) with feature validation.
348459
pub struct GpuBuilder {

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

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -239,15 +239,7 @@ impl BufferBuilder {
239239
data: Vec<Data>,
240240
) -> Result<Buffer, &'static str> {
241241
let element_size = std::mem::size_of::<Data>();
242-
let buffer_length = if self.buffer_length == 0 {
243-
element_size * data.len()
244-
} else {
245-
self.buffer_length
246-
};
247-
248-
if buffer_length == 0 {
249-
return Err("Attempted to create a buffer with zero length.");
250-
}
242+
let buffer_length = self.resolve_length(element_size, data.len())?;
251243

252244
// SAFETY: Converting data to bytes is safe because it's underlying
253245
// type, Data, is constrianed to Copy and the lifetime of the slice does
@@ -290,3 +282,51 @@ impl BufferBuilder {
290282
.build(render_context, mesh.vertices().to_vec());
291283
}
292284
}
285+
286+
impl BufferBuilder {
287+
/// Resolve the effective buffer length from explicit size or data length.
288+
/// Returns an error if the resulting length would be zero.
289+
pub(crate) fn resolve_length(
290+
&self,
291+
element_size: usize,
292+
data_len: usize,
293+
) -> Result<usize, &'static str> {
294+
let buffer_length = if self.buffer_length == 0 {
295+
element_size * data_len
296+
} else {
297+
self.buffer_length
298+
};
299+
if buffer_length == 0 {
300+
return Err("Attempted to create a buffer with zero length.");
301+
}
302+
return Ok(buffer_length);
303+
}
304+
}
305+
306+
#[cfg(test)]
307+
mod tests {
308+
use super::*;
309+
310+
#[test]
311+
fn resolve_length_rejects_zero() {
312+
let builder = BufferBuilder::new();
313+
let result = builder.resolve_length(std::mem::size_of::<u32>(), 0);
314+
assert!(result.is_err());
315+
}
316+
317+
#[test]
318+
fn label_is_recorded_on_builder() {
319+
let mut builder = BufferBuilder::new();
320+
builder.with_label("buffer-test");
321+
// Indirect check via building a small buffer would require a device; ensure
322+
// the label setter stores the value locally instead.
323+
// Access through an internal helper to avoid exposing label publicly.
324+
#[allow(clippy::redundant_closure_call)]
325+
{
326+
// Create a small closure to read the private label field.
327+
// The test module shares the parent scope, so it can access fields.
328+
let read = |b: &BufferBuilder| b.label.as_deref();
329+
assert_eq!(read(&builder), Some("buffer-test"));
330+
}
331+
}
332+
}

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

Lines changed: 10 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use std::iter;
1919

2020
use lambda_platform::wgpu::{
2121
types as wgpu,
22+
CommandEncoder as PlatformCommandEncoder,
2223
Gpu,
2324
GpuBuilder,
2425
Instance,
@@ -251,12 +252,10 @@ impl RenderContext {
251252
};
252253

253254
let view = frame.texture_view();
254-
let mut encoder =
255-
self
256-
.device()
257-
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
258-
label: Some("lambda-render-command-encoder"),
259-
});
255+
let mut encoder = PlatformCommandEncoder::new(
256+
self.device(),
257+
Some("lambda-render-command-encoder"),
258+
);
260259

261260
let mut command_iter = commands.into_iter();
262261
while let Some(command) = command_iter.next() {
@@ -271,21 +270,8 @@ impl RenderContext {
271270
))
272271
})?;
273272

274-
let color_attachment = wgpu::RenderPassColorAttachment {
275-
view,
276-
depth_slice: None,
277-
resolve_target: None,
278-
ops: pass.color_ops(),
279-
};
280-
let color_attachments = [Some(color_attachment)];
281273
let mut pass_encoder =
282-
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
283-
label: pass.label(),
284-
color_attachments: &color_attachments,
285-
depth_stencil_attachment: None,
286-
timestamp_writes: None,
287-
occlusion_query_set: None,
288-
});
274+
encoder.begin_render_pass(pass.label(), view, pass.color_ops());
289275

290276
self.encode_pass(&mut pass_encoder, viewport, &mut command_iter)?;
291277
}
@@ -306,7 +292,7 @@ impl RenderContext {
306292
/// Encode a single render pass and consume commands until `EndRenderPass`.
307293
fn encode_pass<I>(
308294
&mut self,
309-
pass: &mut wgpu::RenderPass<'_>,
295+
pass: &mut lambda_platform::wgpu::RenderPass<'_>,
310296
initial_viewport: viewport::Viewport,
311297
commands: &mut I,
312298
) -> Result<(), RenderError>
@@ -372,7 +358,7 @@ impl RenderContext {
372358
));
373359
})?;
374360

375-
pass.set_vertex_buffer(buffer as u32, buffer_ref.raw().slice(..));
361+
pass.set_vertex_buffer(buffer as u32, buffer_ref.raw());
376362
}
377363
RenderCommand::PushConstants {
378364
pipeline,
@@ -394,7 +380,7 @@ impl RenderContext {
394380
pass.set_push_constants(stage.to_wgpu(), offset, slice);
395381
}
396382
RenderCommand::Draw { vertices } => {
397-
pass.draw(vertices, 0..1);
383+
pass.draw(vertices);
398384
}
399385
RenderCommand::BeginRenderPass { .. } => {
400386
return Err(RenderError::Configuration(
@@ -411,7 +397,7 @@ impl RenderContext {
411397

412398
/// Apply both viewport and scissor state to the active pass.
413399
fn apply_viewport(
414-
pass: &mut wgpu::RenderPass<'_>,
400+
pass: &mut lambda_platform::wgpu::RenderPass<'_>,
415401
viewport: &viewport::Viewport,
416402
) {
417403
let (x, y, width, height, min_depth, max_depth) = viewport.viewport_f32();

0 commit comments

Comments
 (0)