From 5ee2f0e6ad7244a541a0ae944e1e4fc7bc2ee5ab Mon Sep 17 00:00:00 2001 From: vmarcella Date: Sat, 1 Nov 2025 17:18:41 -0700 Subject: [PATCH 01/11] [add] render pipeline abstraction. --- crates/lambda-rs-platform/src/wgpu/mod.rs | 1 + .../lambda-rs-platform/src/wgpu/pipeline.rs | 243 ++++++++++++++++++ crates/lambda-rs/src/render/pipeline.rs | 116 +++------ 3 files changed, 281 insertions(+), 79 deletions(-) create mode 100644 crates/lambda-rs-platform/src/wgpu/pipeline.rs diff --git a/crates/lambda-rs-platform/src/wgpu/mod.rs b/crates/lambda-rs-platform/src/wgpu/mod.rs index b30569ff..006be3fc 100644 --- a/crates/lambda-rs-platform/src/wgpu/mod.rs +++ b/crates/lambda-rs-platform/src/wgpu/mod.rs @@ -17,6 +17,7 @@ use crate::winit::WindowHandle; pub mod bind; pub mod buffer; +pub mod pipeline; #[derive(Debug, Clone)] /// Builder for creating a `wgpu::Instance` with consistent defaults. diff --git a/crates/lambda-rs-platform/src/wgpu/pipeline.rs b/crates/lambda-rs-platform/src/wgpu/pipeline.rs new file mode 100644 index 00000000..b0285361 --- /dev/null +++ b/crates/lambda-rs-platform/src/wgpu/pipeline.rs @@ -0,0 +1,243 @@ +//! Pipeline and shader module wrappers/builders for the platform layer. + +use crate::wgpu::types as wgpu; + +#[derive(Debug)] +/// Wrapper around `wgpu::ShaderModule` that preserves a label. +pub struct ShaderModule { + raw: wgpu::ShaderModule, + label: Option, +} + +impl ShaderModule { + /// Create a shader module from SPIR-V words. + pub fn from_spirv( + device: &wgpu::Device, + words: &[u32], + label: Option<&str>, + ) -> Self { + let raw = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label, + source: wgpu::ShaderSource::SpirV(std::borrow::Cow::Borrowed(words)), + }); + return Self { + raw, + label: label.map(|s| s.to_string()), + }; + } + + /// Borrow the raw shader module. + pub fn raw(&self) -> &wgpu::ShaderModule { + &self.raw + } +} + +#[derive(Debug)] +/// Wrapper around `wgpu::PipelineLayout`. +pub struct PipelineLayout { + raw: wgpu::PipelineLayout, + label: Option, +} + +impl PipelineLayout { + /// Borrow the raw pipeline layout. + pub fn raw(&self) -> &wgpu::PipelineLayout { + &self.raw + } +} + +/// Builder for creating a `PipelineLayout`. +pub struct PipelineLayoutBuilder<'a> { + label: Option, + layouts: Vec<&'a wgpu::BindGroupLayout>, + push_constant_ranges: Vec, +} + +impl<'a> PipelineLayoutBuilder<'a> { + /// New builder with no layouts or push constants. + pub fn new() -> Self { + return Self { + label: None, + layouts: Vec::new(), + push_constant_ranges: Vec::new(), + }; + } + + /// Attach a label. + pub fn with_label(mut self, label: &str) -> Self { + self.label = Some(label.to_string()); + self + } + + /// Provide bind group layouts. + pub fn with_layouts(mut self, layouts: &'a [&wgpu::BindGroupLayout]) -> Self { + self.layouts = layouts.to_vec(); + self + } + + /// Provide push constant ranges. + pub fn with_push_constants( + mut self, + ranges: Vec, + ) -> Self { + self.push_constant_ranges = ranges; + self + } + + /// Build the layout. + pub fn build(self, device: &wgpu::Device) -> PipelineLayout { + let raw = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: self.label.as_deref(), + bind_group_layouts: &self.layouts, + push_constant_ranges: &self.push_constant_ranges, + }); + return PipelineLayout { + raw, + label: self.label, + }; + } +} + +#[derive(Debug)] +/// Wrapper around `wgpu::RenderPipeline`. +pub struct RenderPipeline { + raw: wgpu::RenderPipeline, + label: Option, +} + +impl RenderPipeline { + /// Borrow the raw pipeline. + pub fn raw(&self) -> &wgpu::RenderPipeline { + &self.raw + } + /// Consume and return the raw pipeline. + pub fn into_raw(self) -> wgpu::RenderPipeline { + self.raw + } +} + +/// Builder for creating a graphics render pipeline. +pub struct RenderPipelineBuilder<'a> { + label: Option, + layout: Option<&'a wgpu::PipelineLayout>, + vertex_buffers: Vec<(u64, Vec)>, + cull_mode: Option, + color_target: Option, +} + +impl<'a> RenderPipelineBuilder<'a> { + /// New builder with defaults. + pub fn new() -> Self { + return Self { + label: None, + layout: None, + vertex_buffers: Vec::new(), + cull_mode: Some(wgpu::Face::Back), + color_target: None, + }; + } + + /// Attach a label. + pub fn with_label(mut self, label: &str) -> Self { + self.label = Some(label.to_string()); + self + } + + /// Use the provided pipeline layout. + pub fn with_layout(mut self, layout: &'a PipelineLayout) -> Self { + self.layout = Some(layout.raw()); + self + } + + /// Add a vertex buffer layout with attributes. + pub fn with_vertex_buffer( + mut self, + array_stride: u64, + attributes: Vec, + ) -> Self { + self.vertex_buffers.push((array_stride, attributes)); + self + } + + /// Set cull mode (None disables culling). + pub fn with_cull_mode(mut self, face: Option) -> Self { + self.cull_mode = face; + self + } + + /// Set single color target for fragment stage. + pub fn with_color_target(mut self, target: wgpu::ColorTargetState) -> Self { + self.color_target = Some(target); + self + } + + /// Build the render pipeline from provided shader modules. + pub fn build( + self, + device: &wgpu::Device, + vertex_shader: &ShaderModule, + fragment_shader: Option<&ShaderModule>, + ) -> RenderPipeline { + let mut attr_storage: Vec> = Vec::new(); + let mut strides: Vec = Vec::new(); + for (stride, attrs) in &self.vertex_buffers { + let boxed: Box<[wgpu::VertexAttribute]> = + attrs.clone().into_boxed_slice(); + attr_storage.push(boxed); + strides.push(*stride); + } + // Now build layouts referencing the stable storage in `attr_storage`. + let mut vbl: Vec> = Vec::new(); + for (i, boxed) in attr_storage.iter().enumerate() { + let slice = boxed.as_ref(); + vbl.push(wgpu::VertexBufferLayout { + array_stride: strides[i], + step_mode: wgpu::VertexStepMode::Vertex, + attributes: slice, + }); + } + + let color_targets: Vec> = + match &self.color_target { + Some(ct) => vec![Some(ct.clone())], + None => Vec::new(), + }; + + let fragment = fragment_shader.map(|fs| wgpu::FragmentState { + module: fs.raw(), + entry_point: Some("main"), + compilation_options: wgpu::PipelineCompilationOptions::default(), + targets: color_targets.as_slice(), + }); + + let vertex_state = wgpu::VertexState { + module: vertex_shader.raw(), + entry_point: Some("main"), + compilation_options: wgpu::PipelineCompilationOptions::default(), + buffers: vbl.as_slice(), + }; + + let primitive_state = wgpu::PrimitiveState { + cull_mode: self.cull_mode, + ..wgpu::PrimitiveState::default() + }; + + let layout_ref = self.layout; + let raw = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: self.label.as_deref(), + layout: layout_ref, + vertex: vertex_state, + primitive: primitive_state, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + fragment, + multiview: None, + cache: None, + }); + + return RenderPipeline { + raw, + label: self.label, + }; + } +} diff --git a/crates/lambda-rs/src/render/pipeline.rs b/crates/lambda-rs/src/render/pipeline.rs index f7099f49..facd8983 100644 --- a/crates/lambda-rs/src/render/pipeline.rs +++ b/crates/lambda-rs/src/render/pipeline.rs @@ -2,12 +2,14 @@ //! applications. use std::{ - borrow::Cow, ops::Range, rc::Rc, }; -use lambda_platform::wgpu::types as wgpu; +use lambda_platform::wgpu::{ + pipeline as platform_pipeline, + types as wgpu, +}; use super::{ bind, @@ -177,21 +179,21 @@ impl RenderPipelineBuilder { let device = render_context.device(); let surface_format = render_context.surface_format(); - let vertex_shader_module = - device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("lambda-vertex-shader"), - source: wgpu::ShaderSource::SpirV(Cow::Owned( - vertex_shader.as_binary(), - )), - }); - - let fragment_shader_module = fragment_shader.map(|shader| { - device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("lambda-fragment-shader"), - source: wgpu::ShaderSource::SpirV(Cow::Owned(shader.as_binary())), - }) + // Shader modules + let vertex_module = platform_pipeline::ShaderModule::from_spirv( + device, + &vertex_shader.as_binary(), + Some("lambda-vertex-shader"), + ); + let fragment_module = fragment_shader.map(|shader| { + platform_pipeline::ShaderModule::from_spirv( + device, + &shader.as_binary(), + Some("lambda-fragment-shader"), + ) }); + // Push constant ranges let push_constant_ranges: Vec = self .push_constants .iter() @@ -201,6 +203,7 @@ impl RenderPipelineBuilder { }) .collect(); + // Bind group layouts limit check let max_bind_groups = render_context.limit_max_bind_groups() as usize; assert!( self.bind_group_layouts.len() <= max_bind_groups, @@ -209,22 +212,22 @@ impl RenderPipelineBuilder { max_bind_groups ); - let bind_group_layout_refs: Vec<&wgpu::BindGroupLayout> = + // Pipeline layout via platform + let bgl_raw: Vec<&wgpu::BindGroupLayout> = self.bind_group_layouts.iter().map(|l| l.raw()).collect(); - let pipeline_layout = - device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("lambda-pipeline-layout"), - bind_group_layouts: &bind_group_layout_refs, - push_constant_ranges: &push_constant_ranges, - }); + let pipeline_layout = platform_pipeline::PipelineLayoutBuilder::new() + .with_label("lambda-pipeline-layout") + .with_layouts(&bgl_raw) + .with_push_constants(push_constant_ranges) + .build(device); - let mut attribute_storage: Vec> = - Vec::with_capacity(self.bindings.len()); - let mut vertex_buffer_layouts: Vec> = - Vec::with_capacity(self.bindings.len()); + // Vertex buffers and attributes let mut buffers = Vec::with_capacity(self.bindings.len()); + let mut rp_builder = platform_pipeline::RenderPipelineBuilder::new() + .with_label(self.label.as_deref().unwrap_or("lambda-render-pipeline")) + .with_layout(&pipeline_layout) + .with_cull_mode(self.culling.to_wgpu()); - // First, collect attributes and buffers for binding in &self.bindings { let attributes: Vec = binding .attributes @@ -235,68 +238,23 @@ impl RenderPipelineBuilder { format: attribute.element.format.to_vertex_format(), }) .collect(); - attribute_storage.push(attributes.into_boxed_slice()); + rp_builder = + rp_builder.with_vertex_buffer(binding.buffer.stride(), attributes); buffers.push(binding.buffer.clone()); } - // Then, build layouts referencing the stable storage - for (i, binding) in self.bindings.iter().enumerate() { - let attributes_slice = attribute_storage[i].as_ref(); - vertex_buffer_layouts.push(wgpu::VertexBufferLayout { - array_stride: binding.buffer.stride(), - step_mode: wgpu::VertexStepMode::Vertex, - attributes: attributes_slice, - }); - } - - // Stable storage for color targets to satisfy borrow checker - let mut color_targets: Vec> = Vec::new(); - if fragment_shader_module.is_some() { - color_targets.push(Some(wgpu::ColorTargetState { + if fragment_module.is_some() { + rp_builder = rp_builder.with_color_target(wgpu::ColorTargetState { format: surface_format, blend: Some(wgpu::BlendState::ALPHA_BLENDING), write_mask: wgpu::ColorWrites::ALL, - })); + }); } - let fragment = - fragment_shader_module - .as_ref() - .map(|module| wgpu::FragmentState { - module, - entry_point: Some("main"), - compilation_options: wgpu::PipelineCompilationOptions::default(), - targets: color_targets.as_slice(), - }); - - let vertex_state = wgpu::VertexState { - module: &vertex_shader_module, - entry_point: Some("main"), - compilation_options: wgpu::PipelineCompilationOptions::default(), - buffers: vertex_buffer_layouts.as_slice(), - }; - - let primitive_state = wgpu::PrimitiveState { - cull_mode: self.culling.to_wgpu(), - ..wgpu::PrimitiveState::default() - }; - - let pipeline_descriptor = wgpu::RenderPipelineDescriptor { - label: self.label.as_deref(), - layout: Some(&pipeline_layout), - vertex: vertex_state, - primitive: primitive_state, - depth_stencil: None, - multisample: wgpu::MultisampleState::default(), - fragment, - multiview: None, - cache: None, - }; - - let pipeline = device.create_render_pipeline(&pipeline_descriptor); + let rp = rp_builder.build(device, &vertex_module, fragment_module.as_ref()); return RenderPipeline { - pipeline: Rc::new(pipeline), + pipeline: Rc::new(rp.into_raw()), buffers, }; } From 0ad77c07330e9874b01cad8ed2d349b08cf2a3ec Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 3 Nov 2025 14:45:52 -0800 Subject: [PATCH 02/11] [update] rendering implementation to hide wgpu types from being directly used inside the high level rendering engine. --- crates/lambda-rs-platform/src/wgpu/bind.rs | 27 +- crates/lambda-rs-platform/src/wgpu/buffer.rs | 31 +- crates/lambda-rs-platform/src/wgpu/gpu.rs | 211 ++++++++ crates/lambda-rs-platform/src/wgpu/mod.rs | 473 +++--------------- .../lambda-rs-platform/src/wgpu/pipeline.rs | 231 ++++++--- crates/lambda-rs-platform/src/wgpu/surface.rs | 418 ++++++++++++++++ crates/lambda-rs-platform/src/wgpu/vertex.rs | 22 + crates/lambda-rs/examples/push_constants.rs | 2 +- .../examples/uniform_buffer_triangle.rs | 2 +- crates/lambda-rs/src/render/bind.rs | 81 ++- crates/lambda-rs/src/render/buffer.rs | 41 +- crates/lambda-rs/src/render/mesh.rs | 10 +- crates/lambda-rs/src/render/mod.rs | 77 +-- crates/lambda-rs/src/render/pipeline.rs | 113 ++--- crates/lambda-rs/src/render/render_pass.rs | 17 +- crates/lambda-rs/src/render/vertex.rs | 23 +- 16 files changed, 1074 insertions(+), 705 deletions(-) create mode 100644 crates/lambda-rs-platform/src/wgpu/gpu.rs create mode 100644 crates/lambda-rs-platform/src/wgpu/surface.rs create mode 100644 crates/lambda-rs-platform/src/wgpu/vertex.rs diff --git a/crates/lambda-rs-platform/src/wgpu/bind.rs b/crates/lambda-rs-platform/src/wgpu/bind.rs index 0b47fdc4..184e74c9 100644 --- a/crates/lambda-rs-platform/src/wgpu/bind.rs +++ b/crates/lambda-rs-platform/src/wgpu/bind.rs @@ -6,7 +6,12 @@ use std::num::NonZeroU64; -use crate::wgpu::types as wgpu; +use wgpu; + +use crate::wgpu::{ + buffer, + Gpu, +}; #[derive(Debug)] /// Wrapper around `wgpu::BindGroupLayout` that preserves a label. @@ -151,12 +156,14 @@ impl BindGroupLayoutBuilder { } /// Build the layout using the provided device. - pub fn build(self, device: &wgpu::Device) -> BindGroupLayout { + pub fn build(self, gpu: &Gpu) -> BindGroupLayout { let raw = - device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: self.label.as_deref(), - entries: &self.entries, - }); + gpu + .device() + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: self.label.as_deref(), + entries: &self.entries, + }); return BindGroupLayout { raw, label: self.label, @@ -198,14 +205,14 @@ impl<'a> BindGroupBuilder<'a> { pub fn with_uniform( mut self, binding: u32, - buffer: &'a wgpu::Buffer, + buffer: &'a buffer::Buffer, offset: u64, size: Option, ) -> Self { self.entries.push(wgpu::BindGroupEntry { binding, resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer, + buffer: buffer.raw(), offset, size, }), @@ -214,11 +221,11 @@ impl<'a> BindGroupBuilder<'a> { } /// Build the bind group with the accumulated entries. - pub fn build(self, device: &wgpu::Device) -> BindGroup { + pub fn build(self, gpu: &Gpu) -> BindGroup { let layout = self .layout .expect("BindGroupBuilder requires a layout before build"); - let raw = device.create_bind_group(&wgpu::BindGroupDescriptor { + let raw = gpu.device().create_bind_group(&wgpu::BindGroupDescriptor { label: self.label.as_deref(), layout, entries: &self.entries, diff --git a/crates/lambda-rs-platform/src/wgpu/buffer.rs b/crates/lambda-rs-platform/src/wgpu/buffer.rs index 4901f03c..819adb0b 100644 --- a/crates/lambda-rs-platform/src/wgpu/buffer.rs +++ b/crates/lambda-rs-platform/src/wgpu/buffer.rs @@ -3,12 +3,13 @@ //! This module provides a thin wrapper over `wgpu::Buffer` plus a small //! builder that handles common initialization patterns and keeps label and //! usage metadata for debugging/inspection. - -use crate::wgpu::{ - types as wgpu, - types::util::DeviceExt, +use wgpu::{ + self, + util::DeviceExt, }; +use crate::wgpu::Gpu; + #[derive(Clone, Copy, Debug)] /// Platform buffer usage flags. pub struct Usage(pub(crate) wgpu::BufferUsages); @@ -54,7 +55,7 @@ pub struct Buffer { impl Buffer { /// Borrow the underlying `wgpu::Buffer`. - pub fn raw(&self) -> &wgpu::Buffer { + pub(crate) fn raw(&self) -> &wgpu::Buffer { return &self.raw; } @@ -72,6 +73,11 @@ impl Buffer { pub fn usage(&self) -> wgpu::BufferUsages { return self.usage; } + + /// Write raw bytes into the buffer at the given offset. + pub fn write_bytes(&self, gpu: &Gpu, offset: u64, data: &[u8]) { + gpu.queue().write_buffer(&self.raw, offset, data); + } } #[derive(Default)] @@ -119,7 +125,7 @@ impl BufferBuilder { } /// Create a buffer initialized with `contents`. - pub fn build_init(self, device: &wgpu::Device, contents: &[u8]) -> Buffer { + pub fn build_init(self, gpu: &Gpu, contents: &[u8]) -> Buffer { let size = if self.size == 0 { contents.len() } else { @@ -131,11 +137,14 @@ impl BufferBuilder { usage |= wgpu::BufferUsages::COPY_DST; } - let raw = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: self.label.as_deref(), - contents, - usage, - }); + let raw = + gpu + .device() + .create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: self.label.as_deref(), + contents, + usage, + }); return Buffer { raw, diff --git a/crates/lambda-rs-platform/src/wgpu/gpu.rs b/crates/lambda-rs-platform/src/wgpu/gpu.rs new file mode 100644 index 00000000..4f00f982 --- /dev/null +++ b/crates/lambda-rs-platform/src/wgpu/gpu.rs @@ -0,0 +1,211 @@ +use pollster::block_on; + +use super::{ + surface::Surface, + Instance, +}; + +#[derive(Clone, Copy, Debug)] +/// Public, engine-facing subset of device limits. +pub struct GpuLimits { + pub max_uniform_buffer_binding_size: u64, + pub max_bind_groups: u32, + pub min_uniform_buffer_offset_alignment: u32, +} + +#[derive(Debug, Clone)] +/// Builder for a `Gpu` (adapter, device, queue) with feature validation. +pub struct GpuBuilder { + label: Option, + power_preference: wgpu::PowerPreference, + force_fallback_adapter: bool, + required_features: wgpu::Features, + memory_hints: wgpu::MemoryHints, +} + +impl GpuBuilder { + /// Create a builder with defaults favoring performance and push constants. + pub fn new() -> Self { + Self { + label: Some("Lambda GPU".to_string()), + power_preference: wgpu::PowerPreference::HighPerformance, + force_fallback_adapter: false, + required_features: wgpu::Features::PUSH_CONSTANTS, + memory_hints: wgpu::MemoryHints::Performance, + } + } + + /// Attach a label used for the device. + pub fn with_label(mut self, label: &str) -> Self { + self.label = Some(label.to_string()); + self + } + + /// Select the adapter power preference (e.g., LowPower for laptops). + pub fn with_power_preference( + mut self, + preference: wgpu::PowerPreference, + ) -> Self { + self.power_preference = preference; + self + } + + /// Force using a fallback adapter when a primary device is unavailable. + pub fn force_fallback(mut self, force: bool) -> Self { + self.force_fallback_adapter = force; + self + } + + /// Require `wgpu::Features` to be present on the adapter. + pub fn with_required_features(mut self, features: wgpu::Features) -> Self { + self.required_features = features; + self + } + + /// Provide memory allocation hints for the device. + pub fn with_memory_hints(mut self, hints: wgpu::MemoryHints) -> Self { + self.memory_hints = hints; + self + } + + /// Request an adapter and device/queue pair and return a `Gpu` wrapper. + /// + /// Returns an error if no adapter is available, required features are + /// missing, or device creation fails. + pub fn build<'surface, 'window>( + self, + instance: &Instance, + surface: Option<&Surface<'surface>>, + ) -> Result { + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions { + power_preference: self.power_preference, + force_fallback_adapter: self.force_fallback_adapter, + compatible_surface: surface.map(|surface| surface.surface()), + }) + .map_err(|_| GpuBuildError::AdapterUnavailable)?; + + let adapter_features = adapter.features(); + if !adapter_features.contains(self.required_features) { + return Err(GpuBuildError::MissingFeatures { + requested: self.required_features, + available: adapter_features, + }); + } + + let descriptor = wgpu::DeviceDescriptor { + label: self.label.as_deref(), + required_features: self.required_features, + required_limits: adapter.limits(), + memory_hints: self.memory_hints, + trace: wgpu::Trace::Off, + }; + + let (device, queue) = block_on(adapter.request_device(&descriptor))?; + + return Ok(Gpu { + adapter, + device, + queue, + features: descriptor.required_features, + limits: descriptor.required_limits, + }); + } +} + +#[derive(Debug)] +/// Errors emitted while building a `Gpu`. +pub enum GpuBuildError { + /// No compatible adapter could be found. + AdapterUnavailable, + /// The requested features are not supported by the selected adapter. + MissingFeatures { + requested: wgpu::Features, + available: wgpu::Features, + }, + /// Wrapper for `wgpu::RequestDeviceError`. + RequestDevice(wgpu::RequestDeviceError), +} + +impl From for GpuBuildError { + fn from(error: wgpu::RequestDeviceError) -> Self { + return GpuBuildError::RequestDevice(error); + } +} + +#[derive(Debug)] +/// Holds the chosen adapter along with its logical device and submission queue +/// plus immutable copies of features and limits used to create the device. +pub struct Gpu { + adapter: wgpu::Adapter, + device: wgpu::Device, + queue: wgpu::Queue, + features: wgpu::Features, + limits: wgpu::Limits, +} + +impl Gpu { + /// Borrow the adapter used to create the device. + /// + /// Crate-visible to avoid exposing raw `wgpu` to higher layers. + pub(crate) fn adapter(&self) -> &wgpu::Adapter { + &self.adapter + } + + /// Borrow the logical device for resource creation. + /// + /// Crate-visible to avoid exposing raw `wgpu` to higher layers. + pub(crate) fn device(&self) -> &wgpu::Device { + &self.device + } + + /// Borrow the submission queue for command submission. + /// + /// Crate-visible to avoid exposing raw `wgpu` to higher layers. + pub(crate) fn queue(&self) -> &wgpu::Queue { + &self.queue + } + + /// Features that were required and enabled during device creation. + pub(crate) fn features(&self) -> wgpu::Features { + self.features + } + + /// Limits captured at device creation time. + pub fn limits(&self) -> GpuLimits { + return GpuLimits { + max_uniform_buffer_binding_size: self + .limits + .max_uniform_buffer_binding_size + .into(), + max_bind_groups: self.limits.max_bind_groups, + min_uniform_buffer_offset_alignment: self + .limits + .min_uniform_buffer_offset_alignment, + }; + } + + /// Submit one or more command buffers to the device queue. + pub fn submit(&self, list: I) + where + I: IntoIterator, + { + let iter = list.into_iter().map(|cb| cb.into_raw()); + self.queue.submit(iter); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn gpu_build_error_wraps_request_device_error() { + // RequestDeviceError is opaque in wgpu 26 (no public constructors or variants). + // This test previously validated pattern matching on a specific variant; now we + // simply assert the From implementation exists by + // checking the trait bound at compile time. + fn assert_from_impl>() {} + assert_from_impl::(); + } +} diff --git a/crates/lambda-rs-platform/src/wgpu/mod.rs b/crates/lambda-rs-platform/src/wgpu/mod.rs index 006be3fc..5cda65e8 100644 --- a/crates/lambda-rs-platform/src/wgpu/mod.rs +++ b/crates/lambda-rs-platform/src/wgpu/mod.rs @@ -7,17 +7,29 @@ //! important handles when you need to drop down to raw `wgpu`. use pollster::block_on; -pub use wgpu as types; -use wgpu::rwh::{ - HasDisplayHandle as _, - HasWindowHandle as _, -}; - -use crate::winit::WindowHandle; pub mod bind; pub mod buffer; +pub mod gpu; pub mod pipeline; +pub mod surface; +pub mod vertex; + +pub use gpu::{ + Gpu, + GpuBuildError, + GpuBuilder, +}; +pub use surface::{ + Frame, + PresentMode, + Surface, + SurfaceBuilder, + SurfaceConfig, + SurfaceError, + SurfaceFormat, + TextureUsages, +}; #[derive(Debug, Clone)] /// Builder for creating a `wgpu::Instance` with consistent defaults. @@ -127,236 +139,32 @@ impl Instance { } } -#[derive(Debug, Clone)] -/// Builder for creating a `Surface` bound to a `winit` window. -pub struct SurfaceBuilder { - label: Option, -} - -impl SurfaceBuilder { - /// Create a builder with no label. - pub fn new() -> Self { - Self { label: None } - } - - /// Attach a human‑readable label for debugging/profiling. - pub fn with_label(mut self, label: &str) -> Self { - self.label = Some(label.to_string()); - self - } - - /// Create a `wgpu::Surface` for the provided `WindowHandle`. - /// - /// Safety: we use `create_surface_unsafe` by forwarding raw window/display - /// handles from `winit`. Lambda guarantees the window outlives the surface - /// for the duration of the runtime. - pub fn build<'window>( - self, - instance: &Instance, - window: &'window WindowHandle, - ) -> Result, wgpu::CreateSurfaceError> { - // SAFETY: We ensure the raw window/display handles outlive the surface by - // keeping the window alive for the duration of the application runtime. - // Obtain raw handles via raw-window-handle 0.6 traits. - let raw_display_handle = window - .window_handle - .display_handle() - .expect("Failed to get display handle from window") - .as_raw(); - let raw_window_handle = window - .window_handle - .window_handle() - .expect("Failed to get window handle from window") - .as_raw(); - - let surface = unsafe { - instance.raw().create_surface_unsafe( - wgpu::SurfaceTargetUnsafe::RawHandle { - raw_display_handle, - raw_window_handle, - }, - )? - }; - - Ok(Surface { - label: self.label.unwrap_or_else(|| "Lambda Surface".to_string()), - surface, - configuration: None, - format: None, - }) - } -} +// ---------------------- Command Encoding Abstractions ----------------------- #[derive(Debug)] -/// Presentation surface wrapper with cached configuration and format. -pub struct Surface<'window> { - label: String, - surface: wgpu::Surface<'window>, - configuration: Option, - format: Option, -} - -impl<'window> Surface<'window> { - /// Immutable label used for debugging. - pub fn label(&self) -> &str { - &self.label - } - - /// Borrow the raw `wgpu::Surface`. - pub fn surface(&self) -> &wgpu::Surface<'window> { - &self.surface - } - - /// Current configuration, if the surface has been configured. - pub fn configuration(&self) -> Option<&wgpu::SurfaceConfiguration> { - self.configuration.as_ref() - } - - /// Preferred surface format if known (set during configuration). - pub fn format(&self) -> Option { - self.format - } - - /// Configure the surface with the provided `wgpu::SurfaceConfiguration` and - /// cache the result for queries such as `format()`. - pub fn configure( - &mut self, - device: &wgpu::Device, - config: &wgpu::SurfaceConfiguration, - ) { - self.surface.configure(device, config); - self.configuration = Some(config.clone()); - self.format = Some(config.format); - } - - /// Configure the surface using common engine defaults: - /// - sRGB color format if available - /// - fallback present mode compatible with the platform - /// - `RENDER_ATTACHMENT` usage if requested usage is unsupported - pub fn configure_with_defaults( - &mut self, - adapter: &wgpu::Adapter, - device: &wgpu::Device, - size: (u32, u32), - present_mode: wgpu::PresentMode, - usage: wgpu::TextureUsages, - ) -> Result { - let width = size.0.max(1); - let height = size.1.max(1); - - let mut config = self - .surface - .get_default_config(adapter, width, height) - .ok_or_else(|| "Surface not supported by adapter".to_string())?; - - let capabilities = self.surface.get_capabilities(adapter); - - config.format = capabilities - .formats - .iter() - .copied() - .find(|format| format.is_srgb()) - .unwrap_or_else(|| *capabilities.formats.first().unwrap()); - - config.present_mode = if capabilities.present_modes.contains(&present_mode) - { - present_mode - } else { - capabilities - .present_modes - .iter() - .copied() - .find(|mode| { - matches!(mode, wgpu::PresentMode::Fifo | wgpu::PresentMode::AutoVsync) - }) - .unwrap_or(wgpu::PresentMode::Fifo) - }; - - if capabilities.usages.contains(usage) { - config.usage = usage; - } else { - config.usage = wgpu::TextureUsages::RENDER_ATTACHMENT; - } - - if config.view_formats.is_empty() && !config.format.is_srgb() { - config.view_formats.push(config.format.add_srgb_suffix()); - } - - self.configure(device, &config); - Ok(config) - } - - /// Resize the surface while preserving present mode and usage when possible. - pub fn resize( - &mut self, - adapter: &wgpu::Adapter, - device: &wgpu::Device, - size: (u32, u32), - ) -> Result<(), String> { - let present_mode = self - .configuration - .as_ref() - .map(|config| config.present_mode) - .unwrap_or(wgpu::PresentMode::Fifo); - let usage = self - .configuration - .as_ref() - .map(|config| config.usage) - .unwrap_or(wgpu::TextureUsages::RENDER_ATTACHMENT); - - self - .configure_with_defaults(adapter, device, size, present_mode, usage) - .map(|_| ()) - } - - /// Acquire the next swapchain texture and a default view. - pub fn acquire_next_frame(&self) -> Result { - let texture = self.surface.get_current_texture()?; - let view = texture - .texture - .create_view(&wgpu::TextureViewDescriptor::default()); - - Ok(Frame { texture, view }) - } +/// Thin wrapper around `wgpu::CommandEncoder` with convenience helpers. +pub struct CommandEncoder { + raw: wgpu::CommandEncoder, } #[derive(Debug)] -/// A single acquired frame and its default `TextureView`. -pub struct Frame { - texture: wgpu::SurfaceTexture, - view: wgpu::TextureView, +/// Wrapper around `wgpu::CommandBuffer` to avoid exposing raw types upstream. +pub struct CommandBuffer { + raw: wgpu::CommandBuffer, } -impl Frame { - /// Borrow the default view for rendering. - pub fn texture_view(&self) -> &wgpu::TextureView { - &self.view - } - - /// Consume and return the underlying parts. - pub fn into_parts(self) -> (wgpu::SurfaceTexture, wgpu::TextureView) { - (self.texture, self.view) - } - - /// Present the frame to the swapchain. - pub fn present(self) { - self.texture.present(); +impl CommandBuffer { + pub(crate) fn into_raw(self) -> wgpu::CommandBuffer { + self.raw } } -// ---------------------- Command Encoding Abstractions ----------------------- - -#[derive(Debug)] -/// Thin wrapper around `wgpu::CommandEncoder` with convenience helpers. -pub struct CommandEncoder { - raw: wgpu::CommandEncoder, -} - impl CommandEncoder { /// Create a new command encoder with an optional label. - pub fn new(device: &wgpu::Device, label: Option<&str>) -> Self { - let raw = - device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label }); + pub fn new(gpu: &gpu::Gpu, label: Option<&str>) -> Self { + let raw = gpu + .device() + .create_command_encoder(&wgpu::CommandEncoderDescriptor { label }); return Self { raw }; } @@ -365,11 +173,11 @@ impl CommandEncoder { pub fn begin_render_pass<'view>( &'view mut self, label: Option<&str>, - view: &'view wgpu::TextureView, + view: &'view surface::TextureViewRef<'view>, ops: wgpu::Operations, ) -> RenderPass<'view> { let color_attachment = wgpu::RenderPassColorAttachment { - view, + view: view.raw, resolve_target: None, depth_slice: None, ops, @@ -385,9 +193,32 @@ impl CommandEncoder { return RenderPass { raw: pass }; } + /// Begin a render pass that clears the color attachment to the provided RGBA color. + /// + /// This helper avoids exposing raw `wgpu` color/operation types to higher layers. + pub fn begin_render_pass_clear<'view>( + &'view mut self, + label: Option<&str>, + view: &'view surface::TextureViewRef<'view>, + color: [f64; 4], + ) -> RenderPass<'view> { + let ops = wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color { + r: color[0], + g: color[1], + b: color[2], + a: color[3], + }), + store: wgpu::StoreOp::Store, + }; + return self.begin_render_pass(label, view, ops); + } + /// Finish recording and return the command buffer. - pub fn finish(self) -> wgpu::CommandBuffer { - return self.raw.finish(); + pub fn finish(self) -> CommandBuffer { + return CommandBuffer { + raw: self.raw.finish(), + }; } } @@ -400,8 +231,8 @@ pub struct RenderPass<'a> { impl<'a> RenderPass<'a> { /// Set the active render pipeline. - pub fn set_pipeline(&mut self, pipeline: &wgpu::RenderPipeline) { - self.raw.set_pipeline(pipeline); + pub fn set_pipeline(&mut self, pipeline: &pipeline::RenderPipeline) { + self.raw.set_pipeline(pipeline.raw()); } /// Apply viewport state. @@ -428,25 +259,25 @@ impl<'a> RenderPass<'a> { pub fn set_bind_group( &mut self, set: u32, - group: &wgpu::BindGroup, + group: &bind::BindGroup, dynamic_offsets: &[u32], ) { - self.raw.set_bind_group(set, group, dynamic_offsets); + self.raw.set_bind_group(set, group.raw(), dynamic_offsets); } /// Bind a vertex buffer slot. - pub fn set_vertex_buffer(&mut self, slot: u32, buffer: &wgpu::Buffer) { - self.raw.set_vertex_buffer(slot, buffer.slice(..)); + pub fn set_vertex_buffer(&mut self, slot: u32, buffer: &buffer::Buffer) { + self.raw.set_vertex_buffer(slot, buffer.raw().slice(..)); } /// Upload push constants. pub fn set_push_constants( &mut self, - stages: wgpu::ShaderStages, + stages: pipeline::PipelineStage, offset: u32, data: &[u8], ) { - self.raw.set_push_constants(stages, offset, data); + self.raw.set_push_constants(stages.to_wgpu(), offset, data); } /// Issue a non-indexed draw over a vertex range. @@ -455,164 +286,6 @@ impl<'a> RenderPass<'a> { } } -#[derive(Debug, Clone)] -/// Builder for a `Gpu` (adapter, device, queue) with feature validation. -pub struct GpuBuilder { - label: Option, - power_preference: wgpu::PowerPreference, - force_fallback_adapter: bool, - required_features: wgpu::Features, - memory_hints: wgpu::MemoryHints, -} - -impl GpuBuilder { - /// Create a builder with defaults favoring performance and push constants. - pub fn new() -> Self { - Self { - label: Some("Lambda GPU".to_string()), - power_preference: wgpu::PowerPreference::HighPerformance, - force_fallback_adapter: false, - required_features: wgpu::Features::PUSH_CONSTANTS, - memory_hints: wgpu::MemoryHints::Performance, - } - } - - /// Attach a label used for the device. - pub fn with_label(mut self, label: &str) -> Self { - self.label = Some(label.to_string()); - self - } - - /// Select the adapter power preference (e.g., LowPower for laptops). - pub fn with_power_preference( - mut self, - preference: wgpu::PowerPreference, - ) -> Self { - self.power_preference = preference; - self - } - - /// Force using a fallback adapter when a primary device is unavailable. - pub fn force_fallback(mut self, force: bool) -> Self { - self.force_fallback_adapter = force; - self - } - - /// Require `wgpu::Features` to be present on the adapter. - pub fn with_required_features(mut self, features: wgpu::Features) -> Self { - self.required_features = features; - self - } - - /// Provide memory allocation hints for the device. - pub fn with_memory_hints(mut self, hints: wgpu::MemoryHints) -> Self { - self.memory_hints = hints; - self - } - - /// Request an adapter and device/queue pair and return a `Gpu` wrapper. - /// - /// Returns an error if no adapter is available, required features are - /// missing, or device creation fails. - pub fn build<'surface, 'window>( - self, - instance: &Instance, - surface: Option<&Surface<'surface>>, - ) -> Result { - let adapter = instance - .request_adapter(&wgpu::RequestAdapterOptions { - power_preference: self.power_preference, - force_fallback_adapter: self.force_fallback_adapter, - compatible_surface: surface.map(|surface| surface.surface()), - }) - .map_err(|_| GpuBuildError::AdapterUnavailable)?; - - let adapter_features = adapter.features(); - if !adapter_features.contains(self.required_features) { - return Err(GpuBuildError::MissingFeatures { - requested: self.required_features, - available: adapter_features, - }); - } - - let descriptor = wgpu::DeviceDescriptor { - label: self.label.as_deref(), - required_features: self.required_features, - required_limits: adapter.limits(), - memory_hints: self.memory_hints, - trace: wgpu::Trace::Off, - }; - - let (device, queue) = block_on(adapter.request_device(&descriptor))?; - - Ok(Gpu { - adapter, - device, - queue, - features: descriptor.required_features, - limits: descriptor.required_limits, - }) - } -} - -#[derive(Debug)] -/// Errors emitted while building a `Gpu`. -pub enum GpuBuildError { - /// No compatible adapter could be found. - AdapterUnavailable, - /// The requested features are not supported by the selected adapter. - MissingFeatures { - requested: wgpu::Features, - available: wgpu::Features, - }, - /// Wrapper for `wgpu::RequestDeviceError`. - RequestDevice(wgpu::RequestDeviceError), -} - -impl From for GpuBuildError { - fn from(error: wgpu::RequestDeviceError) -> Self { - GpuBuildError::RequestDevice(error) - } -} - -#[derive(Debug)] -/// Holds the chosen adapter along with its logical device and submission queue -/// plus immutable copies of features and limits used to create the device. -pub struct Gpu { - adapter: wgpu::Adapter, - device: wgpu::Device, - queue: wgpu::Queue, - features: wgpu::Features, - limits: wgpu::Limits, -} - -impl Gpu { - /// Borrow the adapter used to create the device. - pub fn adapter(&self) -> &wgpu::Adapter { - &self.adapter - } - - /// Borrow the logical device for resource creation. - pub fn device(&self) -> &wgpu::Device { - &self.device - } - - /// Borrow the submission queue for command submission. - pub fn queue(&self) -> &wgpu::Queue { - &self.queue - } - - /// Features that were required and enabled during device creation. - pub fn features(&self) -> wgpu::Features { - self.features - } - - /// Limits captured at device creation time. - pub fn limits(&self) -> &wgpu::Limits { - &self.limits - } -} - #[cfg(test)] mod tests { use super::*; @@ -622,14 +295,4 @@ mod tests { let instance = InstanceBuilder::new().with_label("Test").build(); assert_eq!(instance.label(), Some("Test")); } - - #[test] - fn gpu_build_error_wraps_request_device_error() { - // RequestDeviceError is opaque in wgpu 26 (no public constructors or variants). - // This test previously validated pattern matching on a specific variant; now we - // simply assert the From implementation exists by - // checking the trait bound at compile time. - fn assert_from_impl>() {} - assert_from_impl::(); - } } diff --git a/crates/lambda-rs-platform/src/wgpu/pipeline.rs b/crates/lambda-rs-platform/src/wgpu/pipeline.rs index b0285361..00be9c3f 100644 --- a/crates/lambda-rs-platform/src/wgpu/pipeline.rs +++ b/crates/lambda-rs-platform/src/wgpu/pipeline.rs @@ -1,6 +1,84 @@ //! Pipeline and shader module wrappers/builders for the platform layer. -use crate::wgpu::types as wgpu; +use std::ops::Range; + +use wgpu; + +use crate::wgpu::{ + bind, + surface::SurfaceFormat, + vertex::ColorFormat, + Gpu, +}; + +#[derive(Clone, Copy, Debug)] +/// Shader stage flags for push constants and visibility. +/// +/// This wrapper avoids exposing `wgpu` directly to higher layers while still +/// allowing flexible combinations when needed. +pub struct PipelineStage(wgpu::ShaderStages); + +impl PipelineStage { + /// Vertex stage only. + pub const VERTEX: PipelineStage = PipelineStage(wgpu::ShaderStages::VERTEX); + /// Fragment stage only. + pub const FRAGMENT: PipelineStage = + PipelineStage(wgpu::ShaderStages::FRAGMENT); + /// Compute stage only. + pub const COMPUTE: PipelineStage = PipelineStage(wgpu::ShaderStages::COMPUTE); + + /// Internal mapping to the underlying graphics API. + pub fn to_wgpu(self) -> wgpu::ShaderStages { + return self.0; + } +} + +impl std::ops::BitOr for PipelineStage { + type Output = PipelineStage; + + fn bitor(self, rhs: PipelineStage) -> PipelineStage { + return PipelineStage(self.0 | rhs.0); + } +} + +impl std::ops::BitOrAssign for PipelineStage { + fn bitor_assign(&mut self, rhs: PipelineStage) { + self.0 |= rhs.0; + } +} + +#[derive(Clone, Debug)] +/// Push constant declaration for a stage and byte range. +pub struct PushConstantRange { + pub stages: PipelineStage, + pub range: Range, +} + +#[derive(Clone, Copy, Debug)] +/// Face culling mode for graphics pipelines. +pub enum CullingMode { + None, + Front, + Back, +} + +impl CullingMode { + fn to_wgpu(self) -> Option { + return match self { + CullingMode::None => None, + CullingMode::Front => Some(wgpu::Face::Front), + CullingMode::Back => Some(wgpu::Face::Back), + }; + } +} + +#[derive(Clone, Copy, Debug)] +/// Description of a single vertex attribute used by a pipeline. +pub struct VertexAttributeDesc { + pub shader_location: u32, + pub offset: u64, + pub format: ColorFormat, +} #[derive(Debug)] /// Wrapper around `wgpu::ShaderModule` that preserves a label. @@ -11,15 +89,13 @@ pub struct ShaderModule { impl ShaderModule { /// Create a shader module from SPIR-V words. - pub fn from_spirv( - device: &wgpu::Device, - words: &[u32], - label: Option<&str>, - ) -> Self { - let raw = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label, - source: wgpu::ShaderSource::SpirV(std::borrow::Cow::Borrowed(words)), - }); + pub fn from_spirv(gpu: &Gpu, words: &[u32], label: Option<&str>) -> Self { + let raw = gpu + .device() + .create_shader_module(wgpu::ShaderModuleDescriptor { + label, + source: wgpu::ShaderSource::SpirV(std::borrow::Cow::Borrowed(words)), + }); return Self { raw, label: label.map(|s| s.to_string()), @@ -42,15 +118,15 @@ pub struct PipelineLayout { impl PipelineLayout { /// Borrow the raw pipeline layout. pub fn raw(&self) -> &wgpu::PipelineLayout { - &self.raw + return &self.raw; } } /// Builder for creating a `PipelineLayout`. pub struct PipelineLayoutBuilder<'a> { label: Option, - layouts: Vec<&'a wgpu::BindGroupLayout>, - push_constant_ranges: Vec, + layouts: Vec<&'a bind::BindGroupLayout>, + push_constant_ranges: Vec, } impl<'a> PipelineLayoutBuilder<'a> { @@ -66,31 +142,42 @@ impl<'a> PipelineLayoutBuilder<'a> { /// Attach a label. pub fn with_label(mut self, label: &str) -> Self { self.label = Some(label.to_string()); - self + return self; } /// Provide bind group layouts. - pub fn with_layouts(mut self, layouts: &'a [&wgpu::BindGroupLayout]) -> Self { + pub fn with_layouts(mut self, layouts: &'a [&bind::BindGroupLayout]) -> Self { self.layouts = layouts.to_vec(); - self + return self; } /// Provide push constant ranges. - pub fn with_push_constants( - mut self, - ranges: Vec, - ) -> Self { + pub fn with_push_constants(mut self, ranges: Vec) -> Self { self.push_constant_ranges = ranges; - self + return self; } /// Build the layout. - pub fn build(self, device: &wgpu::Device) -> PipelineLayout { - let raw = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: self.label.as_deref(), - bind_group_layouts: &self.layouts, - push_constant_ranges: &self.push_constant_ranges, - }); + pub fn build(self, gpu: &Gpu) -> PipelineLayout { + let layouts_raw: Vec<&wgpu::BindGroupLayout> = + self.layouts.iter().map(|l| l.raw()).collect(); + let push_constants_raw: Vec = self + .push_constant_ranges + .iter() + .map(|pcr| wgpu::PushConstantRange { + stages: pcr.stages.to_wgpu(), + range: pcr.range.clone(), + }) + .collect(); + + let raw = + gpu + .device() + .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: self.label.as_deref(), + bind_group_layouts: &layouts_raw, + push_constant_ranges: &push_constants_raw, + }); return PipelineLayout { raw, label: self.label, @@ -107,12 +194,12 @@ pub struct RenderPipeline { impl RenderPipeline { /// Borrow the raw pipeline. - pub fn raw(&self) -> &wgpu::RenderPipeline { - &self.raw + pub(crate) fn raw(&self) -> &wgpu::RenderPipeline { + return &self.raw; } /// Consume and return the raw pipeline. pub fn into_raw(self) -> wgpu::RenderPipeline { - self.raw + return self.raw; } } @@ -120,9 +207,9 @@ impl RenderPipeline { pub struct RenderPipelineBuilder<'a> { label: Option, layout: Option<&'a wgpu::PipelineLayout>, - vertex_buffers: Vec<(u64, Vec)>, - cull_mode: Option, - color_target: Option, + vertex_buffers: Vec<(u64, Vec)>, + cull_mode: CullingMode, + color_target_format: Option, } impl<'a> RenderPipelineBuilder<'a> { @@ -132,57 +219,68 @@ impl<'a> RenderPipelineBuilder<'a> { label: None, layout: None, vertex_buffers: Vec::new(), - cull_mode: Some(wgpu::Face::Back), - color_target: None, + cull_mode: CullingMode::Back, + color_target_format: None, }; } /// Attach a label. pub fn with_label(mut self, label: &str) -> Self { self.label = Some(label.to_string()); - self + return self; } /// Use the provided pipeline layout. pub fn with_layout(mut self, layout: &'a PipelineLayout) -> Self { self.layout = Some(layout.raw()); - self + return self; } /// Add a vertex buffer layout with attributes. pub fn with_vertex_buffer( mut self, array_stride: u64, - attributes: Vec, + attributes: Vec, ) -> Self { self.vertex_buffers.push((array_stride, attributes)); - self + return self; } /// Set cull mode (None disables culling). - pub fn with_cull_mode(mut self, face: Option) -> Self { - self.cull_mode = face; - self + pub fn with_cull_mode(mut self, mode: CullingMode) -> Self { + self.cull_mode = mode; + return self; } - /// Set single color target for fragment stage. - pub fn with_color_target(mut self, target: wgpu::ColorTargetState) -> Self { - self.color_target = Some(target); - self + /// Set single color target for fragment stage from a surface format. + pub fn with_surface_color_target(mut self, format: SurfaceFormat) -> Self { + self.color_target_format = Some(format.to_wgpu()); + return self; } /// Build the render pipeline from provided shader modules. pub fn build( self, - device: &wgpu::Device, + gpu: &Gpu, vertex_shader: &ShaderModule, fragment_shader: Option<&ShaderModule>, ) -> RenderPipeline { + // Convert vertex attributes into raw `wgpu` descriptors while keeping + // storage stable for layout lifetimes. let mut attr_storage: Vec> = Vec::new(); let mut strides: Vec = Vec::new(); for (stride, attrs) in &self.vertex_buffers { - let boxed: Box<[wgpu::VertexAttribute]> = - attrs.clone().into_boxed_slice(); + let mut raw_attrs: Vec = + Vec::with_capacity(attrs.len()); + + for attribute in attrs.iter() { + raw_attrs.push(wgpu::VertexAttribute { + shader_location: attribute.shader_location, + offset: attribute.offset, + format: attribute.format.to_vertex_format(), + }); + } + let boxed: Box<[wgpu::VertexAttribute]> = raw_attrs.into_boxed_slice(); attr_storage.push(boxed); strides.push(*stride); } @@ -198,8 +296,12 @@ impl<'a> RenderPipelineBuilder<'a> { } let color_targets: Vec> = - match &self.color_target { - Some(ct) => vec![Some(ct.clone())], + match &self.color_target_format { + Some(fmt) => vec![Some(wgpu::ColorTargetState { + format: *fmt, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], None => Vec::new(), }; @@ -218,22 +320,25 @@ impl<'a> RenderPipelineBuilder<'a> { }; let primitive_state = wgpu::PrimitiveState { - cull_mode: self.cull_mode, + cull_mode: self.cull_mode.to_wgpu(), ..wgpu::PrimitiveState::default() }; let layout_ref = self.layout; - let raw = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: self.label.as_deref(), - layout: layout_ref, - vertex: vertex_state, - primitive: primitive_state, - depth_stencil: None, - multisample: wgpu::MultisampleState::default(), - fragment, - multiview: None, - cache: None, - }); + let raw = + gpu + .device() + .create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: self.label.as_deref(), + layout: layout_ref, + vertex: vertex_state, + primitive: primitive_state, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + fragment, + multiview: None, + cache: None, + }); return RenderPipeline { raw, diff --git a/crates/lambda-rs-platform/src/wgpu/surface.rs b/crates/lambda-rs-platform/src/wgpu/surface.rs new file mode 100644 index 00000000..cfa96368 --- /dev/null +++ b/crates/lambda-rs-platform/src/wgpu/surface.rs @@ -0,0 +1,418 @@ +use wgpu::rwh::{ + HasDisplayHandle as _, + HasWindowHandle as _, +}; + +use super::{ + gpu::Gpu, + Instance, +}; +use crate::winit::WindowHandle; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +/// Present modes supported by the surface. +/// +/// This wrapper hides the underlying `wgpu` type from higher layers while +/// preserving the same semantics. +pub enum PresentMode { + Fifo, + FifoRelaxed, + Immediate, + Mailbox, + AutoVsync, + AutoNoVsync, +} + +impl PresentMode { + pub(crate) fn to_wgpu(self) -> wgpu::PresentMode { + return match self { + PresentMode::Fifo => wgpu::PresentMode::Fifo, + PresentMode::FifoRelaxed => wgpu::PresentMode::FifoRelaxed, + PresentMode::Immediate => wgpu::PresentMode::Immediate, + PresentMode::Mailbox => wgpu::PresentMode::Mailbox, + PresentMode::AutoVsync => wgpu::PresentMode::AutoVsync, + PresentMode::AutoNoVsync => wgpu::PresentMode::AutoNoVsync, + }; + } + + pub(crate) fn from_wgpu(mode: wgpu::PresentMode) -> Self { + return match mode { + wgpu::PresentMode::Fifo => PresentMode::Fifo, + wgpu::PresentMode::FifoRelaxed => PresentMode::FifoRelaxed, + wgpu::PresentMode::Immediate => PresentMode::Immediate, + wgpu::PresentMode::Mailbox => PresentMode::Mailbox, + wgpu::PresentMode::AutoVsync => PresentMode::AutoVsync, + wgpu::PresentMode::AutoNoVsync => PresentMode::AutoNoVsync, + _ => PresentMode::Fifo, + }; + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +/// Wrapper for texture usage flags used by surfaces. +pub struct TextureUsages(wgpu::TextureUsages); + +impl TextureUsages { + /// Render attachment usage. + pub const RENDER_ATTACHMENT: TextureUsages = + TextureUsages(wgpu::TextureUsages::RENDER_ATTACHMENT); + /// Texture binding usage. + pub const TEXTURE_BINDING: TextureUsages = + TextureUsages(wgpu::TextureUsages::TEXTURE_BINDING); + /// Copy destination usage. + pub const COPY_DST: TextureUsages = + TextureUsages(wgpu::TextureUsages::COPY_DST); + /// Copy source usage. + pub const COPY_SRC: TextureUsages = + TextureUsages(wgpu::TextureUsages::COPY_SRC); + + pub(crate) fn to_wgpu(self) -> wgpu::TextureUsages { + return self.0; + } + + pub(crate) fn from_wgpu(flags: wgpu::TextureUsages) -> Self { + return TextureUsages(flags); + } + + /// Check whether this flags set contains another set. + pub fn contains(self, other: TextureUsages) -> bool { + return self.0.contains(other.0); + } +} + +impl std::ops::BitOr for TextureUsages { + type Output = TextureUsages; + + fn bitor(self, rhs: TextureUsages) -> TextureUsages { + return TextureUsages(self.0 | rhs.0); + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +/// Wrapper around a surface color format. +pub struct SurfaceFormat(wgpu::TextureFormat); + +impl SurfaceFormat { + pub(crate) fn to_wgpu(self) -> wgpu::TextureFormat { + return self.0; + } + + pub(crate) fn from_wgpu(fmt: wgpu::TextureFormat) -> Self { + return SurfaceFormat(fmt); + } + + /// Whether this format is sRGB. + pub fn is_srgb(self) -> bool { + return self.0.is_srgb(); + } + + /// Return the sRGB variant of the format when applicable. + pub fn add_srgb_suffix(self) -> Self { + return SurfaceFormat(self.0.add_srgb_suffix()); + } +} + +#[derive(Clone, Debug)] +/// Public, engine-facing surface configuration that avoids exposing `wgpu`. +pub struct SurfaceConfig { + pub width: u32, + pub height: u32, + pub format: SurfaceFormat, + pub present_mode: PresentMode, + pub usage: TextureUsages, + pub view_formats: Vec, +} + +impl SurfaceConfig { + pub(crate) fn from_wgpu(config: &wgpu::SurfaceConfiguration) -> Self { + return SurfaceConfig { + width: config.width, + height: config.height, + format: SurfaceFormat::from_wgpu(config.format), + present_mode: PresentMode::from_wgpu(config.present_mode), + usage: TextureUsages::from_wgpu(config.usage), + view_formats: config + .view_formats + .iter() + .copied() + .map(SurfaceFormat::from_wgpu) + .collect(), + }; + } + + pub(crate) fn to_wgpu(&self) -> wgpu::SurfaceConfiguration { + let mut view_formats: Vec = Vec::new(); + for vf in &self.view_formats { + view_formats.push(vf.to_wgpu()); + } + return wgpu::SurfaceConfiguration { + usage: self.usage.to_wgpu(), + format: self.format.to_wgpu(), + width: self.width, + height: self.height, + present_mode: self.present_mode.to_wgpu(), + desired_maximum_frame_latency: 2, + alpha_mode: wgpu::CompositeAlphaMode::Opaque, + view_formats, + }; + } +} + +#[derive(Clone, Debug)] +/// Error wrapper for surface acquisition and presentation errors. +pub enum SurfaceError { + /// The surface has been lost and must be recreated. + Lost, + /// The surface configuration is outdated and must be reconfigured. + Outdated, + /// Out of memory. + OutOfMemory, + /// Timed out waiting for a frame. + Timeout, + /// Other/unclassified error (opaque). + Other(String), +} + +impl From for SurfaceError { + fn from(error: wgpu::SurfaceError) -> Self { + use wgpu::SurfaceError as We; + match error { + We::Lost => return SurfaceError::Lost, + We::Outdated => return SurfaceError::Outdated, + We::OutOfMemory => return SurfaceError::OutOfMemory, + We::Timeout => return SurfaceError::Timeout, + _ => return SurfaceError::Other(format!("{:?}", error)), + } + } +} + +#[derive(Debug, Clone)] +/// Builder for creating a `Surface` bound to a `winit` window. +pub struct SurfaceBuilder { + label: Option, +} + +impl SurfaceBuilder { + /// Create a builder with no label. + pub fn new() -> Self { + Self { label: None } + } + + /// Attach a human-readable label for debugging/profiling. + pub fn with_label(mut self, label: &str) -> Self { + self.label = Some(label.to_string()); + self + } + + /// Create a presentation surface for the provided `WindowHandle`. + /// + /// Safety: we use `create_surface_unsafe` by forwarding raw window/display + /// handles from `winit`. Lambda guarantees the window outlives the surface + /// for the duration of the runtime. + pub fn build<'window>( + self, + instance: &Instance, + window: &'window WindowHandle, + ) -> Result, CreateSurfaceError> { + // SAFETY: We ensure the raw window/display handles outlive the surface by + // keeping the window alive for the duration of the application runtime. + // Obtain raw handles via raw-window-handle 0.6 traits. + let raw_display_handle = window + .window_handle + .display_handle() + .expect("Failed to get display handle from window") + .as_raw(); + let raw_window_handle = window + .window_handle + .window_handle() + .expect("Failed to get window handle from window") + .as_raw(); + + let surface = unsafe { + instance + .raw() + .create_surface_unsafe(wgpu::SurfaceTargetUnsafe::RawHandle { + raw_display_handle, + raw_window_handle, + }) + .map_err(CreateSurfaceError::from)? + }; + + Ok(Surface { + label: self.label.unwrap_or_else(|| "Lambda Surface".to_string()), + surface, + configuration: None, + format: None, + }) + } +} + +#[derive(Debug)] +/// Opaque error returned when surface creation fails. +pub struct CreateSurfaceError; + +impl From for CreateSurfaceError { + fn from(_: wgpu::CreateSurfaceError) -> Self { + return CreateSurfaceError; + } +} + +#[derive(Debug)] +/// Presentation surface wrapper with cached configuration and format. +pub struct Surface<'window> { + label: String, + surface: wgpu::Surface<'window>, + configuration: Option, + format: Option, +} + +impl<'window> Surface<'window> { + /// Immutable label used for debugging. + pub fn label(&self) -> &str { + &self.label + } + + /// Borrow the raw `wgpu::Surface` (crate visibility only). + pub(crate) fn surface(&self) -> &wgpu::Surface<'window> { + &self.surface + } + + /// Current configuration, if the surface has been configured. + pub fn configuration(&self) -> Option<&SurfaceConfig> { + return self.configuration.as_ref(); + } + + /// Preferred surface format if known (set during configuration). + pub fn format(&self) -> Option { + return self.format; + } + + /// Configure the surface and cache the result for queries such as `format()`. + pub(crate) fn configure_raw( + &mut self, + device: &wgpu::Device, + config: &wgpu::SurfaceConfiguration, + ) { + self.surface.configure(device, config); + self.configuration = Some(SurfaceConfig::from_wgpu(config)); + self.format = Some(SurfaceFormat::from_wgpu(config.format)); + } + + /// Configure the surface using common engine defaults: + /// - sRGB color format if available + /// - fallback present mode compatible with the platform + /// - `RENDER_ATTACHMENT` usage if requested usage is unsupported + pub fn configure_with_defaults( + &mut self, + gpu: &Gpu, + size: (u32, u32), + present_mode: PresentMode, + usage: TextureUsages, + ) -> Result<(), String> { + let width = size.0.max(1); + let height = size.1.max(1); + + let mut config = self + .surface + .get_default_config(gpu.adapter(), width, height) + .ok_or_else(|| "Surface not supported by adapter".to_string())?; + + let capabilities = self.surface.get_capabilities(gpu.adapter()); + + config.format = capabilities + .formats + .iter() + .copied() + .find(|format| format.is_srgb()) + .unwrap_or_else(|| *capabilities.formats.first().unwrap()); + + let requested_present_mode = present_mode.to_wgpu(); + config.present_mode = if capabilities + .present_modes + .contains(&requested_present_mode) + { + requested_present_mode + } else { + capabilities + .present_modes + .iter() + .copied() + .find(|mode| { + matches!(mode, wgpu::PresentMode::Fifo | wgpu::PresentMode::AutoVsync) + }) + .unwrap_or(wgpu::PresentMode::Fifo) + }; + + if capabilities.usages.contains(usage.to_wgpu()) { + config.usage = usage.to_wgpu(); + } else { + config.usage = wgpu::TextureUsages::RENDER_ATTACHMENT; + } + + if config.view_formats.is_empty() && !config.format.is_srgb() { + config.view_formats.push(config.format.add_srgb_suffix()); + } + + self.configure_raw(gpu.device(), &config); + Ok(()) + } + + /// Resize the surface while preserving present mode and usage when possible. + pub fn resize(&mut self, gpu: &Gpu, size: (u32, u32)) -> Result<(), String> { + let present_mode = self + .configuration + .as_ref() + .map(|config| config.present_mode) + .unwrap_or(PresentMode::Fifo); + let usage = self + .configuration + .as_ref() + .map(|config| config.usage) + .unwrap_or(TextureUsages::RENDER_ATTACHMENT); + + return self.configure_with_defaults(gpu, size, present_mode, usage); + } + + /// Acquire the next swapchain texture and a default view. + pub fn acquire_next_frame(&self) -> Result { + let texture = match self.surface.get_current_texture() { + Ok(t) => t, + Err(e) => return Err(SurfaceError::from(e)), + }; + let view = texture + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + + return Ok(Frame { texture, view }); + } +} + +#[derive(Debug)] +/// A single acquired frame and its default `TextureView`. +pub struct Frame { + texture: wgpu::SurfaceTexture, + view: wgpu::TextureView, +} + +#[derive(Clone, Copy)] +/// Borrowed reference to a texture view used for render passes. +pub struct TextureViewRef<'a> { + pub(crate) raw: &'a wgpu::TextureView, +} + +impl Frame { + /// Borrow the default view for rendering. + pub fn texture_view(&self) -> TextureViewRef<'_> { + return TextureViewRef { raw: &self.view }; + } + + /// Consume and return the underlying parts. + pub(crate) fn into_parts(self) -> (wgpu::SurfaceTexture, wgpu::TextureView) { + return (self.texture, self.view); + } + + /// Present the frame to the swapchain. + pub fn present(self) { + self.texture.present(); + } +} diff --git a/crates/lambda-rs-platform/src/wgpu/vertex.rs b/crates/lambda-rs-platform/src/wgpu/vertex.rs new file mode 100644 index 00000000..560e2df7 --- /dev/null +++ b/crates/lambda-rs-platform/src/wgpu/vertex.rs @@ -0,0 +1,22 @@ +/// Canonical color/attribute formats used by engine pipelines. +#[derive(Clone, Copy, Debug)] +pub enum ColorFormat { + Rgb32Sfloat, + Rgba8Srgb, +} + +impl ColorFormat { + pub fn to_texture_format(self) -> wgpu::TextureFormat { + return match self { + ColorFormat::Rgb32Sfloat => wgpu::TextureFormat::Rgba32Float, + ColorFormat::Rgba8Srgb => wgpu::TextureFormat::Rgba8UnormSrgb, + }; + } + + pub fn to_vertex_format(self) -> wgpu::VertexFormat { + return match self { + ColorFormat::Rgb32Sfloat => wgpu::VertexFormat::Float32x3, + ColorFormat::Rgba8Srgb => wgpu::VertexFormat::Unorm8x4, + }; + } +} diff --git a/crates/lambda-rs/examples/push_constants.rs b/crates/lambda-rs/examples/push_constants.rs index e58bdce0..dda5eeb0 100644 --- a/crates/lambda-rs/examples/push_constants.rs +++ b/crates/lambda-rs/examples/push_constants.rs @@ -32,12 +32,12 @@ use lambda::{ VirtualShader, }, vertex::{ + ColorFormat, VertexAttribute, VertexBuilder, VertexElement, }, viewport, - ColorFormat, ResourceId, }, runtime::start_runtime, diff --git a/crates/lambda-rs/examples/uniform_buffer_triangle.rs b/crates/lambda-rs/examples/uniform_buffer_triangle.rs index ab8bb491..cf99917f 100644 --- a/crates/lambda-rs/examples/uniform_buffer_triangle.rs +++ b/crates/lambda-rs/examples/uniform_buffer_triangle.rs @@ -41,12 +41,12 @@ use lambda::{ VirtualShader, }, vertex::{ + ColorFormat, VertexAttribute, VertexBuilder, VertexElement, }, viewport, - ColorFormat, ResourceId, }, runtime::start_runtime, diff --git a/crates/lambda-rs/src/render/bind.rs b/crates/lambda-rs/src/render/bind.rs index a2aff134..011a764a 100644 --- a/crates/lambda-rs/src/render/bind.rs +++ b/crates/lambda-rs/src/render/bind.rs @@ -6,15 +6,8 @@ use std::rc::Rc; -use lambda_platform::wgpu::types as wgpu; - -use super::{ - buffer::Buffer, - RenderContext, -}; - -#[derive(Debug)] -/// Visibility of a binding across shader stages. +#[derive(Clone, Copy, Debug)] +/// Visibility of a binding across shader stages (engine facing). pub enum BindingVisibility { Vertex, Fragment, @@ -25,45 +18,32 @@ pub enum BindingVisibility { impl BindingVisibility { fn to_platform(self) -> lambda_platform::wgpu::bind::Visibility { - use lambda_platform::wgpu::bind::Visibility as V; - return match self { - BindingVisibility::Vertex => V::Vertex, - BindingVisibility::Fragment => V::Fragment, - BindingVisibility::Compute => V::Compute, - BindingVisibility::VertexAndFragment => V::VertexAndFragment, - BindingVisibility::All => V::All, - }; + match self { + BindingVisibility::Vertex => { + lambda_platform::wgpu::bind::Visibility::Vertex + } + BindingVisibility::Fragment => { + lambda_platform::wgpu::bind::Visibility::Fragment + } + BindingVisibility::Compute => { + lambda_platform::wgpu::bind::Visibility::Compute + } + BindingVisibility::VertexAndFragment => { + lambda_platform::wgpu::bind::Visibility::VertexAndFragment + } + BindingVisibility::All => lambda_platform::wgpu::bind::Visibility::All, + } } } +use super::{ + buffer::Buffer, + RenderContext, +}; + #[cfg(test)] mod tests { use super::*; - - /// This test confirms that every high‑level binding visibility option maps - /// directly to the corresponding visibility option in the platform layer. - /// Matching these values ensures that builder code in this module forwards - /// intent without alteration, which is important for readability and for - /// maintenance when constructing layouts and groups. - #[test] - fn binding_visibility_maps_to_platform_enum() { - use lambda_platform::wgpu::bind::Visibility as P; - - assert!(matches!(BindingVisibility::Vertex.to_platform(), P::Vertex)); - assert!(matches!( - BindingVisibility::Fragment.to_platform(), - P::Fragment - )); - assert!(matches!( - BindingVisibility::Compute.to_platform(), - P::Compute - )); - assert!(matches!( - BindingVisibility::VertexAndFragment.to_platform(), - P::VertexAndFragment - )); - assert!(matches!(BindingVisibility::All.to_platform(), P::All)); - } } #[derive(Debug, Clone)] @@ -75,8 +55,11 @@ pub struct BindGroupLayout { } impl BindGroupLayout { - pub(crate) fn raw(&self) -> &wgpu::BindGroupLayout { - return self.layout.raw(); + /// Borrow the underlying platform bind group layout wrapper. + pub(crate) fn platform_layout( + &self, + ) -> &lambda_platform::wgpu::bind::BindGroupLayout { + return &self.layout; } /// Number of dynamic bindings declared in this layout. @@ -94,8 +77,10 @@ pub struct BindGroup { } impl BindGroup { - pub(crate) fn raw(&self) -> &wgpu::BindGroup { - return self.group.raw(); + pub(crate) fn platform_group( + &self, + ) -> &lambda_platform::wgpu::bind::BindGroup { + return &self.group; } /// Number of dynamic bindings expected when calling set_bind_group. @@ -180,7 +165,7 @@ impl BindGroupLayoutBuilder { }; } - let layout = builder.build(render_context.device()); + let layout = builder.build(render_context.gpu()); return BindGroupLayout { layout: Rc::new(layout), @@ -258,7 +243,7 @@ impl<'a> BindGroupBuilder<'a> { platform = platform.with_uniform(binding, buffer.raw(), offset, size); } - let group = platform.build(render_context.device()); + let group = platform.build(render_context.gpu()); return BindGroup { group: Rc::new(group), dynamic_binding_count: layout.dynamic_binding_count(), diff --git a/crates/lambda-rs/src/render/buffer.rs b/crates/lambda-rs/src/render/buffer.rs index f45d162e..aa28c71f 100644 --- a/crates/lambda-rs/src/render/buffer.rs +++ b/crates/lambda-rs/src/render/buffer.rs @@ -2,10 +2,7 @@ use std::rc::Rc; -use lambda_platform::wgpu::{ - buffer as platform_buffer, - types as wgpu, -}; +use lambda_platform::wgpu::buffer as platform_buffer; use super::{ mesh::Mesh, @@ -98,8 +95,8 @@ impl Buffer { /// created it. Dropping the buffer will release GPU resources. pub fn destroy(self, _render_context: &RenderContext) {} - pub(super) fn raw(&self) -> &wgpu::Buffer { - return self.buffer.raw(); + pub(super) fn raw(&self) -> &platform_buffer::Buffer { + return self.buffer.as_ref(); } pub(super) fn stride(&self) -> u64 { @@ -126,9 +123,8 @@ impl Buffer { std::mem::size_of::(), ) }; - render_context - .queue() - .write_buffer(self.raw(), offset, bytes); + + self.buffer.write_bytes(render_context.gpu(), offset, bytes); } } @@ -149,13 +145,15 @@ impl UniformBuffer { initial: &T, label: Option<&str>, ) -> Result { - let mut builder = BufferBuilder::new(); - builder.with_length(core::mem::size_of::()); - builder.with_usage(Usage::UNIFORM); - builder.with_properties(Properties::CPU_VISIBLE); + let mut builder = BufferBuilder::new() + .with_length(core::mem::size_of::()) + .with_usage(Usage::UNIFORM) + .with_properties(Properties::CPU_VISIBLE); + if let Some(l) = label { - builder.with_label(l); + builder = builder.with_label(l); } + let inner = builder.build(render_context, vec![*initial])?; return Ok(Self { inner, @@ -201,31 +199,31 @@ impl BufferBuilder { } /// Set the length of the buffer in bytes. Defaults to the size of `data`. - pub fn with_length(&mut self, size: usize) -> &mut Self { + pub fn with_length(mut self, size: usize) -> Self { self.buffer_length = size; return self; } /// Set the logical type of buffer to be created (vertex/index/...). - pub fn with_buffer_type(&mut self, buffer_type: BufferType) -> &mut Self { + pub fn with_buffer_type(mut self, buffer_type: BufferType) -> Self { self.buffer_type = buffer_type; return self; } /// Set `wgpu` usage flags (bit‑or `Usage` values). - pub fn with_usage(&mut self, usage: Usage) -> &mut Self { + pub fn with_usage(mut self, usage: Usage) -> Self { self.usage = usage; return self; } /// Control CPU visibility and residency preferences. - pub fn with_properties(&mut self, properties: Properties) -> &mut Self { + pub fn with_properties(mut self, properties: Properties) -> Self { self.properties = properties; return self; } /// Attach a human‑readable label for debugging/profiling. - pub fn with_label(&mut self, label: &str) -> &mut Self { + pub fn with_label(mut self, label: &str) -> Self { self.label = Some(label.to_string()); return self; } @@ -259,7 +257,7 @@ impl BufferBuilder { builder = builder.with_label(label); } - let buffer = builder.build_init(render_context.device(), bytes); + let buffer = builder.build_init(render_context.gpu(), bytes); return Ok(Buffer { buffer: Rc::new(buffer), @@ -316,8 +314,7 @@ mod tests { #[test] fn label_is_recorded_on_builder() { - let mut builder = BufferBuilder::new(); - builder.with_label("buffer-test"); + let builder = BufferBuilder::new().with_label("buffer-test"); // Indirect check: validate the internal label is stored on the builder. // Test module is a child of this module and can access private fields. assert_eq!(builder.label.as_deref(), Some("buffer-test")); diff --git a/crates/lambda-rs/src/render/mesh.rs b/crates/lambda-rs/src/render/mesh.rs index d549fb1c..1f438399 100644 --- a/crates/lambda-rs/src/render/mesh.rs +++ b/crates/lambda-rs/src/render/mesh.rs @@ -2,13 +2,11 @@ use lambda_platform::obj::load_textured_obj_from_file; -use super::{ - vertex::{ - Vertex, - VertexAttribute, - VertexElement, - }, +use super::vertex::{ ColorFormat, + Vertex, + VertexAttribute, + VertexElement, }; // ---------------------------------- Mesh ------------------------------------ diff --git a/crates/lambda-rs/src/render/mod.rs b/crates/lambda-rs/src/render/mod.rs index 29bf25df..e1dfe101 100644 --- a/crates/lambda-rs/src/render/mod.rs +++ b/crates/lambda-rs/src/render/mod.rs @@ -18,7 +18,6 @@ pub mod window; use std::iter; use lambda_platform::wgpu::{ - types as wgpu, CommandEncoder as PlatformCommandEncoder, Gpu, GpuBuilder, @@ -28,7 +27,6 @@ use lambda_platform::wgpu::{ SurfaceBuilder, }; use logging; -pub use vertex::ColorFormat; use self::{ command::RenderCommand, @@ -81,24 +79,30 @@ impl RenderContextBuilder { .expect("Failed to create GPU device"); let size = window.dimensions(); - let config = surface + surface .configure_with_defaults( - gpu.adapter(), - gpu.device(), + &gpu, size, - wgpu::PresentMode::Fifo, - wgpu::TextureUsages::RENDER_ATTACHMENT, + lambda_platform::wgpu::PresentMode::Fifo, + lambda_platform::wgpu::TextureUsages::RENDER_ATTACHMENT, ) .expect("Failed to configure surface"); + let config = surface + .configuration() + .cloned() + .expect("Surface was not configured"); + let present_mode = config.present_mode; + let texture_usage = config.usage; + return RenderContext { label: name, instance, surface, gpu, config, - present_mode: wgpu::PresentMode::Fifo, - texture_usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + present_mode, + texture_usage, size, render_passes: vec![], render_pipelines: vec![], @@ -118,9 +122,9 @@ pub struct RenderContext { instance: Instance, surface: Surface<'static>, gpu: Gpu, - config: wgpu::SurfaceConfiguration, - present_mode: wgpu::PresentMode, - texture_usage: wgpu::TextureUsages, + config: lambda_platform::wgpu::SurfaceConfig, + present_mode: lambda_platform::wgpu::PresentMode, + texture_usage: lambda_platform::wgpu::TextureUsages, size: (u32, u32), render_passes: Vec, render_pipelines: Vec, @@ -203,21 +207,17 @@ impl RenderContext { return &self.render_pipelines[id]; } - pub(crate) fn device(&self) -> &wgpu::Device { - return self.gpu.device(); + pub(crate) fn gpu(&self) -> &Gpu { + return &self.gpu; } - pub(crate) fn queue(&self) -> &wgpu::Queue { - return self.gpu.queue(); - } - - pub(crate) fn surface_format(&self) -> wgpu::TextureFormat { + pub(crate) fn surface_format(&self) -> lambda_platform::wgpu::SurfaceFormat { return self.config.format; } /// Device limit: maximum bytes that can be bound for a single uniform buffer binding. pub fn limit_max_uniform_buffer_binding_size(&self) -> u64 { - return self.gpu.limits().max_uniform_buffer_binding_size.into(); + return self.gpu.limits().max_uniform_buffer_binding_size; } /// Device limit: number of bind groups that can be used by a pipeline layout. @@ -241,7 +241,8 @@ impl RenderContext { let mut frame = match self.surface.acquire_next_frame() { Ok(frame) => frame, - Err(wgpu::SurfaceError::Lost) | Err(wgpu::SurfaceError::Outdated) => { + Err(lambda_platform::wgpu::SurfaceError::Lost) + | Err(lambda_platform::wgpu::SurfaceError::Outdated) => { self.reconfigure_surface(self.size)?; self .surface @@ -253,7 +254,7 @@ impl RenderContext { let view = frame.texture_view(); let mut encoder = PlatformCommandEncoder::new( - self.device(), + self.gpu(), Some("lambda-render-command-encoder"), ); @@ -270,8 +271,11 @@ impl RenderContext { )) })?; - let mut pass_encoder = - encoder.begin_render_pass(pass.label(), view, pass.color_ops()); + let mut pass_encoder = encoder.begin_render_pass_clear( + pass.label(), + &view, + pass.clear_color(), + ); self.encode_pass(&mut pass_encoder, viewport, &mut command_iter)?; } @@ -284,7 +288,7 @@ impl RenderContext { } } - self.queue().submit(iter::once(encoder.finish())); + self.gpu.submit(iter::once(encoder.finish())); frame.present(); return Ok(()); } @@ -342,7 +346,11 @@ impl RenderContext { set, ) .map_err(RenderError::Configuration)?; - pass.set_bind_group(set, group_ref.raw(), &dynamic_offsets); + pass.set_bind_group( + set, + group_ref.platform_group(), + &dynamic_offsets, + ); } RenderCommand::BindVertexBuffer { pipeline, buffer } => { let pipeline_ref = @@ -377,7 +385,7 @@ impl RenderContext { bytes.len() * std::mem::size_of::(), ) }; - pass.set_push_constants(stage.to_wgpu(), offset, slice); + pass.set_push_constants(stage, offset, slice); } RenderCommand::Draw { vertices } => { pass.draw(vertices); @@ -411,17 +419,20 @@ impl RenderContext { &mut self, size: (u32, u32), ) -> Result<(), RenderError> { - let config = self + self .surface .configure_with_defaults( - self.gpu.adapter(), - self.gpu.device(), + &self.gpu, size, self.present_mode, self.texture_usage, ) .map_err(RenderError::Configuration)?; + let config = self.surface.configuration().cloned().ok_or_else(|| { + RenderError::Configuration("Surface was not configured".to_string()) + })?; + self.present_mode = config.present_mode; self.texture_usage = config.usage; self.config = config; @@ -432,12 +443,12 @@ impl RenderContext { #[derive(Debug)] /// Errors that can occur while preparing or presenting a frame. pub enum RenderError { - Surface(wgpu::SurfaceError), + Surface(lambda_platform::wgpu::SurfaceError), Configuration(String), } -impl From for RenderError { - fn from(error: wgpu::SurfaceError) -> Self { +impl From for RenderError { + fn from(error: lambda_platform::wgpu::SurfaceError) -> Self { return RenderError::Surface(error); } } diff --git a/crates/lambda-rs/src/render/pipeline.rs b/crates/lambda-rs/src/render/pipeline.rs index facd8983..b015779e 100644 --- a/crates/lambda-rs/src/render/pipeline.rs +++ b/crates/lambda-rs/src/render/pipeline.rs @@ -6,10 +6,7 @@ use std::{ rc::Rc, }; -use lambda_platform::wgpu::{ - pipeline as platform_pipeline, - types as wgpu, -}; +use lambda_platform::wgpu::pipeline as platform_pipeline; use super::{ bind, @@ -23,7 +20,7 @@ use super::{ #[derive(Debug)] /// A created graphics pipeline and the vertex buffers it expects. pub struct RenderPipeline { - pipeline: Rc, + pipeline: Rc, buffers: Vec>, } @@ -36,45 +33,14 @@ impl RenderPipeline { return &self.buffers; } - /// Access the underlying wgpu render pipeline. - pub(super) fn pipeline(&self) -> &wgpu::RenderPipeline { + /// Access the underlying platform render pipeline. + pub(super) fn pipeline(&self) -> &platform_pipeline::RenderPipeline { return self.pipeline.as_ref(); } } -#[derive(Clone, Copy, Debug)] -/// Bitflag wrapper for shader stages used by push constants. -pub struct PipelineStage(wgpu::ShaderStages); - -impl PipelineStage { - /// Vertex stage. - pub const VERTEX: PipelineStage = PipelineStage(wgpu::ShaderStages::VERTEX); - /// Fragment stage. - pub const FRAGMENT: PipelineStage = - PipelineStage(wgpu::ShaderStages::FRAGMENT); - /// Compute stage. - pub const COMPUTE: PipelineStage = PipelineStage(wgpu::ShaderStages::COMPUTE); - - pub(crate) fn to_wgpu(self) -> wgpu::ShaderStages { - return self.0; - } -} - -/// Bitwise OR for combining pipeline stages. -impl std::ops::BitOr for PipelineStage { - type Output = PipelineStage; - - fn bitor(self, rhs: PipelineStage) -> PipelineStage { - return PipelineStage(self.0 | rhs.0); - } -} - -/// Bitwise OR assignment for combining pipeline stages. -impl std::ops::BitOrAssign for PipelineStage { - fn bitor_assign(&mut self, rhs: PipelineStage) { - self.0 |= rhs.0; - } -} +/// Public alias for platform shader stage flags used by push constants. +pub use platform_pipeline::PipelineStage; /// Convenience alias for uploading push constants: stage and byte range. pub type PushConstantUpload = (PipelineStage, Range); @@ -84,26 +50,8 @@ struct BufferBinding { attributes: Vec, } -#[derive(Clone, Copy, Debug)] -/// Controls triangle face culling for the graphics pipeline. -pub enum CullingMode { - /// Disable face culling; render both triangle faces. - None, - /// Cull triangles whose winding is counterclockwise after projection. - Front, - /// Cull triangles whose winding is clockwise after projection. - Back, -} - -impl CullingMode { - fn to_wgpu(self) -> Option { - return match self { - CullingMode::None => None, - CullingMode::Front => Some(wgpu::Face::Front), - CullingMode::Back => Some(wgpu::Face::Back), - }; - } -} +/// Public alias for platform culling mode used by pipeline builders. +pub use platform_pipeline::CullingMode; /// Builder for creating a graphics `RenderPipeline`. pub struct RenderPipelineBuilder { @@ -176,29 +124,28 @@ impl RenderPipelineBuilder { vertex_shader: &Shader, fragment_shader: Option<&Shader>, ) -> RenderPipeline { - let device = render_context.device(); let surface_format = render_context.surface_format(); // Shader modules let vertex_module = platform_pipeline::ShaderModule::from_spirv( - device, + render_context.gpu(), &vertex_shader.as_binary(), Some("lambda-vertex-shader"), ); let fragment_module = fragment_shader.map(|shader| { platform_pipeline::ShaderModule::from_spirv( - device, + render_context.gpu(), &shader.as_binary(), Some("lambda-fragment-shader"), ) }); // Push constant ranges - let push_constant_ranges: Vec = self + let push_constant_ranges: Vec = self .push_constants .iter() - .map(|(stage, range)| wgpu::PushConstantRange { - stages: stage.to_wgpu(), + .map(|(stage, range)| platform_pipeline::PushConstantRange { + stages: *stage, range: range.clone(), }) .collect(); @@ -213,48 +160,52 @@ impl RenderPipelineBuilder { ); // Pipeline layout via platform - let bgl_raw: Vec<&wgpu::BindGroupLayout> = - self.bind_group_layouts.iter().map(|l| l.raw()).collect(); + let bgl_platform: Vec<&lambda_platform::wgpu::bind::BindGroupLayout> = self + .bind_group_layouts + .iter() + .map(|l| l.platform_layout()) + .collect(); let pipeline_layout = platform_pipeline::PipelineLayoutBuilder::new() .with_label("lambda-pipeline-layout") - .with_layouts(&bgl_raw) + .with_layouts(&bgl_platform) .with_push_constants(push_constant_ranges) - .build(device); + .build(render_context.gpu()); // Vertex buffers and attributes let mut buffers = Vec::with_capacity(self.bindings.len()); let mut rp_builder = platform_pipeline::RenderPipelineBuilder::new() .with_label(self.label.as_deref().unwrap_or("lambda-render-pipeline")) .with_layout(&pipeline_layout) - .with_cull_mode(self.culling.to_wgpu()); + .with_cull_mode(self.culling); for binding in &self.bindings { - let attributes: Vec = binding + let attributes: Vec = binding .attributes .iter() - .map(|attribute| wgpu::VertexAttribute { + .map(|attribute| platform_pipeline::VertexAttributeDesc { shader_location: attribute.location, offset: (attribute.offset + attribute.element.offset) as u64, - format: attribute.element.format.to_vertex_format(), + format: attribute.element.format.to_platform(), }) .collect(); + rp_builder = rp_builder.with_vertex_buffer(binding.buffer.stride(), attributes); buffers.push(binding.buffer.clone()); } if fragment_module.is_some() { - rp_builder = rp_builder.with_color_target(wgpu::ColorTargetState { - format: surface_format, - blend: Some(wgpu::BlendState::ALPHA_BLENDING), - write_mask: wgpu::ColorWrites::ALL, - }); + rp_builder = rp_builder.with_surface_color_target(surface_format); } - let rp = rp_builder.build(device, &vertex_module, fragment_module.as_ref()); + let pipeline = rp_builder.build( + render_context.gpu(), + &vertex_module, + fragment_module.as_ref(), + ); return RenderPipeline { - pipeline: Rc::new(rp.into_raw()), + pipeline: Rc::new(pipeline), buffers, }; } diff --git a/crates/lambda-rs/src/render/render_pass.rs b/crates/lambda-rs/src/render/render_pass.rs index 6dc39652..42b6c4ab 100644 --- a/crates/lambda-rs/src/render/render_pass.rs +++ b/crates/lambda-rs/src/render/render_pass.rs @@ -1,13 +1,11 @@ //! Render pass builders and definitions for lambda runtimes and applications. -use lambda_platform::wgpu::types as wgpu; - use super::RenderContext; #[derive(Debug, Clone)] /// Immutable parameters used when beginning a render pass. pub struct RenderPass { - clear_color: wgpu::Color, + clear_color: [f64; 4], label: Option, } @@ -15,11 +13,8 @@ impl RenderPass { /// Destroy the pass. Kept for symmetry with other resources. pub fn destroy(self, _render_context: &RenderContext) {} - pub(crate) fn color_ops(&self) -> wgpu::Operations { - wgpu::Operations { - load: wgpu::LoadOp::Clear(self.clear_color), - store: wgpu::StoreOp::Store, - } + pub(crate) fn clear_color(&self) -> [f64; 4] { + return self.clear_color; } pub(crate) fn label(&self) -> Option<&str> { @@ -29,7 +24,7 @@ impl RenderPass { /// Builder for a `RenderPass` description. pub struct RenderPassBuilder { - clear_color: wgpu::Color, + clear_color: [f64; 4], label: Option, } @@ -37,13 +32,13 @@ impl RenderPassBuilder { /// Creates a new render pass builder. pub fn new() -> Self { Self { - clear_color: wgpu::Color::BLACK, + clear_color: [0.0, 0.0, 0.0, 1.0], label: None, } } /// Specify the clear color used for the first color attachment. - pub fn with_clear_color(mut self, color: wgpu::Color) -> Self { + pub fn with_clear_color(mut self, color: [f64; 4]) -> Self { self.clear_color = color; self } diff --git a/crates/lambda-rs/src/render/vertex.rs b/crates/lambda-rs/src/render/vertex.rs index d2d7164a..66b9bf2a 100644 --- a/crates/lambda-rs/src/render/vertex.rs +++ b/crates/lambda-rs/src/render/vertex.rs @@ -1,26 +1,23 @@ //! Vertex data structures. -use lambda_platform::wgpu::types as wgpu; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] /// Canonical color/attribute formats used by engine pipelines. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ColorFormat { Rgb32Sfloat, Rgba8Srgb, } impl ColorFormat { - pub(crate) fn to_texture_format(self) -> wgpu::TextureFormat { - match self { - ColorFormat::Rgb32Sfloat => wgpu::TextureFormat::Rgba32Float, - ColorFormat::Rgba8Srgb => wgpu::TextureFormat::Rgba8UnormSrgb, - } - } - - pub(crate) fn to_vertex_format(self) -> wgpu::VertexFormat { + pub(crate) fn to_platform( + self, + ) -> lambda_platform::wgpu::vertex::ColorFormat { match self { - ColorFormat::Rgb32Sfloat => wgpu::VertexFormat::Float32x3, - ColorFormat::Rgba8Srgb => wgpu::VertexFormat::Unorm8x4, + ColorFormat::Rgb32Sfloat => { + lambda_platform::wgpu::vertex::ColorFormat::Rgb32Sfloat + } + ColorFormat::Rgba8Srgb => { + lambda_platform::wgpu::vertex::ColorFormat::Rgba8Srgb + } } } } From 58577b932ddc528e3b6cf905d7dd386bb24f0785 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 3 Nov 2025 15:09:50 -0800 Subject: [PATCH 03/11] [add] more abstractions to the platform layer, instanced draws, and a few other minor things. --- crates/lambda-rs-platform/src/wgpu/buffer.rs | 22 ++++- crates/lambda-rs-platform/src/wgpu/gpu.rs | 93 ++++++++++++++----- crates/lambda-rs-platform/src/wgpu/mod.rs | 27 +++++- .../lambda-rs-platform/src/wgpu/pipeline.rs | 2 +- crates/lambda-rs-platform/src/wgpu/vertex.rs | 4 +- crates/lambda-rs/src/render/buffer.rs | 6 +- crates/lambda-rs/src/render/command.rs | 12 +++ crates/lambda-rs/src/render/mod.rs | 43 ++++++++- 8 files changed, 173 insertions(+), 36 deletions(-) diff --git a/crates/lambda-rs-platform/src/wgpu/buffer.rs b/crates/lambda-rs-platform/src/wgpu/buffer.rs index 819adb0b..3b3c5a10 100644 --- a/crates/lambda-rs-platform/src/wgpu/buffer.rs +++ b/crates/lambda-rs-platform/src/wgpu/buffer.rs @@ -10,6 +10,22 @@ use wgpu::{ use crate::wgpu::Gpu; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +/// Index format for indexed drawing. +pub enum IndexFormat { + Uint16, + Uint32, +} + +impl IndexFormat { + pub(crate) fn to_wgpu(self) -> wgpu::IndexFormat { + return match self { + IndexFormat::Uint16 => wgpu::IndexFormat::Uint16, + IndexFormat::Uint32 => wgpu::IndexFormat::Uint32, + }; + } +} + #[derive(Clone, Copy, Debug)] /// Platform buffer usage flags. pub struct Usage(pub(crate) wgpu::BufferUsages); @@ -65,13 +81,13 @@ impl Buffer { } /// Size in bytes at creation time. - pub fn size(&self) -> wgpu::BufferAddress { + pub fn size(&self) -> u64 { return self.size; } /// Usage flags used to create the buffer. - pub fn usage(&self) -> wgpu::BufferUsages { - return self.usage; + pub fn usage(&self) -> Usage { + return Usage(self.usage); } /// Write raw bytes into the buffer at the given offset. diff --git a/crates/lambda-rs-platform/src/wgpu/gpu.rs b/crates/lambda-rs-platform/src/wgpu/gpu.rs index 4f00f982..9a8702f0 100644 --- a/crates/lambda-rs-platform/src/wgpu/gpu.rs +++ b/crates/lambda-rs-platform/src/wgpu/gpu.rs @@ -5,6 +5,60 @@ use super::{ Instance, }; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +/// Power preference for selecting a GPU adapter. +pub enum PowerPreference { + HighPerformance, + LowPower, +} + +impl PowerPreference { + pub(crate) fn to_wgpu(self) -> wgpu::PowerPreference { + return match self { + PowerPreference::HighPerformance => { + wgpu::PowerPreference::HighPerformance + } + PowerPreference::LowPower => wgpu::PowerPreference::LowPower, + }; + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +/// Memory allocation hints for device and resource creation. +pub enum MemoryHints { + Performance, + MemoryUsage, +} + +impl MemoryHints { + pub(crate) fn to_wgpu(self) -> wgpu::MemoryHints { + return match self { + MemoryHints::Performance => wgpu::MemoryHints::Performance, + MemoryHints::MemoryUsage => wgpu::MemoryHints::MemoryUsage, + }; + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +/// Feature bitset required/enabled on the device. +pub struct Features(wgpu::Features); + +impl Features { + /// Enable push constants support. + pub const PUSH_CONSTANTS: Features = Features(wgpu::Features::PUSH_CONSTANTS); + + pub(crate) fn to_wgpu(self) -> wgpu::Features { + self.0 + } +} + +impl std::ops::BitOr for Features { + type Output = Features; + fn bitor(self, rhs: Features) -> Features { + return Features(self.0 | rhs.0); + } +} + #[derive(Clone, Copy, Debug)] /// Public, engine-facing subset of device limits. pub struct GpuLimits { @@ -17,10 +71,10 @@ pub struct GpuLimits { /// Builder for a `Gpu` (adapter, device, queue) with feature validation. pub struct GpuBuilder { label: Option, - power_preference: wgpu::PowerPreference, + power_preference: PowerPreference, force_fallback_adapter: bool, - required_features: wgpu::Features, - memory_hints: wgpu::MemoryHints, + required_features: Features, + memory_hints: MemoryHints, } impl GpuBuilder { @@ -28,10 +82,10 @@ impl GpuBuilder { pub fn new() -> Self { Self { label: Some("Lambda GPU".to_string()), - power_preference: wgpu::PowerPreference::HighPerformance, + power_preference: PowerPreference::HighPerformance, force_fallback_adapter: false, - required_features: wgpu::Features::PUSH_CONSTANTS, - memory_hints: wgpu::MemoryHints::Performance, + required_features: Features::PUSH_CONSTANTS, + memory_hints: MemoryHints::Performance, } } @@ -42,10 +96,7 @@ impl GpuBuilder { } /// Select the adapter power preference (e.g., LowPower for laptops). - pub fn with_power_preference( - mut self, - preference: wgpu::PowerPreference, - ) -> Self { + pub fn with_power_preference(mut self, preference: PowerPreference) -> Self { self.power_preference = preference; self } @@ -57,13 +108,13 @@ impl GpuBuilder { } /// Require `wgpu::Features` to be present on the adapter. - pub fn with_required_features(mut self, features: wgpu::Features) -> Self { + pub fn with_required_features(mut self, features: Features) -> Self { self.required_features = features; self } /// Provide memory allocation hints for the device. - pub fn with_memory_hints(mut self, hints: wgpu::MemoryHints) -> Self { + pub fn with_memory_hints(mut self, hints: MemoryHints) -> Self { self.memory_hints = hints; self } @@ -79,25 +130,25 @@ impl GpuBuilder { ) -> Result { let adapter = instance .request_adapter(&wgpu::RequestAdapterOptions { - power_preference: self.power_preference, + power_preference: self.power_preference.to_wgpu(), force_fallback_adapter: self.force_fallback_adapter, compatible_surface: surface.map(|surface| surface.surface()), }) .map_err(|_| GpuBuildError::AdapterUnavailable)?; let adapter_features = adapter.features(); - if !adapter_features.contains(self.required_features) { + if !adapter_features.contains(self.required_features.to_wgpu()) { return Err(GpuBuildError::MissingFeatures { requested: self.required_features, - available: adapter_features, + available: Features(adapter_features), }); } let descriptor = wgpu::DeviceDescriptor { label: self.label.as_deref(), - required_features: self.required_features, + required_features: self.required_features.to_wgpu(), required_limits: adapter.limits(), - memory_hints: self.memory_hints, + memory_hints: self.memory_hints.to_wgpu(), trace: wgpu::Trace::Off, }; @@ -120,16 +171,16 @@ pub enum GpuBuildError { AdapterUnavailable, /// The requested features are not supported by the selected adapter. MissingFeatures { - requested: wgpu::Features, - available: wgpu::Features, + requested: Features, + available: Features, }, /// Wrapper for `wgpu::RequestDeviceError`. - RequestDevice(wgpu::RequestDeviceError), + RequestDevice(String), } impl From for GpuBuildError { fn from(error: wgpu::RequestDeviceError) -> Self { - return GpuBuildError::RequestDevice(error); + return GpuBuildError::RequestDevice(format!("{:?}", error)); } } diff --git a/crates/lambda-rs-platform/src/wgpu/mod.rs b/crates/lambda-rs-platform/src/wgpu/mod.rs index 5cda65e8..e460b144 100644 --- a/crates/lambda-rs-platform/src/wgpu/mod.rs +++ b/crates/lambda-rs-platform/src/wgpu/mod.rs @@ -16,9 +16,12 @@ pub mod surface; pub mod vertex; pub use gpu::{ + Features, Gpu, GpuBuildError, GpuBuilder, + MemoryHints, + PowerPreference, }; pub use surface::{ Frame, @@ -131,7 +134,7 @@ impl Instance { /// /// This simply blocks on `wgpu::Instance::request_adapter` and returns /// `None` if no suitable adapter is found. - pub fn request_adapter<'surface, 'window>( + pub(crate) fn request_adapter<'surface, 'window>( &self, options: &wgpu::RequestAdapterOptions<'surface, 'window>, ) -> Result { @@ -170,7 +173,7 @@ impl CommandEncoder { /// Begin a render pass targeting a single color attachment with the provided /// load/store operations. Depth/stencil is not attached by this helper. - pub fn begin_render_pass<'view>( + pub(crate) fn begin_render_pass<'view>( &'view mut self, label: Option<&str>, view: &'view surface::TextureViewRef<'view>, @@ -270,6 +273,17 @@ impl<'a> RenderPass<'a> { self.raw.set_vertex_buffer(slot, buffer.raw().slice(..)); } + /// Bind an index buffer with the provided index format. + pub fn set_index_buffer( + &mut self, + buffer: &buffer::Buffer, + format: buffer::IndexFormat, + ) { + self + .raw + .set_index_buffer(buffer.raw().slice(..), format.to_wgpu()); + } + /// Upload push constants. pub fn set_push_constants( &mut self, @@ -284,6 +298,15 @@ impl<'a> RenderPass<'a> { pub fn draw(&mut self, vertices: std::ops::Range) { self.raw.draw(vertices, 0..1); } + + /// Issue an indexed draw with a base vertex applied. + pub fn draw_indexed( + &mut self, + indices: std::ops::Range, + base_vertex: i32, + ) { + self.raw.draw_indexed(indices, base_vertex, 0..1); + } } #[cfg(test)] diff --git a/crates/lambda-rs-platform/src/wgpu/pipeline.rs b/crates/lambda-rs-platform/src/wgpu/pipeline.rs index 00be9c3f..11ef2ff1 100644 --- a/crates/lambda-rs-platform/src/wgpu/pipeline.rs +++ b/crates/lambda-rs-platform/src/wgpu/pipeline.rs @@ -198,7 +198,7 @@ impl RenderPipeline { return &self.raw; } /// Consume and return the raw pipeline. - pub fn into_raw(self) -> wgpu::RenderPipeline { + pub(crate) fn into_raw(self) -> wgpu::RenderPipeline { return self.raw; } } diff --git a/crates/lambda-rs-platform/src/wgpu/vertex.rs b/crates/lambda-rs-platform/src/wgpu/vertex.rs index 560e2df7..1c6b78fb 100644 --- a/crates/lambda-rs-platform/src/wgpu/vertex.rs +++ b/crates/lambda-rs-platform/src/wgpu/vertex.rs @@ -6,14 +6,14 @@ pub enum ColorFormat { } impl ColorFormat { - pub fn to_texture_format(self) -> wgpu::TextureFormat { + pub(crate) fn to_texture_format(self) -> wgpu::TextureFormat { return match self { ColorFormat::Rgb32Sfloat => wgpu::TextureFormat::Rgba32Float, ColorFormat::Rgba8Srgb => wgpu::TextureFormat::Rgba8UnormSrgb, }; } - pub fn to_vertex_format(self) -> wgpu::VertexFormat { + pub(crate) fn to_vertex_format(self) -> wgpu::VertexFormat { return match self { ColorFormat::Rgb32Sfloat => wgpu::VertexFormat::Float32x3, ColorFormat::Rgba8Srgb => wgpu::VertexFormat::Unorm8x4, diff --git a/crates/lambda-rs/src/render/buffer.rs b/crates/lambda-rs/src/render/buffer.rs index aa28c71f..5c5344d0 100644 --- a/crates/lambda-rs/src/render/buffer.rs +++ b/crates/lambda-rs/src/render/buffer.rs @@ -10,7 +10,7 @@ use super::{ RenderContext, }; -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] /// High‑level classification for buffers created by the engine. /// /// The type guides default usage flags and how a buffer is bound during @@ -81,8 +81,8 @@ impl Default for Properties { /// Buffer for storing data on the GPU. /// -/// Wraps a `wgpu::Buffer` and tracks the element stride and logical type used -/// when binding to pipeline inputs. +/// Wraps a platform GPU buffer and tracks the element stride and logical type +/// used when binding to pipeline inputs. #[derive(Debug)] pub struct Buffer { buffer: Rc, diff --git a/crates/lambda-rs/src/render/command.rs b/crates/lambda-rs/src/render/command.rs index 80579eb4..d78ef6e1 100644 --- a/crates/lambda-rs/src/render/command.rs +++ b/crates/lambda-rs/src/render/command.rs @@ -42,8 +42,20 @@ pub enum RenderCommand { pipeline: super::ResourceId, buffer: u32, }, + /// Bind an index buffer by resource id with format. + BindIndexBuffer { + /// Resource identifier returned by `RenderContext::attach_buffer`. + buffer: super::ResourceId, + /// Index format for this buffer. + format: lambda_platform::wgpu::buffer::IndexFormat, + }, /// Issue a non‑indexed draw for the provided vertex range. Draw { vertices: Range }, + /// Issue an indexed draw for the provided index range. + DrawIndexed { + indices: Range, + base_vertex: i32, + }, /// Bind a previously created bind group to a set index with optional /// dynamic offsets. Dynamic offsets are counted in bytes and must obey the diff --git a/crates/lambda-rs/src/render/mod.rs b/crates/lambda-rs/src/render/mod.rs index e1dfe101..97010842 100644 --- a/crates/lambda-rs/src/render/mod.rs +++ b/crates/lambda-rs/src/render/mod.rs @@ -15,7 +15,10 @@ pub mod vertex; pub mod viewport; pub mod window; -use std::iter; +use std::{ + iter, + rc::Rc, +}; use lambda_platform::wgpu::{ CommandEncoder as PlatformCommandEncoder, @@ -36,8 +39,8 @@ use self::{ /// Builder for configuring a `RenderContext` tied to a single window. /// -/// The builder wires up a `wgpu::Instance`, `Surface`, and `Gpu` using the -/// cross‑platform platform layer, then configures the surface with reasonable +/// The builder wires up a platform graphics instance, `Surface`, and `Gpu` +/// using the cross‑platform platform layer, then configures the surface with reasonable /// defaults. Use this when setting up rendering for an application window. pub struct RenderContextBuilder { name: String, @@ -108,11 +111,12 @@ impl RenderContextBuilder { render_pipelines: vec![], bind_group_layouts: vec![], bind_groups: vec![], + buffers: vec![], }; } } -/// High‑level rendering context backed by `wgpu` for a single window. +/// High‑level rendering context backed by the platform GPU for a single window. /// /// The context owns the `Instance`, presentation `Surface`, and `Gpu` device /// objects and maintains a set of attached render passes and pipelines used @@ -130,6 +134,7 @@ pub struct RenderContext { render_pipelines: Vec, bind_group_layouts: Vec, bind_groups: Vec, + buffers: Vec>, } /// Opaque handle used to refer to resources attached to a `RenderContext`. @@ -167,6 +172,13 @@ impl RenderContext { return id; } + /// Attach a generic GPU buffer and return a handle for render commands. + pub fn attach_buffer(&mut self, buffer: buffer::Buffer) -> ResourceId { + let id = self.buffers.len(); + self.buffers.push(Rc::new(buffer)); + return id; + } + /// Explicitly destroy the context. Dropping also releases resources. pub fn destroy(self) { drop(self); @@ -368,6 +380,23 @@ impl RenderContext { pass.set_vertex_buffer(buffer as u32, buffer_ref.raw()); } + RenderCommand::BindIndexBuffer { buffer, format } => { + let buffer_ref = self.buffers.get(buffer).ok_or_else(|| { + return RenderError::Configuration(format!( + "Index buffer id {} not found", + buffer + )); + })?; + // Soft validation: encourage correct logical type. + if buffer_ref.buffer_type() != buffer::BufferType::Index { + logging::warn!( + "Binding buffer id {} as index but logical type is {:?}", + buffer, + buffer_ref.buffer_type() + ); + } + pass.set_index_buffer(buffer_ref.raw(), format); + } RenderCommand::PushConstants { pipeline, stage, @@ -390,6 +419,12 @@ impl RenderContext { RenderCommand::Draw { vertices } => { pass.draw(vertices); } + RenderCommand::DrawIndexed { + indices, + base_vertex, + } => { + pass.draw_indexed(indices, base_vertex); + } RenderCommand::BeginRenderPass { .. } => { return Err(RenderError::Configuration( "Nested render passes are not supported.".to_string(), From 786bd67f8f0480892153958ab8f15d64aec300e5 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 3 Nov 2025 15:13:57 -0800 Subject: [PATCH 04/11] [update] surface reconfiguration. --- crates/lambda-rs/src/render/mod.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/crates/lambda-rs/src/render/mod.rs b/crates/lambda-rs/src/render/mod.rs index 97010842..e275e764 100644 --- a/crates/lambda-rs/src/render/mod.rs +++ b/crates/lambda-rs/src/render/mod.rs @@ -456,12 +456,7 @@ impl RenderContext { ) -> Result<(), RenderError> { self .surface - .configure_with_defaults( - &self.gpu, - size, - self.present_mode, - self.texture_usage, - ) + .resize(&self.gpu, size) .map_err(RenderError::Configuration)?; let config = self.surface.configuration().cloned().ok_or_else(|| { From 87b00d761beb2851c29ec970f8f3b789257c1e3a Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 3 Nov 2025 15:31:32 -0800 Subject: [PATCH 05/11] [update] surface reconfiguration. --- crates/lambda-rs-platform/src/lib.rs | 4 + crates/lambda-rs-platform/src/wgpu/vertex.rs | 6 ++ crates/lambda-rs/src/lib.rs | 12 ++- crates/lambda-rs/src/render/bind.rs | 56 ++++++++++-- crates/lambda-rs/src/render/buffer.rs | 71 ++++++++++++++-- crates/lambda-rs/src/render/command.rs | 16 +++- crates/lambda-rs/src/render/mesh.rs | 12 ++- crates/lambda-rs/src/render/mod.rs | 89 +++++++++++++++++--- crates/lambda-rs/src/render/pipeline.rs | 36 +++++++- crates/lambda-rs/src/render/render_pass.rs | 13 ++- crates/lambda-rs/src/render/scene_math.rs | 22 ++++- crates/lambda-rs/src/render/shader.rs | 28 +++++- crates/lambda-rs/src/render/vertex.rs | 12 ++- crates/lambda-rs/src/render/viewport.rs | 8 +- crates/lambda-rs/src/render/window.rs | 6 +- 15 files changed, 349 insertions(+), 42 deletions(-) diff --git a/crates/lambda-rs-platform/src/lib.rs b/crates/lambda-rs-platform/src/lib.rs index 2e4ca19c..faefca66 100644 --- a/crates/lambda-rs-platform/src/lib.rs +++ b/crates/lambda-rs-platform/src/lib.rs @@ -5,6 +5,10 @@ //! (graphics) that provide consistent defaults and ergonomic builders, along //! with shader compilation backends and small helper modules (e.g., OBJ //! loading and random number generation). +//! +//! Stability: this is an internal support layer for `lambda-rs`. Public +//! types are exposed as a convenience to the higher‑level crate and MAY change +//! between releases to fit engine needs. pub mod obj; pub mod rand; pub mod shader; diff --git a/crates/lambda-rs-platform/src/wgpu/vertex.rs b/crates/lambda-rs-platform/src/wgpu/vertex.rs index 1c6b78fb..da8b2ded 100644 --- a/crates/lambda-rs-platform/src/wgpu/vertex.rs +++ b/crates/lambda-rs-platform/src/wgpu/vertex.rs @@ -1,3 +1,9 @@ +//! Vertex color/attribute formats used by the platform layer. +//! +//! These map directly to `wgpu` texture/vertex formats and are re‑exported via +//! the high‑level rendering module. This is an internal surface that may +//! evolve with engine needs. + /// Canonical color/attribute formats used by engine pipelines. #[derive(Clone, Copy, Debug)] pub enum ColorFormat { diff --git a/crates/lambda-rs/src/lib.rs b/crates/lambda-rs/src/lib.rs index b3a9b2b2..109eb8f9 100644 --- a/crates/lambda-rs/src/lib.rs +++ b/crates/lambda-rs/src/lib.rs @@ -1,5 +1,15 @@ #![allow(clippy::needless_return)] -//! Lambda is a simple, fast, and safe compute engine written in Rust. +//! Lambda is a simple, fast, and safe engine for desktop applications and rendering in Rust. +//! +//! High‑level modules +//! - `render`: windowed rendering built on top of platform abstractions with +//! explicit command encoding and ergonomic builders. +//! - `runtimes`: application runtime helpers that create windows and drive the +//! event/render loop. +//! - `math`: minimal vector/matrix utilities used by examples and helpers. +//! +//! See runnable examples in `crates/lambda-rs/examples/` and integration tests +//! under `crates/lambda-rs/tests/` for typical usage patterns. pub mod component; pub mod events; diff --git a/crates/lambda-rs/src/render/bind.rs b/crates/lambda-rs/src/render/bind.rs index 011a764a..03ff6571 100644 --- a/crates/lambda-rs/src/render/bind.rs +++ b/crates/lambda-rs/src/render/bind.rs @@ -1,13 +1,30 @@ -//! High-level bind group and bind group layout wrappers and builders. +//! Bind group layouts and bind groups for resource binding. //! -//! This module exposes ergonomic builders for declaring uniform buffer -//! bindings and constructing bind groups, following the same style used by the -//! buffer, pipeline, and render pass builders. +//! Purpose +//! - Describe how shader stages access resources via `BindGroupLayout`. +//! - Create `BindGroup` instances that bind buffers to specific indices for a +//! pipeline layout set. +//! +//! Scope and usage +//! - The engine exposes uniform buffer bindings first, with optional dynamic +//! offsets. Storage textures and samplers may be added in the future. +//! - A layout declares binding indices and stage visibility. A bind group then +//! provides concrete buffers for those indices. At draw time, a group is +//! bound to a set index on the pipeline. +//! - For dynamic uniform bindings, pass one offset per dynamic binding at +//! `SetBindGroup` time. Offsets MUST follow the device’s +//! `min_uniform_buffer_offset_alignment`. +//! +//! See `crates/lambda-rs/examples/uniform_buffer_triangle.rs` for a complete +//! example. use std::rc::Rc; #[derive(Clone, Copy, Debug)] -/// Visibility of a binding across shader stages (engine facing). +/// Visibility of a binding across shader stages (engine‑facing). +/// +/// Select one or more shader stages that read a bound resource. Use +/// `VertexAndFragment` for shared layouts in typical graphics pipelines. pub enum BindingVisibility { Vertex, Fragment, @@ -48,6 +65,9 @@ mod tests { #[derive(Debug, Clone)] /// Bind group layout used when creating pipelines and bind groups. +/// +/// Holds a platform layout and the number of dynamic bindings so callers can +/// validate dynamic offset counts at bind time. pub struct BindGroupLayout { layout: Rc, /// Total number of dynamic bindings declared in this layout. @@ -70,6 +90,10 @@ impl BindGroupLayout { #[derive(Debug, Clone)] /// Bind group that binds one or more resources to a pipeline set index. +/// +/// The group mirrors the structure of its `BindGroupLayout`. When using +/// dynamic uniforms, record a corresponding list of byte offsets in the +/// `RenderCommand::SetBindGroup` command. pub struct BindGroup { group: Rc, /// Cached number of dynamic bindings expected when binding this group. @@ -90,6 +114,16 @@ impl BindGroup { } /// Builder for creating a bind group layout with uniform buffer bindings. +/// +/// Example +/// ```rust +/// // One static camera UBO at binding 0 and one dynamic model UBO at 1. +/// // Visible in both vertex and fragment stages. +/// use lambda::render::bind::{BindGroupLayoutBuilder, BindingVisibility}; +/// let bgl = BindGroupLayoutBuilder::new() +/// .with_uniform(0, BindingVisibility::VertexAndFragment) +/// .with_uniform_dynamic(1, BindingVisibility::VertexAndFragment); +/// ``` pub struct BindGroupLayoutBuilder { label: Option, entries: Vec<(u32, BindingVisibility, bool)>, @@ -175,6 +209,18 @@ impl BindGroupLayoutBuilder { } /// Builder for creating a bind group for a previously built layout. +/// +/// Example +/// ```rust +/// // Assume `camera_ubo` and `model_ubo` are created `UniformBuffer`. +/// // Bind them to match the layout: camera at 0, model at 1 with dynamic offset. +/// use lambda::render::bind::BindGroupBuilder; +/// let group = BindGroupBuilder::new() +/// .with_layout(&layout) +/// .with_uniform(0, camera_ubo.raw(), 0, None) +/// .with_uniform(1, model_ubo.raw(), 0, None); +/// // During rendering, provide a dynamic offset for binding 1 in bytes. +/// ``` pub struct BindGroupBuilder<'a> { label: Option, layout: Option<&'a BindGroupLayout>, diff --git a/crates/lambda-rs/src/render/buffer.rs b/crates/lambda-rs/src/render/buffer.rs index 5c5344d0..6e34630f 100644 --- a/crates/lambda-rs/src/render/buffer.rs +++ b/crates/lambda-rs/src/render/buffer.rs @@ -1,4 +1,22 @@ -//! Buffers for allocating memory on the GPU. +//! GPU buffers for vertex/index data and uniforms. +//! +//! Purpose +//! - Allocate memory on the GPU for vertex and index streams, per‑draw or +//! per‑frame uniform data, and general storage when needed. +//! - Provide a stable engine‑facing `Buffer` with logical type and stride so +//! pipelines and commands can bind and validate buffers correctly. +//! +//! Usage +//! - Use `BufferBuilder` to create typed buffers with explicit usage and +//! residency properties. +//! - Use `UniformBuffer` for a concise pattern when a single `T` value is +//! updated on the CPU and bound as a uniform. +//! +//! Examples +//! - Creating a vertex buffer from a mesh: `BufferBuilder::build_from_mesh`. +//! - Creating a uniform buffer and updating it each frame: +//! see `UniformBuffer` below and the runnable example +//! `crates/lambda-rs/examples/uniform_buffer_triangle.rs`. use std::rc::Rc; @@ -14,7 +32,11 @@ use super::{ /// High‑level classification for buffers created by the engine. /// /// The type guides default usage flags and how a buffer is bound during -/// encoding (e.g., as a vertex or index buffer). +/// encoding: +/// - `Vertex`: per‑vertex attribute streams consumed by the vertex stage. +/// - `Index`: index streams used for indexed drawing. +/// - `Uniform`: small, read‑only parameters used by shaders. +/// - `Storage`: general read/write data (not yet surfaced by high‑level APIs). pub enum BufferType { Vertex, Index, @@ -23,7 +45,7 @@ pub enum BufferType { } #[derive(Clone, Copy, Debug)] -/// Buffer usage flags (engine-facing), mapped to platform usage internally. +/// Buffer usage flags (engine‑facing), mapped to platform usage internally. pub struct Usage(platform_buffer::Usage); impl Usage { @@ -57,6 +79,9 @@ impl Default for Usage { #[derive(Clone, Copy, Debug)] /// Buffer allocation properties that control residency and CPU visibility. +/// +/// Use `CPU_VISIBLE` for frequently updated data (e.g., uniform uploads). +/// Prefer `DEVICE_LOCAL` for static geometry uploaded once. pub struct Properties { cpu_visible: bool, } @@ -83,6 +108,11 @@ impl Default for Properties { /// /// Wraps a platform GPU buffer and tracks the element stride and logical type /// used when binding to pipeline inputs. +/// +/// Notes +/// - Writing is performed via the device queue using `write_value` or by +/// creating CPU‑visible buffers and re‑building with new contents when +/// appropriate. #[derive(Debug)] pub struct Buffer { buffer: Rc, @@ -132,7 +162,20 @@ impl Buffer { /// /// Stores a single value of type `T` and provides a convenience method to /// upload updates to the GPU. The underlying buffer has `UNIFORM` usage and -/// is CPU‑visible by default for easy updates via `Queue::write_buffer`. +/// is CPU‑visible by default for direct queue writes. +/// +/// Example +/// ```rust +/// // Model‑view‑projection updated every frame +/// #[repr(C)] +/// #[derive(Clone, Copy)] +/// struct Mvp { m: [[f32;4];4] } +/// let mut mvp = Mvp { m: [[0.0;4];4] }; +/// let mvp_ubo = UniformBuffer::new(render_context, &mvp, Some("mvp")).unwrap(); +/// // ... later per‑frame +/// mvp = compute_next_mvp(); +/// mvp_ubo.write(&render_context, &mvp); +/// ``` pub struct UniformBuffer { inner: Buffer, _phantom: core::marker::PhantomData, @@ -174,10 +217,22 @@ impl UniformBuffer { /// Builder for creating `Buffer` objects with explicit usage and properties. /// -/// A buffer is a block of memory the GPU can access. You supply a total byte -/// length, usage flags, and residency properties; the builder will initialize -/// the buffer with provided contents and add `COPY_DST` when CPU visibility is -/// requested. +/// A buffer is a block of memory the GPU can access. Supply a total byte +/// length, usage flags, and residency properties; the builder initializes the +/// buffer with provided contents and adds the necessary copy usage when CPU +/// visibility is requested. +/// +/// Example (vertex buffer) +/// ```rust +/// use lambda::render::buffer::{BufferBuilder, Usage, Properties, BufferType}; +/// let vertices: Vec = build_vertices(); +/// let vb = BufferBuilder::new() +/// .with_usage(Usage::VERTEX) +/// .with_properties(Properties::DEVICE_LOCAL) +/// .with_buffer_type(BufferType::Vertex) +/// .build(render_context, vertices) +/// .unwrap(); +/// ``` pub struct BufferBuilder { buffer_length: usize, usage: Usage, diff --git a/crates/lambda-rs/src/render/command.rs b/crates/lambda-rs/src/render/command.rs index d78ef6e1..87456e3f 100644 --- a/crates/lambda-rs/src/render/command.rs +++ b/crates/lambda-rs/src/render/command.rs @@ -1,4 +1,9 @@ -//! Render command definitions for lambda runtimes. +//! Render command definitions for Lambda runtimes. +//! +//! A frame is described as a linear list of `RenderCommand`s. The sequence +//! MUST start a render pass before any draw‑related commands and MUST end the +//! pass explicitly. Resource and pipeline handles referenced by the commands +//! MUST have been attached to the active `RenderContext`. use std::ops::Range; @@ -8,6 +13,10 @@ use super::{ }; /// Commands recorded and executed by the `RenderContext` to produce a frame. +/// +/// Order and validity are enforced by the encoder where possible. Invalid +/// sequences (e.g., nested passes or missing `EndRenderPass`) are reported as +/// configuration errors. #[derive(Debug, Clone)] pub enum RenderCommand { /// Set one or more viewports starting at `start_at` slot. @@ -22,7 +31,7 @@ pub enum RenderCommand { }, /// Bind a previously attached graphics pipeline by id. SetPipeline { pipeline: super::ResourceId }, - /// Begin a render pass that targets the swapchain. + /// Begin a render pass that targets the swapchain color attachment. BeginRenderPass { render_pass: super::ResourceId, viewport: Viewport, @@ -31,6 +40,9 @@ pub enum RenderCommand { EndRenderPass, /// Upload push constants for the active pipeline/stage at `offset`. + /// + /// The byte vector is interpreted as tightly packed `u32` words; the + /// builder turns it into raw bytes when encoding. PushConstants { pipeline: super::ResourceId, stage: PipelineStage, diff --git a/crates/lambda-rs/src/render/mesh.rs b/crates/lambda-rs/src/render/mesh.rs index 1f438399..2cb3fce7 100644 --- a/crates/lambda-rs/src/render/mesh.rs +++ b/crates/lambda-rs/src/render/mesh.rs @@ -1,4 +1,12 @@ -//! Mesh Implementation +//! Simple mesh container used by examples and helpers. +//! +//! Purpose +//! - Hold a `Vec` and matching `VertexAttribute` layout used to build +//! vertex buffers and pipelines. +//! - Provide minimal builders plus an OBJ loader path for quick iteration. +//! +//! Note: this is a convenience structure for examples; larger applications may +//! want dedicated asset/geometry systems. use lambda_platform::obj::load_textured_obj_from_file; @@ -32,7 +40,7 @@ impl Mesh { // ------------------------------ MeshBuilder --------------------------------- -/// Construction for a mesh. +/// Builder for constructing a `Mesh` from vertices and attributes. #[derive(Clone, Debug)] pub struct MeshBuilder { capacity: usize, diff --git a/crates/lambda-rs/src/render/mod.rs b/crates/lambda-rs/src/render/mod.rs index e275e764..9bf54104 100644 --- a/crates/lambda-rs/src/render/mod.rs +++ b/crates/lambda-rs/src/render/mod.rs @@ -1,5 +1,32 @@ -//! High level Rendering API designed for cross platform rendering and -//! windowing. +//! High‑level rendering API for cross‑platform windowed applications. +//! +//! The rendering module provides a small set of stable, engine‑facing types +//! that assemble a frame using explicit commands. It hides lower‑level +//! platform details (the `wgpu` device, queue, surfaces, and raw descriptors) +//! behind builders and handles while keeping configuration visible and +//! predictable. +//! +//! Concepts +//! - `RenderContext`: owns the graphics instance, presentation surface, and +//! GPU device/queue for a single window. It is the submit point for per‑frame +//! command encoding. +//! - `RenderPass` and `RenderPipeline`: immutable descriptions used when +//! beginning a pass and binding a pipeline. Pipelines declare their vertex +//! inputs, push constants, and layout (bind group layouts). +//! - `Buffer`, `BindGroupLayout`, and `BindGroup`: GPU resources created via +//! builders and attached to the context, then referenced by small integer +//! handles when encoding commands. +//! - `RenderCommand`: an explicit, validated sequence that begins with +//! `BeginRenderPass`, binds state, draws, and ends with `EndRenderPass`. +//! +//! Minimal flow +//! 1) Create a window and a `RenderContext` with `RenderContextBuilder`. +//! 2) Build resources (buffers, bind group layouts, shaders, pipelines). +//! 3) Record a `Vec` each frame and pass it to +//! `RenderContext::render`. +//! +//! See workspace examples under `crates/lambda-rs/examples/` for runnable +//! end‑to‑end snippets. // Module Exports pub mod bind; @@ -37,11 +64,22 @@ use self::{ render_pass::RenderPass, }; -/// Builder for configuring a `RenderContext` tied to a single window. +/// Builder for configuring a `RenderContext` tied to one window. /// -/// The builder wires up a platform graphics instance, `Surface`, and `Gpu` -/// using the cross‑platform platform layer, then configures the surface with reasonable -/// defaults. Use this when setting up rendering for an application window. +/// Purpose +/// - Construct the graphics `Instance`, presentation `Surface`, and logical +/// `Gpu` using the platform layer. +/// - Configure the surface with sane defaults (sRGB when available, +/// `Fifo`/vsync‑compatible present mode, `RENDER_ATTACHMENT` usage). +/// +/// Usage +/// - Create with a human‑readable name used in debug labels. +/// - Optionally adjust timeouts, then `build(window)` to obtain a +/// `RenderContext`. +/// +/// Typical use is in an application runtime immediately after creating a +/// window. The returned `RenderContext` owns all GPU objects required to render +/// to that window. pub struct RenderContextBuilder { name: String, _render_timeout: u64, @@ -116,11 +154,22 @@ impl RenderContextBuilder { } } -/// High‑level rendering context backed by the platform GPU for a single window. +/// High‑level rendering context for a single window. /// -/// The context owns the `Instance`, presentation `Surface`, and `Gpu` device -/// objects and maintains a set of attached render passes and pipelines used -/// while encoding command streams for each frame. +/// Purpose +/// - Own the platform `Instance`, presentation `Surface`, and logical `Gpu` +/// objects bound to one window. +/// - Host immutable resources (`RenderPass`, `RenderPipeline`, bind layouts, +/// bind groups, and buffers) and expose small integer handles to reference +/// them when recording commands. +/// - Encode and submit per‑frame work based on an explicit `RenderCommand` +/// list. +/// +/// Behavior +/// - All methods avoid panics unless explicitly documented; recoverable errors +/// are logged and dropped to keep the app running where possible. +/// - Surface loss or outdated configuration triggers transparent +/// reconfiguration with preserved present mode and usage. pub struct RenderContext { label: String, instance: Instance, @@ -186,7 +235,15 @@ impl RenderContext { /// Render a list of commands. No‑ops when the list is empty. /// - /// Errors are logged and do not panic; see `RenderError` for cases. + /// Expectations + /// - The sequence MUST begin a render pass before issuing draw‑related + /// commands and MUST terminate that pass with `EndRenderPass`. + /// - Referenced resource handles (passes, pipelines, buffers, bind groups) + /// MUST have been attached to this context. + /// + /// Error handling + /// - Errors are logged and do not panic (e.g., lost/outdated surface, + /// missing resources, invalid dynamic offsets). See `RenderError`. pub fn render(&mut self, commands: Vec) { if commands.is_empty() { return; @@ -210,11 +267,15 @@ impl RenderContext { } /// Borrow a previously attached render pass by id. + /// + /// Panics if `id` does not refer to an attached pass. pub fn get_render_pass(&self, id: ResourceId) -> &RenderPass { return &self.render_passes[id]; } /// Borrow a previously attached render pipeline by id. + /// + /// Panics if `id` does not refer to an attached pipeline. pub fn get_render_pipeline(&self, id: ResourceId) -> &RenderPipeline { return &self.render_pipelines[id]; } @@ -471,7 +532,11 @@ impl RenderContext { } #[derive(Debug)] -/// Errors that can occur while preparing or presenting a frame. +/// Errors reported while preparing or presenting a frame. +/// +/// Variants summarize recoverable issues that can appear during frame +/// acquisition or command encoding. The renderer logs these and continues when +/// possible; callers SHOULD treat them as warnings unless persistent. pub enum RenderError { Surface(lambda_platform::wgpu::SurfaceError), Configuration(String), diff --git a/crates/lambda-rs/src/render/pipeline.rs b/crates/lambda-rs/src/render/pipeline.rs index b015779e..88dcd156 100644 --- a/crates/lambda-rs/src/render/pipeline.rs +++ b/crates/lambda-rs/src/render/pipeline.rs @@ -1,5 +1,29 @@ -//! Render pipeline builders and definitions for lambda runtimes and -//! applications. +//! Graphics render pipelines and builders. +//! +//! Purpose +//! - Define how vertex data flows into the vertex shader (buffer layouts and +//! attributes) and how fragments are produced (optional fragment stage and +//! color target). +//! - Compose a pipeline layout from bind group layouts and optional push +//! constant ranges. +//! +//! Usage +//! - Accumulate vertex buffers and `VertexAttribute` lists matching shader +//! `location`s. +//! - Provide one or more `BindGroupLayout`s used by the shaders. +//! - Supply a vertex shader and optional fragment shader compiled to SPIR‑V. +//! +//! Example +//! ```rust +//! // Single vertex buffer with position/color; one push constant range for the vertex stage +//! use lambda::render::pipeline::{RenderPipelineBuilder, PipelineStage, CullingMode}; +//! let pipeline = RenderPipelineBuilder::new() +//! .with_buffer(vertex_buffer, attributes) +//! .with_push_constant(PipelineStage::VERTEX, 64) +//! .with_layouts(&[&globals_bgl]) +//! .with_culling(CullingMode::Back) +//! .build(&mut render_context, &render_pass, &vs, Some(&fs)); +//! ``` use std::{ ops::Range, @@ -19,6 +43,8 @@ use super::{ #[derive(Debug)] /// A created graphics pipeline and the vertex buffers it expects. +/// +/// Pipelines are immutable; destroy them with the context when no longer needed. pub struct RenderPipeline { pipeline: Rc, buffers: Vec>, @@ -54,6 +80,12 @@ struct BufferBinding { pub use platform_pipeline::CullingMode; /// Builder for creating a graphics `RenderPipeline`. +/// +/// Notes +/// - The number of bind group layouts MUST NOT exceed the device limit; the +/// builder asserts this against the current device. +/// - If a fragment shader is omitted, no color target is attached and the +/// pipeline can still be used for vertex‑only workloads. pub struct RenderPipelineBuilder { push_constants: Vec, bindings: Vec, diff --git a/crates/lambda-rs/src/render/render_pass.rs b/crates/lambda-rs/src/render/render_pass.rs index 42b6c4ab..f1f7af52 100644 --- a/crates/lambda-rs/src/render/render_pass.rs +++ b/crates/lambda-rs/src/render/render_pass.rs @@ -1,9 +1,16 @@ -//! Render pass builders and definitions for lambda runtimes and applications. +//! Render pass descriptions used to clear and begin drawing. +//! +//! A `RenderPass` captures immutable parameters used when beginning a pass +//! against the swapchain (currently a single color attachment and clear color). +//! The pass is referenced by handle from `RenderCommand::BeginRenderPass`. use super::RenderContext; #[derive(Debug, Clone)] /// Immutable parameters used when beginning a render pass. +/// +/// The pass defines the initial clear for the color attachment and an optional +/// label. Depth/stencil may be added in a future iteration. pub struct RenderPass { clear_color: [f64; 4], label: Option, @@ -23,6 +30,10 @@ impl RenderPass { } /// Builder for a `RenderPass` description. +/// +/// The default pass clears to opaque black. Attach a label and a clear color +/// as needed, then register the pass on a `RenderContext` and reference it by +/// handle in a command stream. pub struct RenderPassBuilder { clear_color: [f64; 4], label: Option, diff --git a/crates/lambda-rs/src/render/scene_math.rs b/crates/lambda-rs/src/render/scene_math.rs index fb8d98b6..9666f083 100644 --- a/crates/lambda-rs/src/render/scene_math.rs +++ b/crates/lambda-rs/src/render/scene_math.rs @@ -141,8 +141,28 @@ pub fn compute_perspective_projection( return conversion.multiply(&projection_gl); } -/// Compute a full model-view-projection matrix given a simple camera, a +/// Compute a full model‑view‑projection matrix given a simple camera, a /// viewport, and the model transform parameters. +/// +/// Example +/// ```rust +/// use lambda::render::scene_math::{SimpleCamera, compute_model_view_projection_matrix}; +/// let camera = SimpleCamera { +/// position: [0.0, 0.0, 3.0], +/// field_of_view_in_turns: 0.25, +/// near_clipping_plane: 0.01, +/// far_clipping_plane: 100.0, +/// }; +/// let mvp = compute_model_view_projection_matrix( +/// &camera, +/// 800, +/// 600, +/// [0.0, 0.0, 0.0], +/// [0.0, 1.0, 0.0], +/// 0.0, +/// 1.0, +/// ); +/// ``` pub fn compute_model_view_projection_matrix( camera: &SimpleCamera, viewport_width: u32, diff --git a/crates/lambda-rs/src/render/shader.rs b/crates/lambda-rs/src/render/shader.rs index 50cfc575..ce46d2fc 100644 --- a/crates/lambda-rs/src/render/shader.rs +++ b/crates/lambda-rs/src/render/shader.rs @@ -1,4 +1,12 @@ -//! A module for compiling shaders into SPIR-V binary. +//! Shader compilation to SPIR‑V modules. +//! +//! Purpose +//! - Provide a reusable `ShaderBuilder` that turns a `VirtualShader` (inline +//! GLSL source or file path + metadata) into a SPIR‑V binary suitable for +//! pipeline creation. +//! +//! Use the platform’s shader backend configured for the workspace (e.g., naga +//! or shaderc) without exposing backend‑specific types in the public API. // Expose the platform shader compiler abstraction pub use lambda_platform::shader::{ @@ -9,6 +17,20 @@ pub use lambda_platform::shader::{ }; /// Reusable compiler for turning virtual shaders into SPIR‑V modules. +/// +/// Example +/// ```rust +/// use lambda_platform::shader::{VirtualShader, ShaderKind}; +/// use lambda::render::shader::ShaderBuilder; +/// let vs = VirtualShader::File { +/// path: "crates/lambda-rs/assets/shaders/triangle.vert".into(), +/// kind: ShaderKind::Vertex, +/// name: "triangle-vert".into(), +/// entry_point: "main".into(), +/// }; +/// let mut builder = ShaderBuilder::new(); +/// let vertex_shader = builder.build(vs); +/// ``` pub struct ShaderBuilder { compiler: ShaderCompiler, } @@ -33,9 +55,7 @@ impl ShaderBuilder { } } -/// A shader that has been compiled into SPIR-V binary. Contains the binary -/// representation of the shader as well as the virtual shader that was used -/// to compile it. +/// A shader compiled into SPIR‑V binary along with its `VirtualShader` source. pub struct Shader { binary: Vec, virtual_shader: VirtualShader, diff --git a/crates/lambda-rs/src/render/vertex.rs b/crates/lambda-rs/src/render/vertex.rs index 66b9bf2a..506ffb00 100644 --- a/crates/lambda-rs/src/render/vertex.rs +++ b/crates/lambda-rs/src/render/vertex.rs @@ -1,4 +1,8 @@ -//! Vertex data structures. +//! Vertex attribute formats and a simple `Vertex` type. +//! +//! Pipelines declare per‑buffer `VertexAttribute`s that map engine vertex +//! data into shader inputs by `location`. This module hosts common color +//! formats and a convenience `Vertex`/`VertexBuilder` used in examples. /// Canonical color/attribute formats used by engine pipelines. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -24,6 +28,9 @@ impl ColorFormat { #[derive(Clone, Copy, Debug)] /// A single vertex element (format + byte offset). +/// +/// Combine one or more elements to form a `VertexAttribute` bound at a shader +/// location. Offsets are in bytes from the start of the vertex and the element. pub struct VertexElement { pub format: ColorFormat, pub offset: u32, @@ -31,6 +38,9 @@ pub struct VertexElement { #[derive(Clone, Copy, Debug)] /// Vertex attribute bound to a shader `location` plus relative offsets. +/// +/// `location` MUST match the shader input. The final attribute byte offset is +/// `offset + element.offset`. pub struct VertexAttribute { pub location: u32, pub offset: u32, diff --git a/crates/lambda-rs/src/render/viewport.rs b/crates/lambda-rs/src/render/viewport.rs index 175a54c3..8343d337 100644 --- a/crates/lambda-rs/src/render/viewport.rs +++ b/crates/lambda-rs/src/render/viewport.rs @@ -1,4 +1,8 @@ -//! Viewport for rendering a frame within the RenderContext. +//! Viewport and scissor state for a render pass. +//! +//! A `Viewport` applies both viewport and scissor rectangles to the active +//! render pass. Coordinates are specified in pixels with origin at the +//! top‑left of the surface. #[derive(Debug, Clone, PartialEq)] /// Viewport/scissor rectangle applied during rendering. @@ -28,7 +32,7 @@ impl Viewport { } } -/// Builder for viewports that are used to render a frame within the RenderContext. +/// Builder for viewports used within a render pass. pub struct ViewportBuilder { x: i32, y: i32, diff --git a/crates/lambda-rs/src/render/window.rs b/crates/lambda-rs/src/render/window.rs index ee5567a8..3f4cfb18 100644 --- a/crates/lambda-rs/src/render/window.rs +++ b/crates/lambda-rs/src/render/window.rs @@ -1,4 +1,8 @@ -//! Window implementation for rendering applications. +//! Window construction and handle wrapper for rendering applications. +//! +//! This module wraps a `winit` window with sizing metadata and provides a +//! builder used by runtimes to create a window before constructing a +//! `RenderContext`. use lambda_platform::winit::{ Loop, From 3b34c26645d6bdb4a23709aef65b54493920b605 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 3 Nov 2025 15:46:38 -0800 Subject: [PATCH 06/11] [add] some polish to the API. --- crates/lambda-rs/src/render/buffer.rs | 6 +++--- crates/lambda-rs/src/render/mesh.rs | 3 --- crates/lambda-rs/src/render/pipeline.rs | 4 ++-- crates/lambda-rs/src/render/scene_math.rs | 23 ++++++++++++----------- crates/lambda-rs/src/render/shader.rs | 14 +++++++++++++- crates/lambda-rs/src/render/window.rs | 1 + 6 files changed, 31 insertions(+), 20 deletions(-) diff --git a/crates/lambda-rs/src/render/buffer.rs b/crates/lambda-rs/src/render/buffer.rs index 6e34630f..f127d646 100644 --- a/crates/lambda-rs/src/render/buffer.rs +++ b/crates/lambda-rs/src/render/buffer.rs @@ -121,7 +121,7 @@ pub struct Buffer { } impl Buffer { - /// Destroy the buffer and all it's resources with the render context that + /// Destroy the buffer and all its resources with the render context that /// created it. Dropping the buffer will release GPU resources. pub fn destroy(self, _render_context: &RenderContext) {} @@ -294,8 +294,8 @@ impl BufferBuilder { let element_size = std::mem::size_of::(); let buffer_length = self.resolve_length(element_size, data.len())?; - // SAFETY: Converting data to bytes is safe because it's underlying - // type, Data, is constrianed to Copy and the lifetime of the slice does + // SAFETY: Converting data to bytes is safe because its underlying + // type, Data, is constrained to Copy and the lifetime of the slice does // not outlive data. let bytes = unsafe { std::slice::from_raw_parts( diff --git a/crates/lambda-rs/src/render/mesh.rs b/crates/lambda-rs/src/render/mesh.rs index 2cb3fce7..1a8025d7 100644 --- a/crates/lambda-rs/src/render/mesh.rs +++ b/crates/lambda-rs/src/render/mesh.rs @@ -43,7 +43,6 @@ impl Mesh { /// Builder for constructing a `Mesh` from vertices and attributes. #[derive(Clone, Debug)] pub struct MeshBuilder { - capacity: usize, vertices: Vec, attributes: Vec, } @@ -52,7 +51,6 @@ impl MeshBuilder { /// Creates a new mesh builder. pub fn new() -> Self { return Self { - capacity: 0, vertices: Vec::new(), attributes: Vec::new(), }; @@ -61,7 +59,6 @@ impl MeshBuilder { /// Allocates memory for the given number of vertices and fills /// the mesh with empty vertices. pub fn with_capacity(&mut self, size: usize) -> &mut Self { - self.capacity = size; self.vertices.resize( size, Vertex { diff --git a/crates/lambda-rs/src/render/pipeline.rs b/crates/lambda-rs/src/render/pipeline.rs index 88dcd156..1c1b1045 100644 --- a/crates/lambda-rs/src/render/pipeline.rs +++ b/crates/lambda-rs/src/render/pipeline.rs @@ -161,13 +161,13 @@ impl RenderPipelineBuilder { // Shader modules let vertex_module = platform_pipeline::ShaderModule::from_spirv( render_context.gpu(), - &vertex_shader.as_binary(), + vertex_shader.binary(), Some("lambda-vertex-shader"), ); let fragment_module = fragment_shader.map(|shader| { platform_pipeline::ShaderModule::from_spirv( render_context.gpu(), - &shader.as_binary(), + shader.binary(), Some("lambda-fragment-shader"), ) }); diff --git a/crates/lambda-rs/src/render/scene_math.rs b/crates/lambda-rs/src/render/scene_math.rs index 9666f083..08d70254 100644 --- a/crates/lambda-rs/src/render/scene_math.rs +++ b/crates/lambda-rs/src/render/scene_math.rs @@ -10,6 +10,17 @@ use crate::math::{ matrix::Matrix, }; +/// Build a 4x4 uniform scaling matrix with `uniform_scale` on the diagonal +/// and `1.0` in the homogeneous component. +fn scaling_matrix(uniform_scale: f32) -> [[f32; 4]; 4] { + return [ + [uniform_scale, 0.0, 0.0, 0.0], + [0.0, uniform_scale, 0.0, 0.0], + [0.0, 0.0, uniform_scale, 0.0], + [0.0, 0.0, 0.0, 1.0], + ]; +} + /// Convert OpenGL-style normalized device coordinates (Z in [-1, 1]) to /// wgpu/Vulkan/Direct3D normalized device coordinates (Z in [0, 1]). /// @@ -48,17 +59,7 @@ pub fn compute_model_matrix( let mut model: [[f32; 4]; 4] = matrix::identity_matrix(4, 4); // Apply rotation first, then scaling via a diagonal matrix, and finally translation. model = matrix::rotate_matrix(model, rotation_axis, angle_in_turns); - - let mut scaled: [[f32; 4]; 4] = [[0.0; 4]; 4]; - for i in 0..4 { - for j in 0..4 { - if i == j { - scaled[i][j] = if i == 3 { 1.0 } else { uniform_scale }; - } else { - scaled[i][j] = 0.0; - } - } - } + let scaled = scaling_matrix(uniform_scale); model = model.multiply(&scaled); let translation_matrix: [[f32; 4]; 4] = diff --git a/crates/lambda-rs/src/render/shader.rs b/crates/lambda-rs/src/render/shader.rs index ce46d2fc..29ab7dcf 100644 --- a/crates/lambda-rs/src/render/shader.rs +++ b/crates/lambda-rs/src/render/shader.rs @@ -62,7 +62,19 @@ pub struct Shader { } impl Shader { - /// Returns a copy of the SPIR-V binary representation of the shader. + /// Borrow the SPIR‑V binary representation of the shader as a word slice. + /// + /// Prefer this accessor to avoid unnecessary allocations when passing the + /// shader to pipeline builders. Use `as_binary` when an owned buffer is + /// explicitly required. + pub fn binary(&self) -> &[u32] { + return &self.binary; + } + + /// Returns a copy of the SPIR‑V binary representation of the shader. + /// + /// Retained for compatibility with existing code that expects an owned + /// `Vec`. Prefer `binary()` for zero‑copy borrowing. pub fn as_binary(&self) -> Vec { return self.binary.clone(); } diff --git a/crates/lambda-rs/src/render/window.rs b/crates/lambda-rs/src/render/window.rs index 3f4cfb18..c0e37dbf 100644 --- a/crates/lambda-rs/src/render/window.rs +++ b/crates/lambda-rs/src/render/window.rs @@ -52,6 +52,7 @@ impl WindowBuilder { /// surface in `RenderContextBuilder`. This flag is reserved to influence /// that choice and is currently a no‑op. pub fn with_vsync(mut self, vsync: bool) -> Self { + self.vsync = vsync; return self; } From c0879ef8ea28e55203080316876df9d77cade98e Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 3 Nov 2025 15:52:33 -0800 Subject: [PATCH 07/11] [update] render context to not panic. --- crates/lambda-rs/src/render/bind.rs | 10 ++- crates/lambda-rs/src/render/mod.rs | 72 +++++++++++++++++--- crates/lambda-rs/src/render/pipeline.rs | 9 ++- crates/lambda-rs/src/runtimes/application.rs | 9 ++- 4 files changed, 86 insertions(+), 14 deletions(-) diff --git a/crates/lambda-rs/src/render/bind.rs b/crates/lambda-rs/src/render/bind.rs index 03ff6571..2f238c96 100644 --- a/crates/lambda-rs/src/render/bind.rs +++ b/crates/lambda-rs/src/render/bind.rs @@ -278,7 +278,15 @@ impl<'a> BindGroupBuilder<'a> { for (binding, buffer, offset, size) in self.entries.into_iter() { if let Some(sz) = size { - assert!( + if sz.get() > max_binding { + logging::error!( + "Uniform binding at binding={} requests size={} > device limit {}", + binding, + sz.get(), + max_binding + ); + } + debug_assert!( sz.get() <= max_binding, "Uniform binding at binding={} requests size={} > device limit {}", binding, diff --git a/crates/lambda-rs/src/render/mod.rs b/crates/lambda-rs/src/render/mod.rs index 9bf54104..7e456304 100644 --- a/crates/lambda-rs/src/render/mod.rs +++ b/crates/lambda-rs/src/render/mod.rs @@ -82,6 +82,8 @@ use self::{ /// to that window. pub struct RenderContextBuilder { name: String, + /// Reserved for future timeout handling during rendering (nanoseconds). + /// Not currently enforced; kept for forward compatibility with runtime controls. _render_timeout: u64, } @@ -102,7 +104,13 @@ impl RenderContextBuilder { /// Build a `RenderContext` for the provided `window` and configure the /// presentation surface. - pub fn build(self, window: &window::Window) -> RenderContext { + /// + /// Errors are returned instead of panicking to allow callers to surface + /// actionable initialization failures. + pub fn build( + self, + window: &window::Window, + ) -> Result { let RenderContextBuilder { name, .. } = self; let instance = InstanceBuilder::new() @@ -112,12 +120,22 @@ impl RenderContextBuilder { let mut surface = SurfaceBuilder::new() .with_label(&format!("{} Surface", name)) .build(&instance, window.window_handle()) - .expect("Failed to create rendering surface"); + .map_err(|e| { + RenderContextError::SurfaceCreate(format!( + "Failed to create rendering surface: {:?}", + e + )) + })?; let gpu = GpuBuilder::new() .with_label(&format!("{} Device", name)) .build(&instance, Some(&surface)) - .expect("Failed to create GPU device"); + .map_err(|e| { + RenderContextError::GpuCreate(format!( + "Failed to create GPU device: {:?}", + e + )) + })?; let size = window.dimensions(); surface @@ -127,16 +145,22 @@ impl RenderContextBuilder { lambda_platform::wgpu::PresentMode::Fifo, lambda_platform::wgpu::TextureUsages::RENDER_ATTACHMENT, ) - .expect("Failed to configure surface"); - - let config = surface - .configuration() - .cloned() - .expect("Surface was not configured"); + .map_err(|e| { + RenderContextError::SurfaceConfig(format!( + "Failed to configure surface: {:?}", + e + )) + })?; + + let config = surface.configuration().cloned().ok_or_else(|| { + RenderContextError::SurfaceConfig( + "Surface was not configured".to_string(), + ) + })?; let present_mode = config.present_mode; let texture_usage = config.usage; - return RenderContext { + return Ok(RenderContext { label: name, instance, surface, @@ -150,7 +174,7 @@ impl RenderContextBuilder { bind_group_layouts: vec![], bind_groups: vec![], buffers: vec![], - }; + }); } } @@ -547,3 +571,29 @@ impl From for RenderError { return RenderError::Surface(error); } } + +#[derive(Debug)] +/// Errors encountered while creating a `RenderContext`. +/// +/// Returned by `RenderContextBuilder::build` to avoid panics during +/// initialization and provide actionable error messages to callers. +pub enum RenderContextError { + /// Failure creating the presentation surface for the provided window. + SurfaceCreate(String), + /// Failure creating the logical GPU device/queue. + GpuCreate(String), + /// Failure configuring or retrieving the surface configuration. + SurfaceConfig(String), +} + +impl core::fmt::Display for RenderContextError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + RenderContextError::SurfaceCreate(s) => write!(f, "{}", s), + RenderContextError::GpuCreate(s) => write!(f, "{}", s), + RenderContextError::SurfaceConfig(s) => write!(f, "{}", s), + } + } +} + +impl std::error::Error for RenderContextError {} diff --git a/crates/lambda-rs/src/render/pipeline.rs b/crates/lambda-rs/src/render/pipeline.rs index 1c1b1045..ecc66515 100644 --- a/crates/lambda-rs/src/render/pipeline.rs +++ b/crates/lambda-rs/src/render/pipeline.rs @@ -184,7 +184,14 @@ impl RenderPipelineBuilder { // Bind group layouts limit check let max_bind_groups = render_context.limit_max_bind_groups() as usize; - assert!( + if self.bind_group_layouts.len() > max_bind_groups { + logging::error!( + "Pipeline declares {} bind group layouts, exceeds device max {}", + self.bind_group_layouts.len(), + max_bind_groups + ); + } + debug_assert!( self.bind_group_layouts.len() <= max_bind_groups, "Pipeline declares {} bind group layouts, exceeds device max {}", self.bind_group_layouts.len(), diff --git a/crates/lambda-rs/src/runtimes/application.rs b/crates/lambda-rs/src/runtimes/application.rs index 4cf2b51d..3eea9dbf 100644 --- a/crates/lambda-rs/src/runtimes/application.rs +++ b/crates/lambda-rs/src/runtimes/application.rs @@ -149,7 +149,14 @@ impl Runtime<(), String> for ApplicationRuntime { let mut event_loop = LoopBuilder::new().build(); let window = self.window_builder.build(&mut event_loop); let mut component_stack = self.component_stack; - let mut render_context = self.render_context_builder.build(&window); + let mut render_context = match self.render_context_builder.build(&window) { + Ok(ctx) => ctx, + Err(err) => { + let msg = format!("Failed to initialize render context: {}", err); + logging::error!("{}", msg); + return Err(msg); + } + }; let mut active_render_context = Some(render_context); let publisher = event_loop.create_event_publisher(); From df6c1028861d76c60814901384c7e0429aa8a57d Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 3 Nov 2025 17:46:25 -0800 Subject: [PATCH 08/11] [update] doc examples to be ignored. --- crates/lambda-rs/src/render/bind.rs | 4 ++-- crates/lambda-rs/src/render/buffer.rs | 4 ++-- crates/lambda-rs/src/render/pipeline.rs | 2 +- crates/lambda-rs/src/render/shader.rs | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/lambda-rs/src/render/bind.rs b/crates/lambda-rs/src/render/bind.rs index 2f238c96..f4ed1418 100644 --- a/crates/lambda-rs/src/render/bind.rs +++ b/crates/lambda-rs/src/render/bind.rs @@ -116,7 +116,7 @@ impl BindGroup { /// Builder for creating a bind group layout with uniform buffer bindings. /// /// Example -/// ```rust +/// ```rust,ignore /// // One static camera UBO at binding 0 and one dynamic model UBO at 1. /// // Visible in both vertex and fragment stages. /// use lambda::render::bind::{BindGroupLayoutBuilder, BindingVisibility}; @@ -211,7 +211,7 @@ impl BindGroupLayoutBuilder { /// Builder for creating a bind group for a previously built layout. /// /// Example -/// ```rust +/// ```rust,ignore /// // Assume `camera_ubo` and `model_ubo` are created `UniformBuffer`. /// // Bind them to match the layout: camera at 0, model at 1 with dynamic offset. /// use lambda::render::bind::BindGroupBuilder; diff --git a/crates/lambda-rs/src/render/buffer.rs b/crates/lambda-rs/src/render/buffer.rs index f127d646..7ea5fcb1 100644 --- a/crates/lambda-rs/src/render/buffer.rs +++ b/crates/lambda-rs/src/render/buffer.rs @@ -165,7 +165,7 @@ impl Buffer { /// is CPU‑visible by default for direct queue writes. /// /// Example -/// ```rust +/// ```rust,ignore /// // Model‑view‑projection updated every frame /// #[repr(C)] /// #[derive(Clone, Copy)] @@ -223,7 +223,7 @@ impl UniformBuffer { /// visibility is requested. /// /// Example (vertex buffer) -/// ```rust +/// ```rust,ignore /// use lambda::render::buffer::{BufferBuilder, Usage, Properties, BufferType}; /// let vertices: Vec = build_vertices(); /// let vb = BufferBuilder::new() diff --git a/crates/lambda-rs/src/render/pipeline.rs b/crates/lambda-rs/src/render/pipeline.rs index ecc66515..cfcfeac9 100644 --- a/crates/lambda-rs/src/render/pipeline.rs +++ b/crates/lambda-rs/src/render/pipeline.rs @@ -14,7 +14,7 @@ //! - Supply a vertex shader and optional fragment shader compiled to SPIR‑V. //! //! Example -//! ```rust +//! ```rust,ignore //! // Single vertex buffer with position/color; one push constant range for the vertex stage //! use lambda::render::pipeline::{RenderPipelineBuilder, PipelineStage, CullingMode}; //! let pipeline = RenderPipelineBuilder::new() diff --git a/crates/lambda-rs/src/render/shader.rs b/crates/lambda-rs/src/render/shader.rs index 29ab7dcf..57e1e63b 100644 --- a/crates/lambda-rs/src/render/shader.rs +++ b/crates/lambda-rs/src/render/shader.rs @@ -19,7 +19,7 @@ pub use lambda_platform::shader::{ /// Reusable compiler for turning virtual shaders into SPIR‑V modules. /// /// Example -/// ```rust +/// ```rust,no_run /// use lambda_platform::shader::{VirtualShader, ShaderKind}; /// use lambda::render::shader::ShaderBuilder; /// let vs = VirtualShader::File { From 484b1829d014b0bd67c4907bde8450e430da6a1f Mon Sep 17 00:00:00 2001 From: vmarcella Date: Tue, 4 Nov 2025 19:44:27 -0800 Subject: [PATCH 09/11] [refactor] instance, gpu, and render passes. --- crates/lambda-rs-platform/src/wgpu/bind.rs | 2 +- crates/lambda-rs-platform/src/wgpu/buffer.rs | 2 +- crates/lambda-rs-platform/src/wgpu/command.rs | 50 +++ crates/lambda-rs-platform/src/wgpu/gpu.rs | 5 +- .../lambda-rs-platform/src/wgpu/instance.rs | 216 ++++++++++++ crates/lambda-rs-platform/src/wgpu/mod.rs | 310 +----------------- .../lambda-rs-platform/src/wgpu/pipeline.rs | 2 +- .../src/wgpu/render_pass.rs | 302 +++++++++++++++++ crates/lambda-rs-platform/src/wgpu/surface.rs | 2 +- crates/lambda-rs/src/render/mod.rs | 97 +++--- crates/lambda-rs/src/render/render_pass.rs | 70 ++++ 11 files changed, 707 insertions(+), 351 deletions(-) create mode 100644 crates/lambda-rs-platform/src/wgpu/command.rs create mode 100644 crates/lambda-rs-platform/src/wgpu/instance.rs create mode 100644 crates/lambda-rs-platform/src/wgpu/render_pass.rs diff --git a/crates/lambda-rs-platform/src/wgpu/bind.rs b/crates/lambda-rs-platform/src/wgpu/bind.rs index 184e74c9..318750b2 100644 --- a/crates/lambda-rs-platform/src/wgpu/bind.rs +++ b/crates/lambda-rs-platform/src/wgpu/bind.rs @@ -10,7 +10,7 @@ use wgpu; use crate::wgpu::{ buffer, - Gpu, + gpu::Gpu, }; #[derive(Debug)] diff --git a/crates/lambda-rs-platform/src/wgpu/buffer.rs b/crates/lambda-rs-platform/src/wgpu/buffer.rs index 3b3c5a10..97bdc050 100644 --- a/crates/lambda-rs-platform/src/wgpu/buffer.rs +++ b/crates/lambda-rs-platform/src/wgpu/buffer.rs @@ -8,7 +8,7 @@ use wgpu::{ util::DeviceExt, }; -use crate::wgpu::Gpu; +use crate::wgpu::gpu::Gpu; #[derive(Clone, Copy, Debug, PartialEq, Eq)] /// Index format for indexed drawing. diff --git a/crates/lambda-rs-platform/src/wgpu/command.rs b/crates/lambda-rs-platform/src/wgpu/command.rs new file mode 100644 index 00000000..f715a3fe --- /dev/null +++ b/crates/lambda-rs-platform/src/wgpu/command.rs @@ -0,0 +1,50 @@ +//! Command encoding abstractions around `wgpu::CommandEncoder`, `wgpu::RenderPass`, +//! and `wgpu::CommandBuffer` that expose only the operations needed by the +//! engine while keeping raw `wgpu` types crate-internal. + +use super::gpu; + +#[derive(Debug)] +/// Thin wrapper around `wgpu::CommandEncoder` with convenience helpers. +pub struct CommandEncoder { + raw: wgpu::CommandEncoder, +} + +#[derive(Debug)] +/// Wrapper around `wgpu::CommandBuffer` to avoid exposing raw types upstream. +pub struct CommandBuffer { + raw: wgpu::CommandBuffer, +} + +impl CommandBuffer { + pub(crate) fn into_raw(self) -> wgpu::CommandBuffer { + self.raw + } +} + +impl CommandEncoder { + /// Create a new command encoder with an optional label. + pub fn new(gpu: &gpu::Gpu, label: Option<&str>) -> Self { + let raw = gpu + .device() + .create_command_encoder(&wgpu::CommandEncoderDescriptor { label }); + return Self { raw }; + } + + /// Internal helper for beginning a render pass. Used by the render pass builder. + pub(crate) fn begin_render_pass_raw<'view>( + &'view mut self, + desc: &wgpu::RenderPassDescriptor<'view>, + ) -> wgpu::RenderPass<'view> { + return self.raw.begin_render_pass(desc); + } + + /// Finish recording and return the command buffer. + pub fn finish(self) -> CommandBuffer { + return CommandBuffer { + raw: self.raw.finish(), + }; + } +} + +// RenderPass wrapper and its methods now live under `wgpu::render_pass`. diff --git a/crates/lambda-rs-platform/src/wgpu/gpu.rs b/crates/lambda-rs-platform/src/wgpu/gpu.rs index 9a8702f0..b36550e2 100644 --- a/crates/lambda-rs-platform/src/wgpu/gpu.rs +++ b/crates/lambda-rs-platform/src/wgpu/gpu.rs @@ -1,8 +1,9 @@ use pollster::block_on; use super::{ + command::CommandBuffer, + instance::Instance, surface::Surface, - Instance, }; #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -239,7 +240,7 @@ impl Gpu { /// Submit one or more command buffers to the device queue. pub fn submit(&self, list: I) where - I: IntoIterator, + I: IntoIterator, { let iter = list.into_iter().map(|cb| cb.into_raw()); self.queue.submit(iter); diff --git a/crates/lambda-rs-platform/src/wgpu/instance.rs b/crates/lambda-rs-platform/src/wgpu/instance.rs new file mode 100644 index 00000000..0550d388 --- /dev/null +++ b/crates/lambda-rs-platform/src/wgpu/instance.rs @@ -0,0 +1,216 @@ +//! Instance abstractions and type wrappers to avoid leaking raw `wgpu` types +//! at higher layers. This keeps the platform crate free to evolve with `wgpu` +//! while presenting a stable surface to the engine. + +use pollster::block_on; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +/// Wrapper over `wgpu::Backends` as a bitset. +pub struct Backends(wgpu::Backends); + +impl Backends { + /// Primary desktop backends (Vulkan/Metal/DX12) per `wgpu` defaults. + pub const PRIMARY: Backends = Backends(wgpu::Backends::PRIMARY); + /// Vulkan backend. + pub const VULKAN: Backends = Backends(wgpu::Backends::VULKAN); + /// Metal backend (macOS/iOS). + pub const METAL: Backends = Backends(wgpu::Backends::METAL); + /// DirectX 12 backend (Windows). + pub const DX12: Backends = Backends(wgpu::Backends::DX12); + /// OpenGL / WebGL backend. + pub const GL: Backends = Backends(wgpu::Backends::GL); + /// Browser WebGPU backend. + pub const BROWSER_WEBGPU: Backends = Backends(wgpu::Backends::BROWSER_WEBGPU); + + pub(crate) fn to_wgpu(self) -> wgpu::Backends { + self.0 + } +} + +impl Default for Backends { + fn default() -> Self { + return Backends::PRIMARY; + } +} + +impl std::ops::BitOr for Backends { + type Output = Backends; + fn bitor(self, rhs: Backends) -> Backends { + return Backends(self.0 | rhs.0); + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +/// Wrapper over `wgpu::InstanceFlags` as a bitset. +pub struct InstanceFlags(wgpu::InstanceFlags); + +impl InstanceFlags { + /// Validation flags (debugging and validation). + pub const VALIDATION: InstanceFlags = + InstanceFlags(wgpu::InstanceFlags::VALIDATION); + /// Enable additional debugging features where available. + pub const DEBUG: InstanceFlags = InstanceFlags(wgpu::InstanceFlags::DEBUG); + + pub(crate) fn to_wgpu(self) -> wgpu::InstanceFlags { + self.0 + } +} + +impl Default for InstanceFlags { + fn default() -> Self { + return InstanceFlags(wgpu::InstanceFlags::default()); + } +} + +impl std::ops::BitOr for InstanceFlags { + type Output = InstanceFlags; + fn bitor(self, rhs: InstanceFlags) -> InstanceFlags { + return InstanceFlags(self.0 | rhs.0); + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +/// Which DX12 shader compiler to use on Windows platforms. +pub enum Dx12Compiler { + Fxc, +} + +impl Dx12Compiler { + pub(crate) fn to_wgpu(self) -> wgpu::Dx12Compiler { + return match self { + Dx12Compiler::Fxc => wgpu::Dx12Compiler::Fxc, + }; + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +/// OpenGL ES 3 minor version (used by GL/Web targets). +pub enum Gles3MinorVersion { + Automatic, + Version0, + Version1, + Version2, +} + +impl Gles3MinorVersion { + pub(crate) fn to_wgpu(self) -> wgpu::Gles3MinorVersion { + return match self { + Gles3MinorVersion::Automatic => wgpu::Gles3MinorVersion::Automatic, + Gles3MinorVersion::Version0 => wgpu::Gles3MinorVersion::Version0, + Gles3MinorVersion::Version1 => wgpu::Gles3MinorVersion::Version1, + Gles3MinorVersion::Version2 => wgpu::Gles3MinorVersion::Version2, + }; + } +} + +#[derive(Debug, Clone)] +/// Builder for creating a `wgpu::Instance` with consistent defaults. +/// +/// Defaults to primary backends and no special flags. Options map to +/// `wgpu::InstanceDescriptor` internally without leaking raw types. +pub struct InstanceBuilder { + label: Option, + backends: Backends, + flags: InstanceFlags, + // Keep backend options/memory thresholds internal; expose focused knobs via + // typed methods to avoid leaking raw structs. + backend_options: wgpu::BackendOptions, + memory_budget_thresholds: wgpu::MemoryBudgetThresholds, +} + +impl InstanceBuilder { + /// Construct a new builder with Lambda defaults. + pub fn new() -> Self { + Self { + label: None, + backends: Backends::PRIMARY, + flags: InstanceFlags::default(), + backend_options: wgpu::BackendOptions::default(), + memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(), + } + } + + /// Attach a debug label to the instance. + pub fn with_label(mut self, label: &str) -> Self { + self.label = Some(label.to_string()); + self + } + + /// Select which graphics backends to enable. + pub fn with_backends(mut self, backends: Backends) -> Self { + self.backends = backends; + self + } + + /// Set additional instance flags (e.g., debugging/validation). + pub fn with_flags(mut self, flags: InstanceFlags) -> Self { + self.flags = flags; + self + } + + /// Choose a DX12 shader compiler variant when on Windows. + pub fn with_dx12_shader_compiler(mut self, compiler: Dx12Compiler) -> Self { + self.backend_options.dx12.shader_compiler = compiler.to_wgpu(); + self + } + + /// Configure the GLES minor version for WebGL/OpenGL ES targets. + pub fn with_gles_minor_version(mut self, version: Gles3MinorVersion) -> Self { + self.backend_options.gl.gles_minor_version = version.to_wgpu(); + self + } + + /// Build the `Instance` wrapper from the accumulated options. + pub fn build(self) -> Instance { + let descriptor = wgpu::InstanceDescriptor { + backends: self.backends.to_wgpu(), + flags: self.flags.to_wgpu(), + memory_budget_thresholds: self.memory_budget_thresholds, + backend_options: self.backend_options, + }; + + Instance { + label: self.label, + instance: wgpu::Instance::new(&descriptor), + } + } +} + +#[derive(Debug)] +/// Thin wrapper over `wgpu::Instance` that preserves a user label and exposes +/// a blocking `request_adapter` convenience. +pub struct Instance { + pub(crate) label: Option, + pub(crate) instance: wgpu::Instance, +} + +impl Instance { + /// Borrow the underlying `wgpu::Instance`. + pub fn raw(&self) -> &wgpu::Instance { + &self.instance + } + + /// Return the optional label attached at construction time. + pub fn label(&self) -> Option<&str> { + self.label.as_deref() + } + + /// Request a compatible GPU adapter synchronously. + pub(crate) fn request_adapter<'surface, 'window>( + &self, + options: &wgpu::RequestAdapterOptions<'surface, 'window>, + ) -> Result { + block_on(self.instance.request_adapter(options)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn instance_builder_sets_label() { + let instance = InstanceBuilder::new().with_label("Test").build(); + assert_eq!(instance.label(), Some("Test")); + } +} diff --git a/crates/lambda-rs-platform/src/wgpu/mod.rs b/crates/lambda-rs-platform/src/wgpu/mod.rs index e460b144..7213041c 100644 --- a/crates/lambda-rs-platform/src/wgpu/mod.rs +++ b/crates/lambda-rs-platform/src/wgpu/mod.rs @@ -6,316 +6,14 @@ //! defaults and narrow the surface area used by Lambda, without hiding //! important handles when you need to drop down to raw `wgpu`. -use pollster::block_on; +// keep this module focused on exports and submodules pub mod bind; pub mod buffer; +pub mod command; pub mod gpu; +pub mod instance; pub mod pipeline; +pub mod render_pass; pub mod surface; pub mod vertex; - -pub use gpu::{ - Features, - Gpu, - GpuBuildError, - GpuBuilder, - MemoryHints, - PowerPreference, -}; -pub use surface::{ - Frame, - PresentMode, - Surface, - SurfaceBuilder, - SurfaceConfig, - SurfaceError, - SurfaceFormat, - TextureUsages, -}; - -#[derive(Debug, Clone)] -/// Builder for creating a `wgpu::Instance` with consistent defaults. -/// -/// - Defaults to primary backends and no special flags. -/// - All options map 1:1 to the underlying `wgpu::InstanceDescriptor`. -pub struct InstanceBuilder { - label: Option, - backends: wgpu::Backends, - flags: wgpu::InstanceFlags, - backend_options: wgpu::BackendOptions, - memory_budget_thresholds: wgpu::MemoryBudgetThresholds, -} - -impl InstanceBuilder { - /// Construct a new builder with Lambda defaults. - pub fn new() -> Self { - Self { - label: None, - backends: wgpu::Backends::PRIMARY, - flags: wgpu::InstanceFlags::default(), - backend_options: wgpu::BackendOptions::default(), - memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(), - } - } - - /// Attach a debug label to the instance. - pub fn with_label(mut self, label: &str) -> Self { - self.label = Some(label.to_string()); - self - } - - /// Select which graphics backends to enable. - pub fn with_backends(mut self, backends: wgpu::Backends) -> Self { - self.backends = backends; - self - } - - /// Set additional instance flags (e.g., debugging). - pub fn with_flags(mut self, flags: wgpu::InstanceFlags) -> Self { - self.flags = flags; - self - } - - /// Choose a DX12 shader compiler variant when on Windows. - pub fn with_dx12_shader_compiler( - mut self, - compiler: wgpu::Dx12Compiler, - ) -> Self { - self.backend_options.dx12.shader_compiler = compiler; - self - } - - /// Configure the GLES minor version for WebGL/OpenGL ES targets. - pub fn with_gles_minor_version( - mut self, - version: wgpu::Gles3MinorVersion, - ) -> Self { - self.backend_options.gl.gles_minor_version = version; - self - } - - /// Build the `Instance` wrapper from the accumulated options. - pub fn build(self) -> Instance { - let descriptor = wgpu::InstanceDescriptor { - backends: self.backends, - flags: self.flags, - memory_budget_thresholds: self.memory_budget_thresholds, - backend_options: self.backend_options, - }; - - Instance { - label: self.label, - instance: wgpu::Instance::new(&descriptor), - } - } -} - -#[derive(Debug)] -/// Thin wrapper over `wgpu::Instance` that preserves a user label and exposes -/// a blocking `request_adapter` convenience. -pub struct Instance { - label: Option, - instance: wgpu::Instance, -} - -impl Instance { - /// Borrow the underlying `wgpu::Instance`. - pub fn raw(&self) -> &wgpu::Instance { - &self.instance - } - - /// Return the optional label attached at construction time. - pub fn label(&self) -> Option<&str> { - self.label.as_deref() - } - - /// Request a compatible GPU adapter synchronously. - /// - /// This simply blocks on `wgpu::Instance::request_adapter` and returns - /// `None` if no suitable adapter is found. - pub(crate) fn request_adapter<'surface, 'window>( - &self, - options: &wgpu::RequestAdapterOptions<'surface, 'window>, - ) -> Result { - block_on(self.instance.request_adapter(options)) - } -} - -// ---------------------- Command Encoding Abstractions ----------------------- - -#[derive(Debug)] -/// Thin wrapper around `wgpu::CommandEncoder` with convenience helpers. -pub struct CommandEncoder { - raw: wgpu::CommandEncoder, -} - -#[derive(Debug)] -/// Wrapper around `wgpu::CommandBuffer` to avoid exposing raw types upstream. -pub struct CommandBuffer { - raw: wgpu::CommandBuffer, -} - -impl CommandBuffer { - pub(crate) fn into_raw(self) -> wgpu::CommandBuffer { - self.raw - } -} - -impl CommandEncoder { - /// Create a new command encoder with an optional label. - pub fn new(gpu: &gpu::Gpu, label: Option<&str>) -> Self { - let raw = gpu - .device() - .create_command_encoder(&wgpu::CommandEncoderDescriptor { label }); - return Self { raw }; - } - - /// Begin a render pass targeting a single color attachment with the provided - /// load/store operations. Depth/stencil is not attached by this helper. - pub(crate) fn begin_render_pass<'view>( - &'view mut self, - label: Option<&str>, - view: &'view surface::TextureViewRef<'view>, - ops: wgpu::Operations, - ) -> RenderPass<'view> { - let color_attachment = wgpu::RenderPassColorAttachment { - view: view.raw, - resolve_target: None, - depth_slice: None, - ops, - }; - let color_attachments = [Some(color_attachment)]; - let pass = self.raw.begin_render_pass(&wgpu::RenderPassDescriptor { - label, - color_attachments: &color_attachments, - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }); - return RenderPass { raw: pass }; - } - - /// Begin a render pass that clears the color attachment to the provided RGBA color. - /// - /// This helper avoids exposing raw `wgpu` color/operation types to higher layers. - pub fn begin_render_pass_clear<'view>( - &'view mut self, - label: Option<&str>, - view: &'view surface::TextureViewRef<'view>, - color: [f64; 4], - ) -> RenderPass<'view> { - let ops = wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color { - r: color[0], - g: color[1], - b: color[2], - a: color[3], - }), - store: wgpu::StoreOp::Store, - }; - return self.begin_render_pass(label, view, ops); - } - - /// Finish recording and return the command buffer. - pub fn finish(self) -> CommandBuffer { - return CommandBuffer { - raw: self.raw.finish(), - }; - } -} - -#[derive(Debug)] -/// Wrapper around `wgpu::RenderPass<'_>` exposing the operations needed by the -/// Lambda renderer without leaking raw `wgpu` types at the call sites. -pub struct RenderPass<'a> { - raw: wgpu::RenderPass<'a>, -} - -impl<'a> RenderPass<'a> { - /// Set the active render pipeline. - pub fn set_pipeline(&mut self, pipeline: &pipeline::RenderPipeline) { - self.raw.set_pipeline(pipeline.raw()); - } - - /// Apply viewport state. - pub fn set_viewport( - &mut self, - x: f32, - y: f32, - width: f32, - height: f32, - min_depth: f32, - max_depth: f32, - ) { - self - .raw - .set_viewport(x, y, width, height, min_depth, max_depth); - } - - /// Apply scissor rectangle. - pub fn set_scissor_rect(&mut self, x: u32, y: u32, width: u32, height: u32) { - self.raw.set_scissor_rect(x, y, width, height); - } - - /// Bind a group with optional dynamic offsets. - pub fn set_bind_group( - &mut self, - set: u32, - group: &bind::BindGroup, - dynamic_offsets: &[u32], - ) { - self.raw.set_bind_group(set, group.raw(), dynamic_offsets); - } - - /// Bind a vertex buffer slot. - pub fn set_vertex_buffer(&mut self, slot: u32, buffer: &buffer::Buffer) { - self.raw.set_vertex_buffer(slot, buffer.raw().slice(..)); - } - - /// Bind an index buffer with the provided index format. - pub fn set_index_buffer( - &mut self, - buffer: &buffer::Buffer, - format: buffer::IndexFormat, - ) { - self - .raw - .set_index_buffer(buffer.raw().slice(..), format.to_wgpu()); - } - - /// Upload push constants. - pub fn set_push_constants( - &mut self, - stages: pipeline::PipelineStage, - offset: u32, - data: &[u8], - ) { - self.raw.set_push_constants(stages.to_wgpu(), offset, data); - } - - /// Issue a non-indexed draw over a vertex range. - pub fn draw(&mut self, vertices: std::ops::Range) { - self.raw.draw(vertices, 0..1); - } - - /// Issue an indexed draw with a base vertex applied. - pub fn draw_indexed( - &mut self, - indices: std::ops::Range, - base_vertex: i32, - ) { - self.raw.draw_indexed(indices, base_vertex, 0..1); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn instance_builder_sets_label() { - let instance = InstanceBuilder::new().with_label("Test").build(); - assert_eq!(instance.label(), Some("Test")); - } -} diff --git a/crates/lambda-rs-platform/src/wgpu/pipeline.rs b/crates/lambda-rs-platform/src/wgpu/pipeline.rs index 11ef2ff1..1574f84d 100644 --- a/crates/lambda-rs-platform/src/wgpu/pipeline.rs +++ b/crates/lambda-rs-platform/src/wgpu/pipeline.rs @@ -6,9 +6,9 @@ use wgpu; use crate::wgpu::{ bind, + gpu::Gpu, surface::SurfaceFormat, vertex::ColorFormat, - Gpu, }; #[derive(Clone, Copy, Debug)] diff --git a/crates/lambda-rs-platform/src/wgpu/render_pass.rs b/crates/lambda-rs-platform/src/wgpu/render_pass.rs new file mode 100644 index 00000000..f34d0aaf --- /dev/null +++ b/crates/lambda-rs-platform/src/wgpu/render_pass.rs @@ -0,0 +1,302 @@ +//! Render pass wrapper and builder for starting a pass on a command encoder. +//! +//! This module provides a small builder to describe the pass setup (label and +//! clear color) and a thin `RenderPass` wrapper around `wgpu::RenderPass<'_>`. +//! +//! Building a render pass implicitly begins the pass on the provided +//! `CommandEncoder` and texture view. The returned `RenderPass` borrows the +//! encoder and remains valid until dropped. + +use wgpu::{ + self, + RenderPassColorAttachment, +}; + +use super::{ + bind, + buffer, + command, + pipeline, + surface, +}; + +#[derive(Clone, Copy, Debug, PartialEq)] +/// Color load operation for a render pass color attachment. +pub enum ColorLoadOp { + /// Load the existing contents of the attachment. + Load, + /// Clear the attachment to the provided RGBA color. + Clear([f64; 4]), +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +/// Store operation for a render pass attachment. +pub enum StoreOp { + /// Store the results to the attachment at the end of the pass. + Store, + /// Discard the results at the end of the pass when possible. + Discard, +} + +#[derive(Clone, Copy, Debug)] +/// Combined load and store operations for the color attachment. +pub struct ColorOperations { + pub load: ColorLoadOp, + pub store: StoreOp, +} + +impl Default for ColorOperations { + fn default() -> Self { + return Self { + load: ColorLoadOp::Clear([0.0, 0.0, 0.0, 1.0]), + store: StoreOp::Store, + }; + } +} + +#[derive(Clone, Debug, Default)] +/// Configuration for beginning a render pass. +pub struct RenderPassConfig { + pub label: Option, + pub color_operations: ColorOperations, +} + +#[derive(Debug)] +/// Wrapper around `wgpu::RenderPass<'_>` exposing the operations needed by the +/// engine without leaking raw `wgpu` types at the call sites. +pub struct RenderPass<'a> { + pub(super) raw: wgpu::RenderPass<'a>, +} + +#[derive(Debug)] +struct RenderPassKeepAlive<'a> { + color_attachments: [Option>; 1], + label: Option, +} + +impl<'a> RenderPass<'a> { + /// Set the active render pipeline. + pub fn set_pipeline(&mut self, pipeline: &pipeline::RenderPipeline) { + self.raw.set_pipeline(pipeline.raw()); + } + + /// Apply viewport state. + pub fn set_viewport( + &mut self, + x: f32, + y: f32, + width: f32, + height: f32, + min_depth: f32, + max_depth: f32, + ) { + self + .raw + .set_viewport(x, y, width, height, min_depth, max_depth); + } + + /// Apply scissor rectangle. + pub fn set_scissor_rect(&mut self, x: u32, y: u32, width: u32, height: u32) { + self.raw.set_scissor_rect(x, y, width, height); + } + + /// Bind a group with optional dynamic offsets. + pub fn set_bind_group( + &mut self, + set: u32, + group: &bind::BindGroup, + dynamic_offsets: &[u32], + ) { + self.raw.set_bind_group(set, group.raw(), dynamic_offsets); + } + + /// Bind a vertex buffer slot. + pub fn set_vertex_buffer(&mut self, slot: u32, buffer: &buffer::Buffer) { + self.raw.set_vertex_buffer(slot, buffer.raw().slice(..)); + } + + /// Bind an index buffer with the provided index format. + pub fn set_index_buffer( + &mut self, + buffer: &buffer::Buffer, + format: buffer::IndexFormat, + ) { + self + .raw + .set_index_buffer(buffer.raw().slice(..), format.to_wgpu()); + } + + /// Upload push constants. + pub fn set_push_constants( + &mut self, + stages: pipeline::PipelineStage, + offset: u32, + data: &[u8], + ) { + self.raw.set_push_constants(stages.to_wgpu(), offset, data); + } + + /// Issue a non-indexed draw over a vertex range. + pub fn draw(&mut self, vertices: std::ops::Range) { + self.raw.draw(vertices, 0..1); + } + + /// Issue an indexed draw with a base vertex applied. + pub fn draw_indexed( + &mut self, + indices: std::ops::Range, + base_vertex: i32, + ) { + self.raw.draw_indexed(indices, base_vertex, 0..1); + } +} + +#[derive(Debug, Default)] +/// Wrapper for a variably sized list of color attachments passed into a render +/// pass. The attachments borrow `TextureView` references for the duration of +/// the pass. +pub struct RenderColorAttachments<'a> { + attachments: Vec>>, +} + +impl<'a> RenderColorAttachments<'a> { + /// Create an empty attachments list. + pub fn new() -> Self { + return Self { + attachments: Vec::new(), + }; + } + + /// Append a color attachment targeting the provided `TextureView`. + /// + /// The load/store operations are initialized to load/store and will be + /// configured by the render pass builder before beginning the pass. + pub fn push_color(&mut self, view: surface::TextureViewRef<'a>) -> &mut Self { + let attachment = wgpu::RenderPassColorAttachment { + view: view.raw, + resolve_target: None, + depth_slice: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + }; + self.attachments.push(Some(attachment)); + return self; + } + + /// Apply the same operations to all color attachments. + pub(crate) fn set_operations_for_all( + &mut self, + operations: wgpu::Operations, + ) { + for attachment in &mut self.attachments { + if let Some(ref mut a) = attachment { + a.ops = operations; + } + } + } + + pub(crate) fn as_slice( + &self, + ) -> &[Option>] { + return &self.attachments; + } +} + +#[derive(Debug, Default)] +/// Builder for beginning a render pass targeting a single color attachment. +/// +/// Building a render pass implicitly begins one on the provided encoder and +/// returns a `RenderPass` wrapper that borrows the encoder. +pub struct RenderPassBuilder { + config: RenderPassConfig, +} + +impl RenderPassBuilder { + /// Create a new builder. Defaults to clearing to opaque black. + pub fn new() -> Self { + Self { + config: RenderPassConfig { + label: None, + color_operations: ColorOperations::default(), + }, + } + } + + /// Attach a debug label to the render pass. + pub fn with_label(mut self, label: &str) -> Self { + self.config.label = Some(label.to_string()); + return self; + } + + /// Set the clear color for the color attachment. + pub fn with_clear_color(mut self, color: [f64; 4]) -> Self { + self.config.color_operations.load = ColorLoadOp::Clear(color); + self.config.color_operations.store = StoreOp::Store; + return self; + } + + /// Set color load operation (load or clear with the previously set color). + pub fn with_color_load_op(mut self, load: ColorLoadOp) -> Self { + self.config.color_operations.load = load; + return self; + } + + /// Set color store operation (store or discard). + pub fn with_store_op(mut self, store: StoreOp) -> Self { + self.config.color_operations.store = store; + return self; + } + + /// Provide combined color operations. + pub fn with_color_operations(mut self, operations: ColorOperations) -> Self { + self.config.color_operations = operations; + return self; + } + + /// Build (begin) the render pass on the provided encoder using the provided + /// color attachments list. The attachments list MUST outlive the returned + /// render pass value. + pub fn build<'view>( + &'view self, + encoder: &'view mut command::CommandEncoder, + attachments: &'view mut RenderColorAttachments<'view>, + ) -> RenderPass<'view> { + let operations = match self.config.color_operations.load { + ColorLoadOp::Load => wgpu::Operations { + load: wgpu::LoadOp::Load, + store: match self.config.color_operations.store { + StoreOp::Store => wgpu::StoreOp::Store, + StoreOp::Discard => wgpu::StoreOp::Discard, + }, + }, + ColorLoadOp::Clear(color) => wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color { + r: color[0], + g: color[1], + b: color[2], + a: color[3], + }), + store: match self.config.color_operations.store { + StoreOp::Store => wgpu::StoreOp::Store, + StoreOp::Discard => wgpu::StoreOp::Discard, + }, + }, + }; + + // Apply operations to all provided attachments. + attachments.set_operations_for_all(operations); + + let desc: wgpu::RenderPassDescriptor<'view> = wgpu::RenderPassDescriptor { + label: self.config.label.as_deref(), + color_attachments: attachments.as_slice(), + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }; + + let pass = encoder.begin_render_pass_raw(&desc); + return RenderPass { raw: pass }; + } +} diff --git a/crates/lambda-rs-platform/src/wgpu/surface.rs b/crates/lambda-rs-platform/src/wgpu/surface.rs index cfa96368..0c6ced5c 100644 --- a/crates/lambda-rs-platform/src/wgpu/surface.rs +++ b/crates/lambda-rs-platform/src/wgpu/surface.rs @@ -5,7 +5,7 @@ use wgpu::rwh::{ use super::{ gpu::Gpu, - Instance, + instance::Instance, }; use crate::winit::WindowHandle; diff --git a/crates/lambda-rs/src/render/mod.rs b/crates/lambda-rs/src/render/mod.rs index 7e456304..5800fa93 100644 --- a/crates/lambda-rs/src/render/mod.rs +++ b/crates/lambda-rs/src/render/mod.rs @@ -47,21 +47,13 @@ use std::{ rc::Rc, }; -use lambda_platform::wgpu::{ - CommandEncoder as PlatformCommandEncoder, - Gpu, - GpuBuilder, - Instance, - InstanceBuilder, - Surface, - SurfaceBuilder, -}; +use lambda_platform::wgpu as platform; use logging; use self::{ command::RenderCommand, pipeline::RenderPipeline, - render_pass::RenderPass, + render_pass::RenderPass as RenderPassDesc, }; /// Builder for configuring a `RenderContext` tied to one window. @@ -113,11 +105,11 @@ impl RenderContextBuilder { ) -> Result { let RenderContextBuilder { name, .. } = self; - let instance = InstanceBuilder::new() + let instance = platform::instance::InstanceBuilder::new() .with_label(&format!("{} Instance", name)) .build(); - let mut surface = SurfaceBuilder::new() + let mut surface = platform::surface::SurfaceBuilder::new() .with_label(&format!("{} Surface", name)) .build(&instance, window.window_handle()) .map_err(|e| { @@ -127,7 +119,7 @@ impl RenderContextBuilder { )) })?; - let gpu = GpuBuilder::new() + let gpu = platform::gpu::GpuBuilder::new() .with_label(&format!("{} Device", name)) .build(&instance, Some(&surface)) .map_err(|e| { @@ -142,8 +134,8 @@ impl RenderContextBuilder { .configure_with_defaults( &gpu, size, - lambda_platform::wgpu::PresentMode::Fifo, - lambda_platform::wgpu::TextureUsages::RENDER_ATTACHMENT, + platform::surface::PresentMode::Fifo, + platform::surface::TextureUsages::RENDER_ATTACHMENT, ) .map_err(|e| { RenderContextError::SurfaceConfig(format!( @@ -196,14 +188,14 @@ impl RenderContextBuilder { /// reconfiguration with preserved present mode and usage. pub struct RenderContext { label: String, - instance: Instance, - surface: Surface<'static>, - gpu: Gpu, - config: lambda_platform::wgpu::SurfaceConfig, - present_mode: lambda_platform::wgpu::PresentMode, - texture_usage: lambda_platform::wgpu::TextureUsages, + instance: platform::instance::Instance, + surface: platform::surface::Surface<'static>, + gpu: platform::gpu::Gpu, + config: platform::surface::SurfaceConfig, + present_mode: platform::surface::PresentMode, + texture_usage: platform::surface::TextureUsages, size: (u32, u32), - render_passes: Vec, + render_passes: Vec, render_pipelines: Vec, bind_group_layouts: Vec, bind_groups: Vec, @@ -222,7 +214,10 @@ impl RenderContext { } /// Attach a render pass and return a handle for use in commands. - pub fn attach_render_pass(&mut self, render_pass: RenderPass) -> ResourceId { + pub fn attach_render_pass( + &mut self, + render_pass: RenderPassDesc, + ) -> ResourceId { let id = self.render_passes.len(); self.render_passes.push(render_pass); return id; @@ -293,7 +288,7 @@ impl RenderContext { /// Borrow a previously attached render pass by id. /// /// Panics if `id` does not refer to an attached pass. - pub fn get_render_pass(&self, id: ResourceId) -> &RenderPass { + pub fn get_render_pass(&self, id: ResourceId) -> &RenderPassDesc { return &self.render_passes[id]; } @@ -304,11 +299,11 @@ impl RenderContext { return &self.render_pipelines[id]; } - pub(crate) fn gpu(&self) -> &Gpu { + pub(crate) fn gpu(&self) -> &platform::gpu::Gpu { return &self.gpu; } - pub(crate) fn surface_format(&self) -> lambda_platform::wgpu::SurfaceFormat { + pub(crate) fn surface_format(&self) -> platform::surface::SurfaceFormat { return self.config.format; } @@ -338,8 +333,8 @@ impl RenderContext { let mut frame = match self.surface.acquire_next_frame() { Ok(frame) => frame, - Err(lambda_platform::wgpu::SurfaceError::Lost) - | Err(lambda_platform::wgpu::SurfaceError::Outdated) => { + Err(platform::surface::SurfaceError::Lost) + | Err(platform::surface::SurfaceError::Outdated) => { self.reconfigure_surface(self.size)?; self .surface @@ -350,7 +345,7 @@ impl RenderContext { }; let view = frame.texture_view(); - let mut encoder = PlatformCommandEncoder::new( + let mut encoder = platform::command::CommandEncoder::new( self.gpu(), Some("lambda-render-command-encoder"), ); @@ -368,11 +363,35 @@ impl RenderContext { )) })?; - let mut pass_encoder = encoder.begin_render_pass_clear( - pass.label(), - &view, - pass.clear_color(), - ); + // Build (begin) the platform render pass using the builder API. + let mut rp_builder = platform::render_pass::RenderPassBuilder::new(); + if let Some(label) = pass.label() { + rp_builder = rp_builder.with_label(label); + } + let ops = pass.color_operations(); + rp_builder = match ops.load { + self::render_pass::ColorLoadOp::Load => rp_builder + .with_color_load_op(platform::render_pass::ColorLoadOp::Load), + self::render_pass::ColorLoadOp::Clear(color) => rp_builder + .with_color_load_op(platform::render_pass::ColorLoadOp::Clear( + color, + )), + }; + rp_builder = match ops.store { + self::render_pass::StoreOp::Store => { + rp_builder.with_store_op(platform::render_pass::StoreOp::Store) + } + self::render_pass::StoreOp::Discard => { + rp_builder.with_store_op(platform::render_pass::StoreOp::Discard) + } + }; + // Create variably sized color attachments and begin the pass. + let mut color_attachments = + platform::render_pass::RenderColorAttachments::new(); + color_attachments.push_color(view); + + let mut pass_encoder = + rp_builder.build(&mut encoder, &mut color_attachments); self.encode_pass(&mut pass_encoder, viewport, &mut command_iter)?; } @@ -393,7 +412,7 @@ impl RenderContext { /// Encode a single render pass and consume commands until `EndRenderPass`. fn encode_pass( &mut self, - pass: &mut lambda_platform::wgpu::RenderPass<'_>, + pass: &mut platform::render_pass::RenderPass<'_>, initial_viewport: viewport::Viewport, commands: &mut I, ) -> Result<(), RenderError> @@ -525,7 +544,7 @@ impl RenderContext { /// Apply both viewport and scissor state to the active pass. fn apply_viewport( - pass: &mut lambda_platform::wgpu::RenderPass<'_>, + pass: &mut platform::render_pass::RenderPass<'_>, viewport: &viewport::Viewport, ) { let (x, y, width, height, min_depth, max_depth) = viewport.viewport_f32(); @@ -562,12 +581,12 @@ impl RenderContext { /// acquisition or command encoding. The renderer logs these and continues when /// possible; callers SHOULD treat them as warnings unless persistent. pub enum RenderError { - Surface(lambda_platform::wgpu::SurfaceError), + Surface(platform::surface::SurfaceError), Configuration(String), } -impl From for RenderError { - fn from(error: lambda_platform::wgpu::SurfaceError) -> Self { +impl From for RenderError { + fn from(error: platform::surface::SurfaceError) -> Self { return RenderError::Surface(error); } } diff --git a/crates/lambda-rs/src/render/render_pass.rs b/crates/lambda-rs/src/render/render_pass.rs index f1f7af52..325bfc11 100644 --- a/crates/lambda-rs/src/render/render_pass.rs +++ b/crates/lambda-rs/src/render/render_pass.rs @@ -6,6 +6,40 @@ use super::RenderContext; +#[derive(Debug, Clone, Copy, PartialEq)] +/// Color load operation for the first color attachment. +pub enum ColorLoadOp { + /// Load existing contents. + Load, + /// Clear to the provided RGBA color. + Clear([f64; 4]), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// Store operation for the first color attachment. +pub enum StoreOp { + /// Store results at the end of the pass. + Store, + /// Discard results when possible. + Discard, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +/// Combined color operations for the first color attachment. +pub struct ColorOperations { + pub load: ColorLoadOp, + pub store: StoreOp, +} + +impl Default for ColorOperations { + fn default() -> Self { + return Self { + load: ColorLoadOp::Clear([0.0, 0.0, 0.0, 1.0]), + store: StoreOp::Store, + }; + } +} + #[derive(Debug, Clone)] /// Immutable parameters used when beginning a render pass. /// @@ -14,6 +48,7 @@ use super::RenderContext; pub struct RenderPass { clear_color: [f64; 4], label: Option, + color_operations: ColorOperations, } impl RenderPass { @@ -27,6 +62,10 @@ impl RenderPass { pub(crate) fn label(&self) -> Option<&str> { self.label.as_deref() } + + pub(crate) fn color_operations(&self) -> ColorOperations { + return self.color_operations; + } } /// Builder for a `RenderPass` description. @@ -37,6 +76,7 @@ impl RenderPass { pub struct RenderPassBuilder { clear_color: [f64; 4], label: Option, + color_operations: ColorOperations, } impl RenderPassBuilder { @@ -45,12 +85,17 @@ impl RenderPassBuilder { Self { clear_color: [0.0, 0.0, 0.0, 1.0], label: None, + color_operations: ColorOperations::default(), } } /// Specify the clear color used for the first color attachment. pub fn with_clear_color(mut self, color: [f64; 4]) -> Self { self.clear_color = color; + self.color_operations = ColorOperations { + load: ColorLoadOp::Clear(color), + store: StoreOp::Store, + }; self } @@ -60,11 +105,36 @@ impl RenderPassBuilder { self } + /// Specify the color load operation for the first color attachment. + pub fn with_color_load_op(mut self, load: ColorLoadOp) -> Self { + self.color_operations.load = load; + if let ColorLoadOp::Clear(color) = load { + self.clear_color = color; + } + return self; + } + + /// Specify the color store operation for the first color attachment. + pub fn with_store_op(mut self, store: StoreOp) -> Self { + self.color_operations.store = store; + return self; + } + + /// Provide combined color operations for the first color attachment. + pub fn with_color_operations(mut self, operations: ColorOperations) -> Self { + self.color_operations = operations; + if let ColorLoadOp::Clear(color) = operations.load { + self.clear_color = color; + } + return self; + } + /// Build the description used when beginning a render pass. pub fn build(self, _render_context: &RenderContext) -> RenderPass { RenderPass { clear_color: self.clear_color, label: self.label, + color_operations: self.color_operations, } } } From a4fb29be51a7452312f2345c727e18ffa8609378 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Wed, 5 Nov 2025 15:56:55 -0800 Subject: [PATCH 10/11] [fix] documentation. --- crates/lambda-rs-args/src/lib.rs | 10 +++++----- crates/lambda-rs-platform/src/wgpu/bind.rs | 10 +++++----- crates/lambda-rs-platform/src/wgpu/buffer.rs | 8 ++++---- crates/lambda-rs-platform/src/wgpu/command.rs | 4 ++-- crates/lambda-rs-platform/src/wgpu/gpu.rs | 4 ++-- .../lambda-rs-platform/src/wgpu/instance.rs | 10 +++++----- .../lambda-rs-platform/src/wgpu/pipeline.rs | 14 ++++++------- .../src/wgpu/render_pass.rs | 12 +++++------ crates/lambda-rs-platform/src/wgpu/surface.rs | 20 +++++++++---------- crates/lambda-rs/src/render/bind.rs | 6 +++--- crates/lambda-rs/src/render/buffer.rs | 6 +++--- crates/lambda-rs/src/render/mod.rs | 4 ++-- crates/lambda-rs/src/render/pipeline.rs | 2 +- crates/lambda-rs/src/render/render_pass.rs | 8 ++++---- crates/lambda-rs/src/render/vertex.rs | 4 ++-- crates/lambda-rs/src/render/viewport.rs | 2 +- crates/lambda-rs/src/runtimes/application.rs | 2 +- 17 files changed, 63 insertions(+), 63 deletions(-) diff --git a/crates/lambda-rs-args/src/lib.rs b/crates/lambda-rs-args/src/lib.rs index 31cf4e13..2dde60e4 100644 --- a/crates/lambda-rs-args/src/lib.rs +++ b/crates/lambda-rs-args/src/lib.rs @@ -36,8 +36,8 @@ pub struct ArgumentParser { is_subcommand: bool, } -#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] /// Supported value types for an argument definition. +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] pub enum ArgumentType { /// `true`/`false` (or implied by presence when compiled as a flag). Boolean, @@ -61,8 +61,8 @@ pub enum ArgumentType { DoubleList, } -#[derive(Debug, Clone, PartialEq, PartialOrd)] /// Parsed value container used in results and defaults. +#[derive(Debug, Clone, PartialEq, PartialOrd)] pub enum ArgumentValue { None, Boolean(bool), @@ -112,8 +112,8 @@ impl Into for ArgumentValue { } } -#[derive(Debug)] /// Declarative definition for a single CLI argument or positional parameter. +#[derive(Debug)] pub struct Argument { name: String, description: String, @@ -222,8 +222,8 @@ impl Argument { } } -#[derive(Debug, Clone)] /// A single parsed argument result as `(name, value)`. +#[derive(Debug, Clone)] pub struct ParsedArgument { name: String, value: ArgumentValue, @@ -830,8 +830,8 @@ fn parse_value(arg: &Argument, raw: &str) -> Result { } } -#[derive(Debug)] /// Errors that may occur during argument parsing. +#[derive(Debug)] pub enum ArgsError { /// An unknown flag or option was encountered. UnknownArgument(String), diff --git a/crates/lambda-rs-platform/src/wgpu/bind.rs b/crates/lambda-rs-platform/src/wgpu/bind.rs index 318750b2..4cc5968e 100644 --- a/crates/lambda-rs-platform/src/wgpu/bind.rs +++ b/crates/lambda-rs-platform/src/wgpu/bind.rs @@ -13,8 +13,8 @@ use crate::wgpu::{ gpu::Gpu, }; -#[derive(Debug)] /// Wrapper around `wgpu::BindGroupLayout` that preserves a label. +#[derive(Debug)] pub struct BindGroupLayout { pub(crate) raw: wgpu::BindGroupLayout, pub(crate) label: Option, @@ -32,8 +32,8 @@ impl BindGroupLayout { } } -#[derive(Debug)] /// Wrapper around `wgpu::BindGroup` that preserves a label. +#[derive(Debug)] pub struct BindGroup { pub(crate) raw: wgpu::BindGroup, pub(crate) label: Option, @@ -51,8 +51,8 @@ impl BindGroup { } } -#[derive(Clone, Copy, Debug)] /// Visibility of a binding across shader stages. +#[derive(Clone, Copy, Debug)] pub enum Visibility { Vertex, Fragment, @@ -99,8 +99,8 @@ mod tests { } } -#[derive(Default)] /// Builder for creating a `wgpu::BindGroupLayout`. +#[derive(Default)] pub struct BindGroupLayoutBuilder { label: Option, entries: Vec, @@ -171,8 +171,8 @@ impl BindGroupLayoutBuilder { } } -#[derive(Default)] /// Builder for creating a `wgpu::BindGroup`. +#[derive(Default)] pub struct BindGroupBuilder<'a> { label: Option, layout: Option<&'a wgpu::BindGroupLayout>, diff --git a/crates/lambda-rs-platform/src/wgpu/buffer.rs b/crates/lambda-rs-platform/src/wgpu/buffer.rs index 97bdc050..4656eb16 100644 --- a/crates/lambda-rs-platform/src/wgpu/buffer.rs +++ b/crates/lambda-rs-platform/src/wgpu/buffer.rs @@ -10,8 +10,8 @@ use wgpu::{ use crate::wgpu::gpu::Gpu; -#[derive(Clone, Copy, Debug, PartialEq, Eq)] /// Index format for indexed drawing. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum IndexFormat { Uint16, Uint32, @@ -26,8 +26,8 @@ impl IndexFormat { } } -#[derive(Clone, Copy, Debug)] /// Platform buffer usage flags. +#[derive(Clone, Copy, Debug)] pub struct Usage(pub(crate) wgpu::BufferUsages); impl Usage { @@ -60,8 +60,8 @@ impl Default for Usage { } } -#[derive(Debug)] /// Wrapper around `wgpu::Buffer` with metadata. +#[derive(Debug)] pub struct Buffer { pub(crate) raw: wgpu::Buffer, pub(crate) label: Option, @@ -96,8 +96,8 @@ impl Buffer { } } -#[derive(Default)] /// Builder for creating a `Buffer` with optional initial contents. +#[derive(Default)] pub struct BufferBuilder { label: Option, size: usize, diff --git a/crates/lambda-rs-platform/src/wgpu/command.rs b/crates/lambda-rs-platform/src/wgpu/command.rs index f715a3fe..1328cfa9 100644 --- a/crates/lambda-rs-platform/src/wgpu/command.rs +++ b/crates/lambda-rs-platform/src/wgpu/command.rs @@ -4,14 +4,14 @@ use super::gpu; -#[derive(Debug)] /// Thin wrapper around `wgpu::CommandEncoder` with convenience helpers. +#[derive(Debug)] pub struct CommandEncoder { raw: wgpu::CommandEncoder, } -#[derive(Debug)] /// Wrapper around `wgpu::CommandBuffer` to avoid exposing raw types upstream. +#[derive(Debug)] pub struct CommandBuffer { raw: wgpu::CommandBuffer, } diff --git a/crates/lambda-rs-platform/src/wgpu/gpu.rs b/crates/lambda-rs-platform/src/wgpu/gpu.rs index b36550e2..2a544e74 100644 --- a/crates/lambda-rs-platform/src/wgpu/gpu.rs +++ b/crates/lambda-rs-platform/src/wgpu/gpu.rs @@ -165,8 +165,8 @@ impl GpuBuilder { } } -#[derive(Debug)] /// Errors emitted while building a `Gpu`. +#[derive(Debug)] pub enum GpuBuildError { /// No compatible adapter could be found. AdapterUnavailable, @@ -185,9 +185,9 @@ impl From for GpuBuildError { } } -#[derive(Debug)] /// Holds the chosen adapter along with its logical device and submission queue /// plus immutable copies of features and limits used to create the device. +#[derive(Debug)] pub struct Gpu { adapter: wgpu::Adapter, device: wgpu::Device, diff --git a/crates/lambda-rs-platform/src/wgpu/instance.rs b/crates/lambda-rs-platform/src/wgpu/instance.rs index 0550d388..83ef3368 100644 --- a/crates/lambda-rs-platform/src/wgpu/instance.rs +++ b/crates/lambda-rs-platform/src/wgpu/instance.rs @@ -4,8 +4,8 @@ use pollster::block_on; -#[derive(Clone, Copy, Debug, PartialEq, Eq)] /// Wrapper over `wgpu::Backends` as a bitset. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Backends(wgpu::Backends); impl Backends { @@ -40,8 +40,8 @@ impl std::ops::BitOr for Backends { } } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] /// Wrapper over `wgpu::InstanceFlags` as a bitset. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct InstanceFlags(wgpu::InstanceFlags); impl InstanceFlags { @@ -69,8 +69,8 @@ impl std::ops::BitOr for InstanceFlags { } } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] /// Which DX12 shader compiler to use on Windows platforms. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Dx12Compiler { Fxc, } @@ -83,8 +83,8 @@ impl Dx12Compiler { } } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] /// OpenGL ES 3 minor version (used by GL/Web targets). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Gles3MinorVersion { Automatic, Version0, @@ -103,8 +103,8 @@ impl Gles3MinorVersion { } } -#[derive(Debug, Clone)] /// Builder for creating a `wgpu::Instance` with consistent defaults. +#[derive(Debug, Clone)] /// /// Defaults to primary backends and no special flags. Options map to /// `wgpu::InstanceDescriptor` internally without leaking raw types. diff --git a/crates/lambda-rs-platform/src/wgpu/pipeline.rs b/crates/lambda-rs-platform/src/wgpu/pipeline.rs index 1574f84d..fadbe2e7 100644 --- a/crates/lambda-rs-platform/src/wgpu/pipeline.rs +++ b/crates/lambda-rs-platform/src/wgpu/pipeline.rs @@ -11,8 +11,8 @@ use crate::wgpu::{ vertex::ColorFormat, }; -#[derive(Clone, Copy, Debug)] /// Shader stage flags for push constants and visibility. +#[derive(Clone, Copy, Debug)] /// /// This wrapper avoids exposing `wgpu` directly to higher layers while still /// allowing flexible combinations when needed. @@ -47,15 +47,15 @@ impl std::ops::BitOrAssign for PipelineStage { } } -#[derive(Clone, Debug)] /// Push constant declaration for a stage and byte range. +#[derive(Clone, Debug)] pub struct PushConstantRange { pub stages: PipelineStage, pub range: Range, } -#[derive(Clone, Copy, Debug)] /// Face culling mode for graphics pipelines. +#[derive(Clone, Copy, Debug)] pub enum CullingMode { None, Front, @@ -72,16 +72,16 @@ impl CullingMode { } } -#[derive(Clone, Copy, Debug)] /// Description of a single vertex attribute used by a pipeline. +#[derive(Clone, Copy, Debug)] pub struct VertexAttributeDesc { pub shader_location: u32, pub offset: u64, pub format: ColorFormat, } -#[derive(Debug)] /// Wrapper around `wgpu::ShaderModule` that preserves a label. +#[derive(Debug)] pub struct ShaderModule { raw: wgpu::ShaderModule, label: Option, @@ -108,8 +108,8 @@ impl ShaderModule { } } -#[derive(Debug)] /// Wrapper around `wgpu::PipelineLayout`. +#[derive(Debug)] pub struct PipelineLayout { raw: wgpu::PipelineLayout, label: Option, @@ -185,8 +185,8 @@ impl<'a> PipelineLayoutBuilder<'a> { } } -#[derive(Debug)] /// Wrapper around `wgpu::RenderPipeline`. +#[derive(Debug)] pub struct RenderPipeline { raw: wgpu::RenderPipeline, label: Option, diff --git a/crates/lambda-rs-platform/src/wgpu/render_pass.rs b/crates/lambda-rs-platform/src/wgpu/render_pass.rs index f34d0aaf..71d09b0a 100644 --- a/crates/lambda-rs-platform/src/wgpu/render_pass.rs +++ b/crates/lambda-rs-platform/src/wgpu/render_pass.rs @@ -20,8 +20,8 @@ use super::{ surface, }; -#[derive(Clone, Copy, Debug, PartialEq)] /// Color load operation for a render pass color attachment. +#[derive(Clone, Copy, Debug, PartialEq)] pub enum ColorLoadOp { /// Load the existing contents of the attachment. Load, @@ -29,8 +29,8 @@ pub enum ColorLoadOp { Clear([f64; 4]), } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] /// Store operation for a render pass attachment. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum StoreOp { /// Store the results to the attachment at the end of the pass. Store, @@ -38,8 +38,8 @@ pub enum StoreOp { Discard, } -#[derive(Clone, Copy, Debug)] /// Combined load and store operations for the color attachment. +#[derive(Clone, Copy, Debug)] pub struct ColorOperations { pub load: ColorLoadOp, pub store: StoreOp, @@ -54,16 +54,16 @@ impl Default for ColorOperations { } } -#[derive(Clone, Debug, Default)] /// Configuration for beginning a render pass. +#[derive(Clone, Debug, Default)] pub struct RenderPassConfig { pub label: Option, pub color_operations: ColorOperations, } -#[derive(Debug)] /// Wrapper around `wgpu::RenderPass<'_>` exposing the operations needed by the /// engine without leaking raw `wgpu` types at the call sites. +#[derive(Debug)] pub struct RenderPass<'a> { pub(super) raw: wgpu::RenderPass<'a>, } @@ -151,10 +151,10 @@ impl<'a> RenderPass<'a> { } } -#[derive(Debug, Default)] /// Wrapper for a variably sized list of color attachments passed into a render /// pass. The attachments borrow `TextureView` references for the duration of /// the pass. +#[derive(Debug, Default)] pub struct RenderColorAttachments<'a> { attachments: Vec>>, } diff --git a/crates/lambda-rs-platform/src/wgpu/surface.rs b/crates/lambda-rs-platform/src/wgpu/surface.rs index 0c6ced5c..d1383fbd 100644 --- a/crates/lambda-rs-platform/src/wgpu/surface.rs +++ b/crates/lambda-rs-platform/src/wgpu/surface.rs @@ -9,8 +9,8 @@ use super::{ }; use crate::winit::WindowHandle; -#[derive(Clone, Copy, Debug, PartialEq, Eq)] /// Present modes supported by the surface. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] /// /// This wrapper hides the underlying `wgpu` type from higher layers while /// preserving the same semantics. @@ -48,8 +48,8 @@ impl PresentMode { } } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] /// Wrapper for texture usage flags used by surfaces. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct TextureUsages(wgpu::TextureUsages); impl TextureUsages { @@ -88,8 +88,8 @@ impl std::ops::BitOr for TextureUsages { } } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] /// Wrapper around a surface color format. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct SurfaceFormat(wgpu::TextureFormat); impl SurfaceFormat { @@ -112,8 +112,8 @@ impl SurfaceFormat { } } -#[derive(Clone, Debug)] /// Public, engine-facing surface configuration that avoids exposing `wgpu`. +#[derive(Clone, Debug)] pub struct SurfaceConfig { pub width: u32, pub height: u32, @@ -158,8 +158,8 @@ impl SurfaceConfig { } } -#[derive(Clone, Debug)] /// Error wrapper for surface acquisition and presentation errors. +#[derive(Clone, Debug)] pub enum SurfaceError { /// The surface has been lost and must be recreated. Lost, @@ -186,8 +186,8 @@ impl From for SurfaceError { } } -#[derive(Debug, Clone)] /// Builder for creating a `Surface` bound to a `winit` window. +#[derive(Debug, Clone)] pub struct SurfaceBuilder { label: Option, } @@ -247,8 +247,8 @@ impl SurfaceBuilder { } } -#[derive(Debug)] /// Opaque error returned when surface creation fails. +#[derive(Debug)] pub struct CreateSurfaceError; impl From for CreateSurfaceError { @@ -257,8 +257,8 @@ impl From for CreateSurfaceError { } } -#[derive(Debug)] /// Presentation surface wrapper with cached configuration and format. +#[derive(Debug)] pub struct Surface<'window> { label: String, surface: wgpu::Surface<'window>, @@ -387,15 +387,15 @@ impl<'window> Surface<'window> { } } -#[derive(Debug)] /// A single acquired frame and its default `TextureView`. +#[derive(Debug)] pub struct Frame { texture: wgpu::SurfaceTexture, view: wgpu::TextureView, } -#[derive(Clone, Copy)] /// Borrowed reference to a texture view used for render passes. +#[derive(Clone, Copy)] pub struct TextureViewRef<'a> { pub(crate) raw: &'a wgpu::TextureView, } diff --git a/crates/lambda-rs/src/render/bind.rs b/crates/lambda-rs/src/render/bind.rs index f4ed1418..d932e83b 100644 --- a/crates/lambda-rs/src/render/bind.rs +++ b/crates/lambda-rs/src/render/bind.rs @@ -20,11 +20,11 @@ use std::rc::Rc; -#[derive(Clone, Copy, Debug)] /// Visibility of a binding across shader stages (engine‑facing). /// /// Select one or more shader stages that read a bound resource. Use /// `VertexAndFragment` for shared layouts in typical graphics pipelines. +#[derive(Clone, Copy, Debug)] pub enum BindingVisibility { Vertex, Fragment, @@ -63,8 +63,8 @@ mod tests { use super::*; } -#[derive(Debug, Clone)] /// Bind group layout used when creating pipelines and bind groups. +#[derive(Debug, Clone)] /// /// Holds a platform layout and the number of dynamic bindings so callers can /// validate dynamic offset counts at bind time. @@ -88,8 +88,8 @@ impl BindGroupLayout { } } -#[derive(Debug, Clone)] /// Bind group that binds one or more resources to a pipeline set index. +#[derive(Debug, Clone)] /// /// The group mirrors the structure of its `BindGroupLayout`. When using /// dynamic uniforms, record a corresponding list of byte offsets in the diff --git a/crates/lambda-rs/src/render/buffer.rs b/crates/lambda-rs/src/render/buffer.rs index 7ea5fcb1..6b996ae7 100644 --- a/crates/lambda-rs/src/render/buffer.rs +++ b/crates/lambda-rs/src/render/buffer.rs @@ -28,8 +28,8 @@ use super::{ RenderContext, }; -#[derive(Clone, Copy, Debug, PartialEq, Eq)] /// High‑level classification for buffers created by the engine. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] /// /// The type guides default usage flags and how a buffer is bound during /// encoding: @@ -44,8 +44,8 @@ pub enum BufferType { Storage, } -#[derive(Clone, Copy, Debug)] /// Buffer usage flags (engine‑facing), mapped to platform usage internally. +#[derive(Clone, Copy, Debug)] pub struct Usage(platform_buffer::Usage); impl Usage { @@ -77,8 +77,8 @@ impl Default for Usage { } } -#[derive(Clone, Copy, Debug)] /// Buffer allocation properties that control residency and CPU visibility. +#[derive(Clone, Copy, Debug)] /// /// Use `CPU_VISIBLE` for frequently updated data (e.g., uniform uploads). /// Prefer `DEVICE_LOCAL` for static geometry uploaded once. diff --git a/crates/lambda-rs/src/render/mod.rs b/crates/lambda-rs/src/render/mod.rs index 5800fa93..2f600964 100644 --- a/crates/lambda-rs/src/render/mod.rs +++ b/crates/lambda-rs/src/render/mod.rs @@ -574,8 +574,8 @@ impl RenderContext { } } -#[derive(Debug)] /// Errors reported while preparing or presenting a frame. +#[derive(Debug)] /// /// Variants summarize recoverable issues that can appear during frame /// acquisition or command encoding. The renderer logs these and continues when @@ -591,8 +591,8 @@ impl From for RenderError { } } -#[derive(Debug)] /// Errors encountered while creating a `RenderContext`. +#[derive(Debug)] /// /// Returned by `RenderContextBuilder::build` to avoid panics during /// initialization and provide actionable error messages to callers. diff --git a/crates/lambda-rs/src/render/pipeline.rs b/crates/lambda-rs/src/render/pipeline.rs index cfcfeac9..d3e1c383 100644 --- a/crates/lambda-rs/src/render/pipeline.rs +++ b/crates/lambda-rs/src/render/pipeline.rs @@ -41,8 +41,8 @@ use super::{ RenderContext, }; -#[derive(Debug)] /// A created graphics pipeline and the vertex buffers it expects. +#[derive(Debug)] /// /// Pipelines are immutable; destroy them with the context when no longer needed. pub struct RenderPipeline { diff --git a/crates/lambda-rs/src/render/render_pass.rs b/crates/lambda-rs/src/render/render_pass.rs index 325bfc11..99c5ddd2 100644 --- a/crates/lambda-rs/src/render/render_pass.rs +++ b/crates/lambda-rs/src/render/render_pass.rs @@ -6,8 +6,8 @@ use super::RenderContext; -#[derive(Debug, Clone, Copy, PartialEq)] /// Color load operation for the first color attachment. +#[derive(Debug, Clone, Copy, PartialEq)] pub enum ColorLoadOp { /// Load existing contents. Load, @@ -15,8 +15,8 @@ pub enum ColorLoadOp { Clear([f64; 4]), } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] /// Store operation for the first color attachment. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum StoreOp { /// Store results at the end of the pass. Store, @@ -24,8 +24,8 @@ pub enum StoreOp { Discard, } -#[derive(Debug, Clone, Copy, PartialEq)] /// Combined color operations for the first color attachment. +#[derive(Debug, Clone, Copy, PartialEq)] pub struct ColorOperations { pub load: ColorLoadOp, pub store: StoreOp, @@ -40,8 +40,8 @@ impl Default for ColorOperations { } } -#[derive(Debug, Clone)] /// Immutable parameters used when beginning a render pass. +#[derive(Debug, Clone)] /// /// The pass defines the initial clear for the color attachment and an optional /// label. Depth/stencil may be added in a future iteration. diff --git a/crates/lambda-rs/src/render/vertex.rs b/crates/lambda-rs/src/render/vertex.rs index 506ffb00..1592a149 100644 --- a/crates/lambda-rs/src/render/vertex.rs +++ b/crates/lambda-rs/src/render/vertex.rs @@ -26,8 +26,8 @@ impl ColorFormat { } } -#[derive(Clone, Copy, Debug)] /// A single vertex element (format + byte offset). +#[derive(Clone, Copy, Debug)] /// /// Combine one or more elements to form a `VertexAttribute` bound at a shader /// location. Offsets are in bytes from the start of the vertex and the element. @@ -36,8 +36,8 @@ pub struct VertexElement { pub offset: u32, } -#[derive(Clone, Copy, Debug)] /// Vertex attribute bound to a shader `location` plus relative offsets. +#[derive(Clone, Copy, Debug)] /// /// `location` MUST match the shader input. The final attribute byte offset is /// `offset + element.offset`. diff --git a/crates/lambda-rs/src/render/viewport.rs b/crates/lambda-rs/src/render/viewport.rs index 8343d337..1c94e9ed 100644 --- a/crates/lambda-rs/src/render/viewport.rs +++ b/crates/lambda-rs/src/render/viewport.rs @@ -4,8 +4,8 @@ //! render pass. Coordinates are specified in pixels with origin at the //! top‑left of the surface. -#[derive(Debug, Clone, PartialEq)] /// Viewport/scissor rectangle applied during rendering. +#[derive(Debug, Clone, PartialEq)] pub struct Viewport { pub x: u32, pub y: u32, diff --git a/crates/lambda-rs/src/runtimes/application.rs b/crates/lambda-rs/src/runtimes/application.rs index 3eea9dbf..5509dc3a 100644 --- a/crates/lambda-rs/src/runtimes/application.rs +++ b/crates/lambda-rs/src/runtimes/application.rs @@ -41,9 +41,9 @@ use crate::{ runtime::Runtime, }; -#[derive(Clone, Debug)] /// Result value used by component callbacks executed under /// `ApplicationRuntime`. +#[derive(Clone, Debug)] /// /// Components can return `Success` when work completed as expected or /// `Failure` to signal a non‑fatal error to the runtime. From d59603e0f63dfa7d27776fff7febd123810aad5d Mon Sep 17 00:00:00 2001 From: vmarcella Date: Fri, 7 Nov 2025 14:21:03 -0800 Subject: [PATCH 11/11] [add] .ignore file. --- .ignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.ignore b/.ignore index eca8cf50..a3c8e83d 100644 --- a/.ignore +++ b/.ignore @@ -1,4 +1,3 @@ -docs archive/lambda_cpp/vendor \.git target