From 3ad80b0b8d003d8ad00d8c9dc7b8787ca4cbe5a1 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Thu, 11 Dec 2025 11:35:09 -0800 Subject: [PATCH 01/28] [add] presentation mode abstraction. --- crates/lambda-rs/src/render/mod.rs | 1 + crates/lambda-rs/src/render/surface.rs | 49 ++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 crates/lambda-rs/src/render/surface.rs diff --git a/crates/lambda-rs/src/render/mod.rs b/crates/lambda-rs/src/render/mod.rs index 1f1f66e3..96119bf0 100644 --- a/crates/lambda-rs/src/render/mod.rs +++ b/crates/lambda-rs/src/render/mod.rs @@ -37,6 +37,7 @@ pub mod pipeline; pub mod render_pass; pub mod scene_math; pub mod shader; +pub mod surface; pub mod texture; pub mod validation; pub mod vertex; diff --git a/crates/lambda-rs/src/render/surface.rs b/crates/lambda-rs/src/render/surface.rs new file mode 100644 index 00000000..f796bbde --- /dev/null +++ b/crates/lambda-rs/src/render/surface.rs @@ -0,0 +1,49 @@ +use lambda_platform::wgpu::surface as platform_surface; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum PresentMode { + /// Vsync enabled; frames wait for vertical blanking interval. + Fifo, + /// Vsync with relaxed timing; may tear if frames miss the interval. + FifoRelaxed, + /// No Vsync; immediate presentation (may tear). + Immediate, + /// Triple-buffered presentation when supported. + Mailbox, + /// Automatic Vsync selection by the platform. + AutoVsync, + /// Automatic non-Vsync selection by the platform. + AutoNoVsync, +} + +impl PresentMode { + pub(crate) fn to_platform(&self) -> platform_surface::PresentMode { + match self { + PresentMode::Fifo => platform_surface::PresentMode::Fifo, + PresentMode::FifoRelaxed => platform_surface::PresentMode::FifoRelaxed, + PresentMode::Immediate => platform_surface::PresentMode::Immediate, + PresentMode::Mailbox => platform_surface::PresentMode::Mailbox, + PresentMode::AutoVsync => platform_surface::PresentMode::AutoVsync, + PresentMode::AutoNoVsync => platform_surface::PresentMode::AutoNoVsync, + } + } + + pub(crate) fn from_platform( + mode: &platform_surface::PresentMode, + ) -> PresentMode { + match mode { + platform_surface::PresentMode::Fifo => PresentMode::Fifo, + platform_surface::PresentMode::FifoRelaxed => PresentMode::FifoRelaxed, + platform_surface::PresentMode::Immediate => PresentMode::Immediate, + platform_surface::PresentMode::Mailbox => PresentMode::Mailbox, + platform_surface::PresentMode::AutoVsync => PresentMode::AutoVsync, + platform_surface::PresentMode::AutoNoVsync => PresentMode::AutoNoVsync, + } + } +} + +impl Default for PresentMode { + fn default() -> Self { + return PresentMode::Fifo; + } +} From 16e3bae6eed41130b70add6a2c6c43296e37c8e6 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Thu, 11 Dec 2025 11:37:32 -0800 Subject: [PATCH 02/28] [add] color attachments abstraction and inline surface calls. --- .../lambda-rs/src/render/color_attachments.rs | 104 ++++++++++++++++++ crates/lambda-rs/src/render/surface.rs | 2 + 2 files changed, 106 insertions(+) create mode 100644 crates/lambda-rs/src/render/color_attachments.rs diff --git a/crates/lambda-rs/src/render/color_attachments.rs b/crates/lambda-rs/src/render/color_attachments.rs new file mode 100644 index 00000000..a2a73561 --- /dev/null +++ b/crates/lambda-rs/src/render/color_attachments.rs @@ -0,0 +1,104 @@ +//! High‑level wrapper for render pass color attachments. +//! +//! This module provides `RenderColorAttachments`, a lightweight engine‑level +//! wrapper that maps to the platform `RenderColorAttachments` type without +//! exposing `wgpu` details at call sites. + +use lambda_platform::wgpu as platform; + +use super::{ + render_pass::RenderPass as RenderPassDesc, + RenderContext, +}; + +#[derive(Debug, Default)] +/// High‑level color attachments collection used when beginning a render pass. +/// +/// This type delegates to the platform `RenderColorAttachments` while keeping +/// the engine API stable and avoiding direct references to platform types in +/// higher‑level modules. +pub(crate) struct RenderColorAttachments<'view> { + inner: platform::render_pass::RenderColorAttachments<'view>, +} + +impl<'view> RenderColorAttachments<'view> { + /// Create an empty color attachments list. + pub(crate) fn new() -> Self { + return Self { + inner: platform::render_pass::RenderColorAttachments::new(), + }; + } + + /// Append a color attachment targeting the provided texture view. + pub(crate) fn push_color( + &mut self, + view: platform::surface::TextureViewRef<'view>, + ) { + self.inner.push_color(view); + } + + /// Append a multi‑sampled color attachment with a resolve target view. + pub(crate) fn push_msaa_color( + &mut self, + msaa_view: platform::surface::TextureViewRef<'view>, + resolve_view: platform::surface::TextureViewRef<'view>, + ) { + self.inner.push_msaa_color(msaa_view, resolve_view); + } + + /// Borrow the underlying platform attachments mutably for pass creation. + pub(crate) fn as_platform_attachments_mut( + &mut self, + ) -> &mut platform::render_pass::RenderColorAttachments<'view> { + return &mut self.inner; + } + + /// Build color attachments for a surface‑backed render pass. + /// + /// This helper encapsulates the logic for configuring single‑sample and + /// multi‑sample color attachments targeting the presentation surface, + /// including creation and reuse of the MSAA resolve target stored on the + /// `RenderContext`. + pub(crate) fn for_surface_pass( + render_context: &mut RenderContext, + pass: &RenderPassDesc, + surface_view: platform::surface::TextureViewRef<'view>, + ) -> Self { + let mut attachments = RenderColorAttachments::new(); + if !pass.uses_color() { + return attachments; + } + + let sample_count = pass.sample_count(); + if sample_count > 1 { + let need_recreate = match &render_context.msaa_color { + Some(_existing) => render_context.msaa_sample_count != sample_count, + None => true, + }; + + if need_recreate { + render_context.msaa_color = Some( + platform::texture::ColorAttachmentTextureBuilder::new( + render_context.config.format, + ) + .with_size(render_context.size.0.max(1), render_context.size.1.max(1)) + .with_sample_count(sample_count) + .with_label("lambda-msaa-color") + .build(render_context.gpu()), + ); + render_context.msaa_sample_count = sample_count; + } + + let msaa_view = render_context + .msaa_color + .as_ref() + .expect("MSAA color attachment should be created") + .view_ref(); + attachments.push_msaa_color(msaa_view, surface_view); + } else { + attachments.push_color(surface_view); + } + + return attachments; + } +} diff --git a/crates/lambda-rs/src/render/surface.rs b/crates/lambda-rs/src/render/surface.rs index f796bbde..802a0856 100644 --- a/crates/lambda-rs/src/render/surface.rs +++ b/crates/lambda-rs/src/render/surface.rs @@ -17,6 +17,7 @@ pub enum PresentMode { } impl PresentMode { + #[inline] pub(crate) fn to_platform(&self) -> platform_surface::PresentMode { match self { PresentMode::Fifo => platform_surface::PresentMode::Fifo, @@ -28,6 +29,7 @@ impl PresentMode { } } + #[inline] pub(crate) fn from_platform( mode: &platform_surface::PresentMode, ) -> PresentMode { From e6c8e2bcda34a9bdc60557c2ac8bd74b9c6cbacb Mon Sep 17 00:00:00 2001 From: vmarcella Date: Thu, 11 Dec 2025 11:51:12 -0800 Subject: [PATCH 03/28] [fix] texture usages implementation to exist within the texture.rs --- crates/lambda-rs-platform/src/wgpu/surface.rs | 49 ++-------- crates/lambda-rs-platform/src/wgpu/texture.rs | 92 +++++++++++++++---- crates/lambda-rs/src/render/mod.rs | 4 +- 3 files changed, 83 insertions(+), 62 deletions(-) diff --git a/crates/lambda-rs-platform/src/wgpu/surface.rs b/crates/lambda-rs-platform/src/wgpu/surface.rs index ff283100..103167d5 100644 --- a/crates/lambda-rs-platform/src/wgpu/surface.rs +++ b/crates/lambda-rs-platform/src/wgpu/surface.rs @@ -6,20 +6,27 @@ use wgpu::rwh::{ use super::{ gpu::Gpu, instance::Instance, + texture::TextureUsages, }; use crate::winit::WindowHandle; /// 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. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum PresentMode { + /// Vsync enabled; frames wait for vertical blanking interval. Fifo, + /// Vsync with relaxed timing; may tear if frames miss the interval. FifoRelaxed, + /// No Vsync; immediate presentation (may tear). Immediate, + /// Triple-buffered presentation when supported. Mailbox, + /// Automatic Vsync selection by the platform. AutoVsync, + /// Automatic non-Vsync selection by the platform. AutoNoVsync, } @@ -48,46 +55,6 @@ impl PresentMode { } } -/// Wrapper for texture usage flags used by surfaces. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -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); - } -} - /// Wrapper around a surface color format. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct SurfaceFormat(wgpu::TextureFormat); diff --git a/crates/lambda-rs-platform/src/wgpu/texture.rs b/crates/lambda-rs-platform/src/wgpu/texture.rs index 60acf215..5d5f8596 100644 --- a/crates/lambda-rs-platform/src/wgpu/texture.rs +++ b/crates/lambda-rs-platform/src/wgpu/texture.rs @@ -8,6 +8,60 @@ use wgpu; use crate::wgpu::gpu::Gpu; +/// Wrapper for texture usage flags. +/// +/// This abstraction hides `wgpu::TextureUsages` from higher layers while +/// preserving bitwise-OR composition for combining flags. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +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); + + /// Create an empty flags set. + pub const fn empty() -> Self { + return TextureUsages(wgpu::TextureUsages::empty()); + } + + 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); + } +} + +impl std::ops::BitOrAssign for TextureUsages { + fn bitor_assign(&mut self, rhs: TextureUsages) { + self.0 |= rhs.0; + } +} + #[derive(Debug)] /// Errors returned when building a texture or preparing its initial upload. pub enum TextureBuildError { @@ -551,16 +605,17 @@ pub struct TextureBuilder { height: u32, /// Depth in texels (1 for 2D). depth: u32, - /// Include `TEXTURE_BINDING` usage. - usage_texture_binding: bool, - /// Include `COPY_DST` usage when uploading initial data. - usage_copy_dst: bool, + /// Combined usage flags for the texture. + usage: TextureUsages, /// Optional tightly‑packed pixel payload for level 0 (rows are `width*bpp`). data: Option>, } impl TextureBuilder { /// Construct a new 2D texture builder for a color format. + /// + /// Default usage is `TEXTURE_BINDING | COPY_DST` for sampling with initial + /// data upload. pub fn new_2d(format: TextureFormat) -> Self { return Self { label: None, @@ -569,13 +624,15 @@ impl TextureBuilder { width: 0, height: 0, depth: 1, - usage_texture_binding: true, - usage_copy_dst: true, + usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST, data: None, }; } /// Construct a new 3D texture builder for a color format. + /// + /// Default usage is `TEXTURE_BINDING | COPY_DST` for sampling with initial + /// data upload. pub fn new_3d(format: TextureFormat) -> Self { return Self { label: None, @@ -584,8 +641,7 @@ impl TextureBuilder { width: 0, height: 0, depth: 0, - usage_texture_binding: true, - usage_copy_dst: true, + usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST, data: None, }; } @@ -612,10 +668,14 @@ impl TextureBuilder { return self; } - /// Control usage flags. Defaults are suitable for sampling with initial upload. - pub fn with_usage(mut self, texture_binding: bool, copy_dst: bool) -> Self { - self.usage_texture_binding = texture_binding; - self.usage_copy_dst = copy_dst; + /// Set the texture usage flags. + /// + /// Use bitwise-OR to combine flags: + /// ```ignore + /// .with_usage(TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST) + /// ``` + pub fn with_usage(mut self, usage: TextureUsages) -> Self { + self.usage = usage; return self; } @@ -676,13 +736,7 @@ impl TextureBuilder { } // Resolve usage flags - let mut usage = wgpu::TextureUsages::empty(); - if self.usage_texture_binding { - usage |= wgpu::TextureUsages::TEXTURE_BINDING; - } - if self.usage_copy_dst { - usage |= wgpu::TextureUsages::COPY_DST; - } + let usage = self.usage.to_wgpu(); let descriptor = wgpu::TextureDescriptor { label: self.label.as_deref(), diff --git a/crates/lambda-rs/src/render/mod.rs b/crates/lambda-rs/src/render/mod.rs index 96119bf0..ec48b13a 100644 --- a/crates/lambda-rs/src/render/mod.rs +++ b/crates/lambda-rs/src/render/mod.rs @@ -139,7 +139,7 @@ impl RenderContextBuilder { &gpu, size, platform::surface::PresentMode::Fifo, - platform::surface::TextureUsages::RENDER_ATTACHMENT, + platform::texture::TextureUsages::RENDER_ATTACHMENT, ) .map_err(|e| { RenderContextError::SurfaceConfig(format!( @@ -213,7 +213,7 @@ pub struct RenderContext { gpu: platform::gpu::Gpu, config: platform::surface::SurfaceConfig, present_mode: platform::surface::PresentMode, - texture_usage: platform::surface::TextureUsages, + texture_usage: platform::texture::TextureUsages, size: (u32, u32), depth_texture: Option, depth_format: platform::texture::DepthFormat, From df944d97285e0687e101082b9ded237f8adcc97f Mon Sep 17 00:00:00 2001 From: vmarcella Date: Thu, 11 Dec 2025 12:05:44 -0800 Subject: [PATCH 04/28] [update] TextureFormat to be the defacto API for all texture/surface formats. --- crates/lambda-rs-platform/src/wgpu/gpu.rs | 20 +-- .../lambda-rs-platform/src/wgpu/pipeline.rs | 10 +- crates/lambda-rs-platform/src/wgpu/surface.rs | 47 ++----- crates/lambda-rs-platform/src/wgpu/texture.rs | 121 ++++++++++++++---- ...u_bind_layout_and_group_texture_sampler.rs | 2 +- .../tests/wgpu_bind_layout_dim3_and_group.rs | 2 +- .../tests/wgpu_texture3d_build_and_upload.rs | 2 +- .../tests/wgpu_texture_build_and_upload.rs | 4 +- crates/lambda-rs/src/render/mod.rs | 4 +- crates/lambda-rs/src/render/pipeline.rs | 6 +- crates/lambda-rs/src/render/render_pass.rs | 6 +- crates/lambda-rs/src/render/texture.rs | 9 +- 12 files changed, 146 insertions(+), 87 deletions(-) diff --git a/crates/lambda-rs-platform/src/wgpu/gpu.rs b/crates/lambda-rs-platform/src/wgpu/gpu.rs index 67c39516..81da708e 100644 --- a/crates/lambda-rs-platform/src/wgpu/gpu.rs +++ b/crates/lambda-rs-platform/src/wgpu/gpu.rs @@ -200,10 +200,10 @@ pub struct Gpu { } impl Gpu { - /// Whether the provided surface format supports the sample count for render attachments. - pub fn supports_sample_count_for_surface( + /// Whether the provided texture format supports the sample count for render attachments. + pub fn supports_sample_count_for_format( &self, - format: super::surface::SurfaceFormat, + format: texture::TextureFormat, sample_count: u32, ) -> bool { return self.supports_sample_count(format.to_wgpu(), sample_count); @@ -350,11 +350,11 @@ mod tests { } }; let surface_format = - surface::SurfaceFormat::from_wgpu(wgpu::TextureFormat::Bgra8UnormSrgb); + texture::TextureFormat::from_wgpu(wgpu::TextureFormat::Bgra8UnormSrgb); let depth_format = texture::DepthFormat::Depth32Float; - assert!(gpu.supports_sample_count_for_surface(surface_format, 1)); - assert!(gpu.supports_sample_count_for_surface(surface_format, 0)); + assert!(gpu.supports_sample_count_for_format(surface_format, 1)); + assert!(gpu.supports_sample_count_for_format(surface_format, 0)); assert!(gpu.supports_sample_count_for_depth(depth_format, 1)); assert!(gpu.supports_sample_count_for_depth(depth_format, 0)); } @@ -372,10 +372,10 @@ mod tests { } }; let surface_format = - surface::SurfaceFormat::from_wgpu(wgpu::TextureFormat::Bgra8Unorm); + texture::TextureFormat::from_wgpu(wgpu::TextureFormat::Bgra8Unorm); let depth_format = texture::DepthFormat::Depth32Float; - assert!(!gpu.supports_sample_count_for_surface(surface_format, 3)); + assert!(!gpu.supports_sample_count_for_format(surface_format, 3)); assert!(!gpu.supports_sample_count_for_depth(depth_format, 3)); } @@ -393,7 +393,7 @@ no compatible GPU adapter" } }; let surface_format = - surface::SurfaceFormat::from_wgpu(wgpu::TextureFormat::Bgra8UnormSrgb); + texture::TextureFormat::from_wgpu(wgpu::TextureFormat::Bgra8UnormSrgb); let features = gpu .adapter .get_texture_format_features(surface_format.to_wgpu()); @@ -405,7 +405,7 @@ no compatible GPU adapter" .contains(wgpu::TextureFormatFeatureFlags::MULTISAMPLE_X4); assert_eq!( - gpu.supports_sample_count_for_surface(surface_format, 4), + gpu.supports_sample_count_for_format(surface_format, 4), expected ); } diff --git a/crates/lambda-rs-platform/src/wgpu/pipeline.rs b/crates/lambda-rs-platform/src/wgpu/pipeline.rs index df934feb..50b61dbf 100644 --- a/crates/lambda-rs-platform/src/wgpu/pipeline.rs +++ b/crates/lambda-rs-platform/src/wgpu/pipeline.rs @@ -8,8 +8,10 @@ pub use crate::wgpu::vertex::VertexStepMode; use crate::wgpu::{ bind, gpu::Gpu, - surface::SurfaceFormat, - texture::DepthFormat, + texture::{ + DepthFormat, + TextureFormat, + }, vertex::ColorFormat, }; @@ -378,8 +380,8 @@ impl<'a> RenderPipelineBuilder<'a> { return self; } - /// Set single color target for fragment stage from a surface format. - pub fn with_surface_color_target(mut self, format: SurfaceFormat) -> Self { + /// Set single color target for fragment stage from a texture format. + pub fn with_color_target(mut self, format: TextureFormat) -> Self { self.color_target_format = Some(format.to_wgpu()); return self; } diff --git a/crates/lambda-rs-platform/src/wgpu/surface.rs b/crates/lambda-rs-platform/src/wgpu/surface.rs index 103167d5..b1618ae3 100644 --- a/crates/lambda-rs-platform/src/wgpu/surface.rs +++ b/crates/lambda-rs-platform/src/wgpu/surface.rs @@ -6,7 +6,10 @@ use wgpu::rwh::{ use super::{ gpu::Gpu, instance::Instance, - texture::TextureUsages, + texture::{ + TextureFormat, + TextureUsages, + }, }; use crate::winit::WindowHandle; @@ -55,43 +58,15 @@ impl PresentMode { } } -/// Wrapper around a surface color format. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct SurfaceFormat(wgpu::TextureFormat); - -impl SurfaceFormat { - /// Common sRGB swapchain format used for windowed rendering. - pub const BGRA8_UNORM_SRGB: SurfaceFormat = - SurfaceFormat(wgpu::TextureFormat::Bgra8UnormSrgb); - - 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()); - } -} - /// Public, engine-facing surface configuration that avoids exposing `wgpu`. #[derive(Clone, Debug)] pub struct SurfaceConfig { pub width: u32, pub height: u32, - pub format: SurfaceFormat, + pub format: TextureFormat, pub present_mode: PresentMode, pub usage: TextureUsages, - pub view_formats: Vec, + pub view_formats: Vec, } impl SurfaceConfig { @@ -99,14 +74,14 @@ impl SurfaceConfig { return SurfaceConfig { width: config.width, height: config.height, - format: SurfaceFormat::from_wgpu(config.format), + format: TextureFormat::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) + .map(TextureFormat::from_wgpu) .collect(), }; } @@ -234,7 +209,7 @@ pub struct Surface<'window> { label: String, surface: wgpu::Surface<'window>, configuration: Option, - format: Option, + format: Option, } impl<'window> Surface<'window> { @@ -254,7 +229,7 @@ impl<'window> Surface<'window> { } /// Preferred surface format if known (set during configuration). - pub fn format(&self) -> Option { + pub fn format(&self) -> Option { return self.format; } @@ -266,7 +241,7 @@ impl<'window> Surface<'window> { ) { self.surface.configure(device, config); self.configuration = Some(SurfaceConfig::from_wgpu(config)); - self.format = Some(SurfaceFormat::from_wgpu(config.format)); + self.format = Some(TextureFormat::from_wgpu(config.format)); } /// Configure the surface using common engine defaults: diff --git a/crates/lambda-rs-platform/src/wgpu/texture.rs b/crates/lambda-rs-platform/src/wgpu/texture.rs index 5d5f8596..d8837a43 100644 --- a/crates/lambda-rs-platform/src/wgpu/texture.rs +++ b/crates/lambda-rs-platform/src/wgpu/texture.rs @@ -71,6 +71,8 @@ pub enum TextureBuildError { DataLengthMismatch { expected: usize, actual: usize }, /// Internal arithmetic overflow while computing sizes or paddings. Overflow, + /// The texture format does not support bytes_per_pixel calculation. + UnsupportedFormat, } /// Align `value` up to the next multiple of `alignment`. @@ -116,26 +118,85 @@ impl AddressMode { } } -/// Supported color texture formats for sampling. +/// Unified texture format wrapper. +/// +/// This abstraction wraps `wgpu::TextureFormat` to hide the underlying graphics +/// API from higher layers. Common formats are exposed as associated constants. #[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub enum TextureFormat { - Rgba8Unorm, - Rgba8UnormSrgb, -} +pub struct TextureFormat(wgpu::TextureFormat); impl TextureFormat { - /// Map to the corresponding `wgpu::TextureFormat`. + // ------------------------------------------------------------------------- + // Common color formats + // ------------------------------------------------------------------------- + + /// 8-bit RGBA, linear (non-sRGB). + pub const RGBA8_UNORM: TextureFormat = + TextureFormat(wgpu::TextureFormat::Rgba8Unorm); + /// 8-bit RGBA, sRGB encoded. + pub const RGBA8_UNORM_SRGB: TextureFormat = + TextureFormat(wgpu::TextureFormat::Rgba8UnormSrgb); + /// 8-bit BGRA, linear (non-sRGB). Common swapchain format. + pub const BGRA8_UNORM: TextureFormat = + TextureFormat(wgpu::TextureFormat::Bgra8Unorm); + /// 8-bit BGRA, sRGB encoded. Common swapchain format. + pub const BGRA8_UNORM_SRGB: TextureFormat = + TextureFormat(wgpu::TextureFormat::Bgra8UnormSrgb); + + // ------------------------------------------------------------------------- + // Depth/stencil formats + // ------------------------------------------------------------------------- + + /// 32-bit floating point depth. + pub const DEPTH32_FLOAT: TextureFormat = + TextureFormat(wgpu::TextureFormat::Depth32Float); + /// 24-bit depth (platform may choose precision). + pub const DEPTH24_PLUS: TextureFormat = + TextureFormat(wgpu::TextureFormat::Depth24Plus); + /// 24-bit depth + 8-bit stencil. + pub const DEPTH24_PLUS_STENCIL8: TextureFormat = + TextureFormat(wgpu::TextureFormat::Depth24PlusStencil8); + + // ------------------------------------------------------------------------- + // Conversions + // ------------------------------------------------------------------------- + pub(crate) fn to_wgpu(self) -> wgpu::TextureFormat { - return match self { - TextureFormat::Rgba8Unorm => wgpu::TextureFormat::Rgba8Unorm, - TextureFormat::Rgba8UnormSrgb => wgpu::TextureFormat::Rgba8UnormSrgb, - }; + return self.0; } - /// Number of bytes per pixel for tightly packed data. - pub fn bytes_per_pixel(self) -> u32 { - return match self { - TextureFormat::Rgba8Unorm | TextureFormat::Rgba8UnormSrgb => 4, + pub(crate) fn from_wgpu(fmt: wgpu::TextureFormat) -> Self { + return TextureFormat(fmt); + } + + // ------------------------------------------------------------------------- + // Format queries + // ------------------------------------------------------------------------- + + /// Whether this format is sRGB encoded. + 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 TextureFormat(self.0.add_srgb_suffix()); + } + + /// Number of bytes per pixel for common formats. + /// + /// Returns `None` for compressed or exotic formats where a simple + /// bytes-per-pixel value is not applicable. + pub fn bytes_per_pixel(self) -> Option { + return match self.0 { + wgpu::TextureFormat::Rgba8Unorm + | wgpu::TextureFormat::Rgba8UnormSrgb + | wgpu::TextureFormat::Bgra8Unorm + | wgpu::TextureFormat::Bgra8UnormSrgb => Some(4), + wgpu::TextureFormat::Depth32Float => Some(4), + wgpu::TextureFormat::Depth24Plus => Some(4), + wgpu::TextureFormat::Depth24PlusStencil8 => Some(4), + _ => None, }; } } @@ -221,13 +282,13 @@ pub struct ColorAttachmentTextureBuilder { label: Option, width: u32, height: u32, - format: crate::wgpu::surface::SurfaceFormat, + format: TextureFormat, sample_count: u32, } impl ColorAttachmentTextureBuilder { /// Create a builder with zero size and sample count 1. - pub fn new(format: crate::wgpu::surface::SurfaceFormat) -> Self { + pub fn new(format: TextureFormat) -> Self { return Self { label: None, width: 0, @@ -714,7 +775,10 @@ impl TextureBuilder { // Validate data length if provided if let Some(ref pixels) = self.data { - let bpp = self.format.bytes_per_pixel() as usize; + let bpp = self + .format + .bytes_per_pixel() + .ok_or(TextureBuildError::UnsupportedFormat)? as usize; let wh = (self.width as usize) .checked_mul(self.height as usize) .ok_or(TextureBuildError::Overflow)?; @@ -768,7 +832,10 @@ impl TextureBuilder { if let Some(pixels) = self.data.as_ref() { // Compute 256-byte aligned bytes_per_row and pad rows if necessary. - let bpp = self.format.bytes_per_pixel(); + let bpp = self + .format + .bytes_per_pixel() + .ok_or(TextureBuildError::UnsupportedFormat)?; let row_bytes = self .width .checked_mul(bpp) @@ -889,19 +956,29 @@ mod tests { #[test] fn texture_format_maps() { assert_eq!( - TextureFormat::Rgba8Unorm.to_wgpu(), + TextureFormat::RGBA8_UNORM.to_wgpu(), wgpu::TextureFormat::Rgba8Unorm ); assert_eq!( - TextureFormat::Rgba8UnormSrgb.to_wgpu(), + TextureFormat::RGBA8_UNORM_SRGB.to_wgpu(), wgpu::TextureFormat::Rgba8UnormSrgb ); + assert_eq!( + TextureFormat::BGRA8_UNORM.to_wgpu(), + wgpu::TextureFormat::Bgra8Unorm + ); + assert_eq!( + TextureFormat::BGRA8_UNORM_SRGB.to_wgpu(), + wgpu::TextureFormat::Bgra8UnormSrgb + ); } #[test] fn bytes_per_pixel_is_correct() { - assert_eq!(TextureFormat::Rgba8Unorm.bytes_per_pixel(), 4); - assert_eq!(TextureFormat::Rgba8UnormSrgb.bytes_per_pixel(), 4); + assert_eq!(TextureFormat::RGBA8_UNORM.bytes_per_pixel(), Some(4)); + assert_eq!(TextureFormat::RGBA8_UNORM_SRGB.bytes_per_pixel(), Some(4)); + assert_eq!(TextureFormat::BGRA8_UNORM.bytes_per_pixel(), Some(4)); + assert_eq!(TextureFormat::BGRA8_UNORM_SRGB.bytes_per_pixel(), Some(4)); } #[test] diff --git a/crates/lambda-rs-platform/tests/wgpu_bind_layout_and_group_texture_sampler.rs b/crates/lambda-rs-platform/tests/wgpu_bind_layout_and_group_texture_sampler.rs index 3339c751..9bf4ba73 100644 --- a/crates/lambda-rs-platform/tests/wgpu_bind_layout_and_group_texture_sampler.rs +++ b/crates/lambda-rs-platform/tests/wgpu_bind_layout_and_group_texture_sampler.rs @@ -19,7 +19,7 @@ fn wgpu_bind_layout_and_group_texture_sampler() { let (w, h) = (4u32, 4u32); let pixels = vec![255u8; (w * h * 4) as usize]; let texture = lambda_platform::wgpu::texture::TextureBuilder::new_2d( - lambda_platform::wgpu::texture::TextureFormat::Rgba8Unorm, + lambda_platform::wgpu::texture::TextureFormat::RGBA8_UNORM, ) .with_size(w, h) .with_data(&pixels) diff --git a/crates/lambda-rs-platform/tests/wgpu_bind_layout_dim3_and_group.rs b/crates/lambda-rs-platform/tests/wgpu_bind_layout_dim3_and_group.rs index 63bdff27..76c34c45 100644 --- a/crates/lambda-rs-platform/tests/wgpu_bind_layout_dim3_and_group.rs +++ b/crates/lambda-rs-platform/tests/wgpu_bind_layout_dim3_and_group.rs @@ -15,7 +15,7 @@ fn wgpu_bind_layout_dim3_and_group() { let (w, h, d) = (2u32, 2u32, 2u32); let pixels = vec![255u8; (w * h * d * 4) as usize]; let tex3d = lambda_platform::wgpu::texture::TextureBuilder::new_3d( - lambda_platform::wgpu::texture::TextureFormat::Rgba8Unorm, + lambda_platform::wgpu::texture::TextureFormat::RGBA8_UNORM, ) .with_size_3d(w, h, d) .with_data(&pixels) diff --git a/crates/lambda-rs-platform/tests/wgpu_texture3d_build_and_upload.rs b/crates/lambda-rs-platform/tests/wgpu_texture3d_build_and_upload.rs index 4d19bec5..9b4e44a1 100644 --- a/crates/lambda-rs-platform/tests/wgpu_texture3d_build_and_upload.rs +++ b/crates/lambda-rs-platform/tests/wgpu_texture3d_build_and_upload.rs @@ -16,7 +16,7 @@ fn wgpu_texture3d_build_and_upload() { let pixels = vec![180u8; (w * h * d * 4) as usize]; let _tex3d = lambda_platform::wgpu::texture::TextureBuilder::new_3d( - lambda_platform::wgpu::texture::TextureFormat::Rgba8Unorm, + lambda_platform::wgpu::texture::TextureFormat::RGBA8_UNORM, ) .with_size_3d(w, h, d) .with_data(&pixels) diff --git a/crates/lambda-rs-platform/tests/wgpu_texture_build_and_upload.rs b/crates/lambda-rs-platform/tests/wgpu_texture_build_and_upload.rs index 259197f6..a96de812 100644 --- a/crates/lambda-rs-platform/tests/wgpu_texture_build_and_upload.rs +++ b/crates/lambda-rs-platform/tests/wgpu_texture_build_and_upload.rs @@ -30,7 +30,7 @@ fn wgpu_texture_build_and_upload_succeeds() { } let _texture = lambda_platform::wgpu::texture::TextureBuilder::new_2d( - lambda_platform::wgpu::texture::TextureFormat::Rgba8UnormSrgb, + lambda_platform::wgpu::texture::TextureFormat::RGBA8_UNORM_SRGB, ) .with_size(w, h) .with_data(&pixels) @@ -46,7 +46,7 @@ fn wgpu_texture_upload_with_padding_bytes_per_row() { let (w, h) = (13u32, 7u32); let pixels = vec![128u8; (w * h * 4) as usize]; let _ = lambda_platform::wgpu::texture::TextureBuilder::new_2d( - lambda_platform::wgpu::texture::TextureFormat::Rgba8Unorm, + lambda_platform::wgpu::texture::TextureFormat::RGBA8_UNORM, ) .with_size(w, h) .with_data(&pixels) diff --git a/crates/lambda-rs/src/render/mod.rs b/crates/lambda-rs/src/render/mod.rs index ec48b13a..5d373b54 100644 --- a/crates/lambda-rs/src/render/mod.rs +++ b/crates/lambda-rs/src/render/mod.rs @@ -344,7 +344,7 @@ impl RenderContext { return &self.gpu; } - pub(crate) fn surface_format(&self) -> platform::surface::SurfaceFormat { + pub(crate) fn surface_format(&self) -> platform::texture::TextureFormat { return self.config.format; } @@ -358,7 +358,7 @@ impl RenderContext { ) -> bool { return self .gpu - .supports_sample_count_for_surface(self.config.format, sample_count); + .supports_sample_count_for_format(self.config.format, sample_count); } pub(crate) fn supports_depth_sample_count( diff --git a/crates/lambda-rs/src/render/pipeline.rs b/crates/lambda-rs/src/render/pipeline.rs index aad3bfbd..719d240b 100644 --- a/crates/lambda-rs/src/render/pipeline.rs +++ b/crates/lambda-rs/src/render/pipeline.rs @@ -226,12 +226,12 @@ pub struct StencilFaceState { impl StencilFaceState { fn to_platform(self) -> platform_pipeline::StencilFaceState { - platform_pipeline::StencilFaceState { + return platform_pipeline::StencilFaceState { compare: self.compare.to_platform(), fail_op: self.fail_op.to_platform(), depth_fail_op: self.depth_fail_op.to_platform(), pass_op: self.pass_op.to_platform(), - } + }; } } @@ -555,7 +555,7 @@ impl RenderPipelineBuilder { } if fragment_module.is_some() { - rp_builder = rp_builder.with_surface_color_target(surface_format); + rp_builder = rp_builder.with_color_target(surface_format); } if self.use_depth { diff --git a/crates/lambda-rs/src/render/render_pass.rs b/crates/lambda-rs/src/render/render_pass.rs index 6d95fd63..d283b802 100644 --- a/crates/lambda-rs/src/render/render_pass.rs +++ b/crates/lambda-rs/src/render/render_pass.rs @@ -302,7 +302,7 @@ impl RenderPassBuilder { fn resolve_sample_count( &self, sample_count: u32, - surface_format: platform::surface::SurfaceFormat, + surface_format: platform::texture::TextureFormat, depth_format: platform::texture::DepthFormat, supports_surface: FSurface, supports_depth: FDepth, @@ -380,8 +380,8 @@ mod tests { use super::*; - fn surface_format() -> platform::surface::SurfaceFormat { - return platform::surface::SurfaceFormat::BGRA8_UNORM_SRGB; + fn surface_format() -> platform::texture::TextureFormat { + return platform::texture::TextureFormat::BGRA8_UNORM_SRGB; } /// Falls back when the surface format rejects the requested sample count. diff --git a/crates/lambda-rs/src/render/texture.rs b/crates/lambda-rs/src/render/texture.rs index acd87060..c7877c12 100644 --- a/crates/lambda-rs/src/render/texture.rs +++ b/crates/lambda-rs/src/render/texture.rs @@ -41,8 +41,10 @@ pub enum TextureFormat { impl TextureFormat { fn to_platform(self) -> platform::TextureFormat { return match self { - TextureFormat::Rgba8Unorm => platform::TextureFormat::Rgba8Unorm, - TextureFormat::Rgba8UnormSrgb => platform::TextureFormat::Rgba8UnormSrgb, + TextureFormat::Rgba8Unorm => platform::TextureFormat::RGBA8_UNORM, + TextureFormat::Rgba8UnormSrgb => { + platform::TextureFormat::RGBA8_UNORM_SRGB + } }; } } @@ -223,6 +225,9 @@ impl TextureBuilder { Err(platform::TextureBuildError::Overflow) => { Err("Overflow while computing texture layout") } + Err(platform::TextureBuildError::UnsupportedFormat) => { + Err("Texture format does not support bytes_per_pixel calculation") + } }; } } From afb144c5878786ab84ed15c810545708ceba2d93 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Thu, 11 Dec 2025 12:06:43 -0800 Subject: [PATCH 05/28] [update] configure_raw to not be public. --- crates/lambda-rs-platform/src/wgpu/surface.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/lambda-rs-platform/src/wgpu/surface.rs b/crates/lambda-rs-platform/src/wgpu/surface.rs index b1618ae3..f2b9308e 100644 --- a/crates/lambda-rs-platform/src/wgpu/surface.rs +++ b/crates/lambda-rs-platform/src/wgpu/surface.rs @@ -234,7 +234,7 @@ impl<'window> Surface<'window> { } /// Configure the surface and cache the result for queries such as `format()`. - pub(crate) fn configure_raw( + fn configure_raw( &mut self, device: &wgpu::Device, config: &wgpu::SurfaceConfiguration, From 4932f60674acb05cc2370a90634ed146f5c0f246 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Thu, 11 Dec 2025 12:16:46 -0800 Subject: [PATCH 06/28] [add] high level texture usages abstraction. --- crates/lambda-rs/src/render/mod.rs | 8 +-- crates/lambda-rs/src/render/texture.rs | 98 +++++++++++++++++++++++++- 2 files changed, 99 insertions(+), 7 deletions(-) diff --git a/crates/lambda-rs/src/render/mod.rs b/crates/lambda-rs/src/render/mod.rs index 5d373b54..1d6f8658 100644 --- a/crates/lambda-rs/src/render/mod.rs +++ b/crates/lambda-rs/src/render/mod.rs @@ -139,7 +139,7 @@ impl RenderContextBuilder { &gpu, size, platform::surface::PresentMode::Fifo, - platform::texture::TextureUsages::RENDER_ATTACHMENT, + texture::TextureUsages::RENDER_ATTACHMENT.to_platform(), ) .map_err(|e| { RenderContextError::SurfaceConfig(format!( @@ -154,7 +154,7 @@ impl RenderContextBuilder { ) })?; let present_mode = config.present_mode; - let texture_usage = config.usage; + let texture_usage = texture::TextureUsages::from_platform(config.usage); // Initialize a depth texture matching the surface size. let depth_format = platform::texture::DepthFormat::Depth32Float; @@ -213,7 +213,7 @@ pub struct RenderContext { gpu: platform::gpu::Gpu, config: platform::surface::SurfaceConfig, present_mode: platform::surface::PresentMode, - texture_usage: platform::texture::TextureUsages, + texture_usage: texture::TextureUsages, size: (u32, u32), depth_texture: Option, depth_format: platform::texture::DepthFormat, @@ -1044,7 +1044,7 @@ impl RenderContext { })?; self.present_mode = config.present_mode; - self.texture_usage = config.usage; + self.texture_usage = texture::TextureUsages::from_platform(config.usage); self.config = config; return Ok(()); } diff --git a/crates/lambda-rs/src/render/texture.rs b/crates/lambda-rs/src/render/texture.rs index c7877c12..4dce73ce 100644 --- a/crates/lambda-rs/src/render/texture.rs +++ b/crates/lambda-rs/src/render/texture.rs @@ -31,22 +31,61 @@ impl DepthFormat { } } -#[derive(Debug, Clone, Copy)] -/// Supported color texture formats for sampling. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// Supported color texture formats for sampling and render targets. pub enum TextureFormat { + /// 8-bit RGBA, linear (non-sRGB). Rgba8Unorm, + /// 8-bit RGBA, sRGB encoded. Rgba8UnormSrgb, + /// 8-bit BGRA, linear (non-sRGB). Common swapchain format. + Bgra8Unorm, + /// 8-bit BGRA, sRGB encoded. Common swapchain format. + Bgra8UnormSrgb, } impl TextureFormat { - fn to_platform(self) -> platform::TextureFormat { + pub(crate) fn to_platform(self) -> platform::TextureFormat { return match self { TextureFormat::Rgba8Unorm => platform::TextureFormat::RGBA8_UNORM, TextureFormat::Rgba8UnormSrgb => { platform::TextureFormat::RGBA8_UNORM_SRGB } + TextureFormat::Bgra8Unorm => platform::TextureFormat::BGRA8_UNORM, + TextureFormat::Bgra8UnormSrgb => { + platform::TextureFormat::BGRA8_UNORM_SRGB + } }; } + + pub(crate) fn from_platform(fmt: platform::TextureFormat) -> Option { + if fmt == platform::TextureFormat::RGBA8_UNORM { + return Some(TextureFormat::Rgba8Unorm); + } + if fmt == platform::TextureFormat::RGBA8_UNORM_SRGB { + return Some(TextureFormat::Rgba8UnormSrgb); + } + if fmt == platform::TextureFormat::BGRA8_UNORM { + return Some(TextureFormat::Bgra8Unorm); + } + if fmt == platform::TextureFormat::BGRA8_UNORM_SRGB { + return Some(TextureFormat::Bgra8UnormSrgb); + } + return None; + } + + /// Whether this format is sRGB encoded. + pub fn is_srgb(self) -> bool { + return matches!( + self, + TextureFormat::Rgba8UnormSrgb | TextureFormat::Bgra8UnormSrgb + ); + } + + /// Number of bytes per pixel for this format. + pub fn bytes_per_pixel(self) -> u32 { + return 4; + } } #[derive(Debug, Clone, Copy)] @@ -98,6 +137,59 @@ impl AddressMode { }; } } + +/// Texture usage flags. +/// +/// Use bitwise-OR to combine flags when creating textures with multiple usages. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct TextureUsages(platform::TextureUsages); + +impl TextureUsages { + /// Texture can be used as a render attachment (color or depth/stencil). + pub const RENDER_ATTACHMENT: TextureUsages = + TextureUsages(platform::TextureUsages::RENDER_ATTACHMENT); + /// Texture can be bound for sampling in shaders. + pub const TEXTURE_BINDING: TextureUsages = + TextureUsages(platform::TextureUsages::TEXTURE_BINDING); + /// Texture can be used as the destination of a copy operation. + pub const COPY_DST: TextureUsages = + TextureUsages(platform::TextureUsages::COPY_DST); + /// Texture can be used as the source of a copy operation. + pub const COPY_SRC: TextureUsages = + TextureUsages(platform::TextureUsages::COPY_SRC); + + /// Create an empty flags set. + pub const fn empty() -> Self { + return TextureUsages(platform::TextureUsages::empty()); + } + + pub(crate) fn to_platform(self) -> platform::TextureUsages { + return self.0; + } + + pub(crate) fn from_platform(usage: platform::TextureUsages) -> Self { + return TextureUsages(usage); + } + + /// 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); + } +} + +impl std::ops::BitOrAssign for TextureUsages { + fn bitor_assign(&mut self, rhs: TextureUsages) { + self.0 |= rhs.0; + } +} #[derive(Debug, Clone)] /// High‑level texture wrapper that owns a platform texture. pub struct Texture { From 3103b0945f54fd50085d422ec5b842862f0e3b1f Mon Sep 17 00:00:00 2001 From: vmarcella Date: Thu, 11 Dec 2025 12:32:14 -0800 Subject: [PATCH 07/28] [add] initial implementations for high level surface types. --- crates/lambda-rs/src/render/mod.rs | 52 ++++++++------- crates/lambda-rs/src/render/pipeline.rs | 2 +- crates/lambda-rs/src/render/render_pass.rs | 2 +- crates/lambda-rs/src/render/surface.rs | 75 ++++++++++++++++++++-- 4 files changed, 101 insertions(+), 30 deletions(-) diff --git a/crates/lambda-rs/src/render/mod.rs b/crates/lambda-rs/src/render/mod.rs index 1d6f8658..65f20c41 100644 --- a/crates/lambda-rs/src/render/mod.rs +++ b/crates/lambda-rs/src/render/mod.rs @@ -138,7 +138,7 @@ impl RenderContextBuilder { .configure_with_defaults( &gpu, size, - platform::surface::PresentMode::Fifo, + surface::PresentMode::default().to_platform(), texture::TextureUsages::RENDER_ATTACHMENT.to_platform(), ) .map_err(|e| { @@ -148,13 +148,14 @@ impl RenderContextBuilder { )) })?; - let config = surface.configuration().cloned().ok_or_else(|| { + let config = surface.configuration().ok_or_else(|| { RenderContextError::SurfaceConfig( "Surface was not configured".to_string(), ) })?; + let config = surface::SurfaceConfig::from_platform(config); let present_mode = config.present_mode; - let texture_usage = texture::TextureUsages::from_platform(config.usage); + let texture_usage = config.usage; // Initialize a depth texture matching the surface size. let depth_format = platform::texture::DepthFormat::Depth32Float; @@ -211,8 +212,8 @@ pub struct RenderContext { instance: platform::instance::Instance, surface: platform::surface::Surface<'static>, gpu: platform::gpu::Gpu, - config: platform::surface::SurfaceConfig, - present_mode: platform::surface::PresentMode, + config: surface::SurfaceConfig, + present_mode: surface::PresentMode, texture_usage: texture::TextureUsages, size: (u32, u32), depth_texture: Option, @@ -344,7 +345,7 @@ impl RenderContext { return &self.gpu; } - pub(crate) fn surface_format(&self) -> platform::texture::TextureFormat { + pub(crate) fn surface_format(&self) -> texture::TextureFormat { return self.config.format; } @@ -356,9 +357,10 @@ impl RenderContext { &self, sample_count: u32, ) -> bool { - return self - .gpu - .supports_sample_count_for_format(self.config.format, sample_count); + return self.gpu.supports_sample_count_for_format( + self.config.format.to_platform(), + sample_count, + ); } pub(crate) fn supports_depth_sample_count( @@ -407,15 +409,18 @@ impl RenderContext { let mut frame = match self.surface.acquire_next_frame() { Ok(frame) => frame, - Err(platform::surface::SurfaceError::Lost) - | Err(platform::surface::SurfaceError::Outdated) => { - self.reconfigure_surface(self.size)?; - self - .surface - .acquire_next_frame() - .map_err(RenderError::Surface)? + Err(err) => { + let high_level_err = surface::SurfaceError::from(err); + match high_level_err { + surface::SurfaceError::Lost | surface::SurfaceError::Outdated => { + self.reconfigure_surface(self.size)?; + self.surface.acquire_next_frame().map_err(|e| { + RenderError::Surface(surface::SurfaceError::from(e)) + })? + } + _ => return Err(RenderError::Surface(high_level_err)), + } } - Err(err) => return Err(RenderError::Surface(err)), }; let view = frame.texture_view(); @@ -472,7 +477,7 @@ impl RenderContext { if need_recreate { self.msaa_color = Some( platform::texture::ColorAttachmentTextureBuilder::new( - self.config.format, + self.config.format.to_platform(), ) .with_size(self.size.0.max(1), self.size.1.max(1)) .with_sample_count(sample_count) @@ -1039,12 +1044,13 @@ impl RenderContext { .resize(&self.gpu, size) .map_err(RenderError::Configuration)?; - let config = self.surface.configuration().cloned().ok_or_else(|| { + let platform_config = self.surface.configuration().ok_or_else(|| { RenderError::Configuration("Surface was not configured".to_string()) })?; + let config = surface::SurfaceConfig::from_platform(platform_config); self.present_mode = config.present_mode; - self.texture_usage = texture::TextureUsages::from_platform(config.usage); + self.texture_usage = config.usage; self.config = config; return Ok(()); } @@ -1089,12 +1095,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(platform::surface::SurfaceError), + Surface(surface::SurfaceError), Configuration(String), } -impl From for RenderError { - fn from(error: platform::surface::SurfaceError) -> Self { +impl From for RenderError { + fn from(error: surface::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 719d240b..aff89869 100644 --- a/crates/lambda-rs/src/render/pipeline.rs +++ b/crates/lambda-rs/src/render/pipeline.rs @@ -555,7 +555,7 @@ impl RenderPipelineBuilder { } if fragment_module.is_some() { - rp_builder = rp_builder.with_color_target(surface_format); + rp_builder = rp_builder.with_color_target(surface_format.to_platform()); } if self.use_depth { diff --git a/crates/lambda-rs/src/render/render_pass.rs b/crates/lambda-rs/src/render/render_pass.rs index d283b802..5412553d 100644 --- a/crates/lambda-rs/src/render/render_pass.rs +++ b/crates/lambda-rs/src/render/render_pass.rs @@ -280,7 +280,7 @@ impl RenderPassBuilder { pub fn build(self, render_context: &RenderContext) -> RenderPass { let sample_count = self.resolve_sample_count( self.sample_count, - render_context.surface_format(), + render_context.surface_format().to_platform(), render_context.depth_format(), |count| render_context.supports_surface_sample_count(count), |format, count| render_context.supports_depth_sample_count(format, count), diff --git a/crates/lambda-rs/src/render/surface.rs b/crates/lambda-rs/src/render/surface.rs index 802a0856..ef0a6fc3 100644 --- a/crates/lambda-rs/src/render/surface.rs +++ b/crates/lambda-rs/src/render/surface.rs @@ -1,5 +1,10 @@ use lambda_platform::wgpu::surface as platform_surface; +use super::texture::{ + TextureFormat, + TextureUsages, +}; + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum PresentMode { /// Vsync enabled; frames wait for vertical blanking interval. @@ -19,28 +24,28 @@ pub enum PresentMode { impl PresentMode { #[inline] pub(crate) fn to_platform(&self) -> platform_surface::PresentMode { - match self { + return match self { PresentMode::Fifo => platform_surface::PresentMode::Fifo, PresentMode::FifoRelaxed => platform_surface::PresentMode::FifoRelaxed, PresentMode::Immediate => platform_surface::PresentMode::Immediate, PresentMode::Mailbox => platform_surface::PresentMode::Mailbox, PresentMode::AutoVsync => platform_surface::PresentMode::AutoVsync, PresentMode::AutoNoVsync => platform_surface::PresentMode::AutoNoVsync, - } + }; } #[inline] pub(crate) fn from_platform( - mode: &platform_surface::PresentMode, + mode: platform_surface::PresentMode, ) -> PresentMode { - match mode { + return match mode { platform_surface::PresentMode::Fifo => PresentMode::Fifo, platform_surface::PresentMode::FifoRelaxed => PresentMode::FifoRelaxed, platform_surface::PresentMode::Immediate => PresentMode::Immediate, platform_surface::PresentMode::Mailbox => PresentMode::Mailbox, platform_surface::PresentMode::AutoVsync => PresentMode::AutoVsync, platform_surface::PresentMode::AutoNoVsync => PresentMode::AutoNoVsync, - } + }; } } @@ -49,3 +54,63 @@ impl Default for PresentMode { return PresentMode::Fifo; } } + +/// High-level surface configuration. +/// +/// Contains the current surface dimensions, format, present mode, and usage +/// flags without exposing platform types. +#[derive(Clone, Debug)] +pub struct SurfaceConfig { + /// Width in pixels. + pub width: u32, + /// Height in pixels. + pub height: u32, + /// The texture format used by the surface. + pub format: TextureFormat, + /// The presentation mode (vsync behavior). + pub present_mode: PresentMode, + /// Texture usage flags for the surface. + pub usage: TextureUsages, +} + +impl SurfaceConfig { + pub(crate) fn from_platform( + config: &platform_surface::SurfaceConfig, + ) -> Self { + return SurfaceConfig { + width: config.width, + height: config.height, + format: TextureFormat::from_platform(config.format) + .unwrap_or(TextureFormat::Bgra8UnormSrgb), + present_mode: PresentMode::from_platform(config.present_mode), + usage: TextureUsages::from_platform(config.usage), + }; + } +} + +/// Error wrapper for surface acquisition and presentation errors. +#[derive(Clone, Debug)] +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. + Other(String), +} + +impl From for SurfaceError { + fn from(error: platform_surface::SurfaceError) -> Self { + return match error { + platform_surface::SurfaceError::Lost => SurfaceError::Lost, + platform_surface::SurfaceError::Outdated => SurfaceError::Outdated, + platform_surface::SurfaceError::OutOfMemory => SurfaceError::OutOfMemory, + platform_surface::SurfaceError::Timeout => SurfaceError::Timeout, + platform_surface::SurfaceError::Other(msg) => SurfaceError::Other(msg), + }; + } +} From 53c8a9fa9fef20a1c8d2b687abe1462dd2448cff Mon Sep 17 00:00:00 2001 From: vmarcella Date: Thu, 11 Dec 2025 12:58:46 -0800 Subject: [PATCH 08/28] [add] high level texture view and frame implementations. --- .../lambda-rs/src/render/color_attachments.rs | 82 ++++++------ crates/lambda-rs/src/render/mod.rs | 120 ++++++++++++------ crates/lambda-rs/src/render/surface.rs | 78 ++++++++++++ 3 files changed, 193 insertions(+), 87 deletions(-) diff --git a/crates/lambda-rs/src/render/color_attachments.rs b/crates/lambda-rs/src/render/color_attachments.rs index a2a73561..30895db1 100644 --- a/crates/lambda-rs/src/render/color_attachments.rs +++ b/crates/lambda-rs/src/render/color_attachments.rs @@ -6,10 +6,7 @@ use lambda_platform::wgpu as platform; -use super::{ - render_pass::RenderPass as RenderPassDesc, - RenderContext, -}; +use super::surface::TextureView; #[derive(Debug, Default)] /// High‑level color attachments collection used when beginning a render pass. @@ -30,20 +27,26 @@ impl<'view> RenderColorAttachments<'view> { } /// Append a color attachment targeting the provided texture view. - pub(crate) fn push_color( - &mut self, - view: platform::surface::TextureViewRef<'view>, - ) { - self.inner.push_color(view); + /// + /// Accepts a high-level `TextureView` and converts it internally to the + /// platform type. + pub(crate) fn push_color(&mut self, view: TextureView<'view>) { + self.inner.push_color(view.to_platform()); } /// Append a multi‑sampled color attachment with a resolve target view. + /// + /// The `msaa_view` is the multi-sampled render target, and `resolve_view` + /// is the single-sample target that receives the resolved output. + /// Both accept high-level `TextureView` types and convert internally. pub(crate) fn push_msaa_color( &mut self, - msaa_view: platform::surface::TextureViewRef<'view>, - resolve_view: platform::surface::TextureViewRef<'view>, + msaa_view: TextureView<'view>, + resolve_view: TextureView<'view>, ) { - self.inner.push_msaa_color(msaa_view, resolve_view); + self + .inner + .push_msaa_color(msaa_view.to_platform(), resolve_view.to_platform()); } /// Borrow the underlying platform attachments mutably for pass creation. @@ -53,48 +56,35 @@ impl<'view> RenderColorAttachments<'view> { return &mut self.inner; } - /// Build color attachments for a surface‑backed render pass. + /// Build color attachments for a surface-backed render pass. /// - /// This helper encapsulates the logic for configuring single‑sample and - /// multi‑sample color attachments targeting the presentation surface, - /// including creation and reuse of the MSAA resolve target stored on the - /// `RenderContext`. + /// This helper configures single-sample or multi-sample color attachments + /// targeting the presentation surface. The MSAA view is optional and should + /// be provided when multi-sampling is enabled. + /// + /// # Arguments + /// * `uses_color` - Whether the render pass uses color output. + /// * `sample_count` - The MSAA sample count (1 for no MSAA). + /// * `msaa_view` - Optional high-level MSAA texture view (required when + /// `sample_count > 1`). + /// * `surface_view` - The high-level surface texture view (resolve target + /// for MSAA, or direct target for single-sample). pub(crate) fn for_surface_pass( - render_context: &mut RenderContext, - pass: &RenderPassDesc, - surface_view: platform::surface::TextureViewRef<'view>, + uses_color: bool, + sample_count: u32, + msaa_view: Option>, + surface_view: TextureView<'view>, ) -> Self { let mut attachments = RenderColorAttachments::new(); - if !pass.uses_color() { + + if !uses_color { return attachments; } - let sample_count = pass.sample_count(); if sample_count > 1 { - let need_recreate = match &render_context.msaa_color { - Some(_existing) => render_context.msaa_sample_count != sample_count, - None => true, - }; - - if need_recreate { - render_context.msaa_color = Some( - platform::texture::ColorAttachmentTextureBuilder::new( - render_context.config.format, - ) - .with_size(render_context.size.0.max(1), render_context.size.1.max(1)) - .with_sample_count(sample_count) - .with_label("lambda-msaa-color") - .build(render_context.gpu()), - ); - render_context.msaa_sample_count = sample_count; - } - - let msaa_view = render_context - .msaa_color - .as_ref() - .expect("MSAA color attachment should be created") - .view_ref(); - attachments.push_msaa_color(msaa_view, surface_view); + let msaa = + msaa_view.expect("MSAA view must be provided when sample_count > 1"); + attachments.push_msaa_color(msaa, surface_view); } else { attachments.push_color(surface_view); } diff --git a/crates/lambda-rs/src/render/mod.rs b/crates/lambda-rs/src/render/mod.rs index 65f20c41..1020766d 100644 --- a/crates/lambda-rs/src/render/mod.rs +++ b/crates/lambda-rs/src/render/mod.rs @@ -44,6 +44,9 @@ pub mod vertex; pub mod viewport; pub mod window; +// Internal modules +mod color_attachments; + use std::{ collections::HashSet, iter, @@ -398,6 +401,40 @@ impl RenderContext { return self.gpu.limits().min_uniform_buffer_offset_alignment; } + /// Ensure the MSAA color attachment texture exists with the given sample + /// count, recreating it if necessary. Returns the texture view reference. + /// + /// This method manages the lifecycle of the internal MSAA texture, creating + /// or recreating it when the sample count changes. + fn ensure_msaa_color_texture( + &mut self, + sample_count: u32, + ) -> platform::surface::TextureViewRef<'_> { + let need_recreate = match &self.msaa_color { + Some(_) => self.msaa_sample_count != sample_count, + None => true, + }; + + if need_recreate { + self.msaa_color = Some( + platform::texture::ColorAttachmentTextureBuilder::new( + self.config.format.to_platform(), + ) + .with_size(self.size.0.max(1), self.size.1.max(1)) + .with_sample_count(sample_count) + .with_label("lambda-msaa-color") + .build(self.gpu()), + ); + self.msaa_sample_count = sample_count; + } + + return self + .msaa_color + .as_ref() + .expect("MSAA color attachment should exist") + .view_ref(); + } + /// Encode and submit GPU work for a single frame. fn render_internal( &mut self, @@ -407,16 +444,18 @@ impl RenderContext { return Ok(()); } - let mut frame = match self.surface.acquire_next_frame() { - Ok(frame) => frame, + let frame = match self.surface.acquire_next_frame() { + Ok(frame) => surface::Frame::from_platform(frame), Err(err) => { let high_level_err = surface::SurfaceError::from(err); match high_level_err { surface::SurfaceError::Lost | surface::SurfaceError::Outdated => { self.reconfigure_surface(self.size)?; - self.surface.acquire_next_frame().map_err(|e| { - RenderError::Surface(surface::SurfaceError::from(e)) - })? + let platform_frame = + self.surface.acquire_next_frame().map_err(|e| { + RenderError::Surface(surface::SurfaceError::from(e)) + })?; + surface::Frame::from_platform(platform_frame) } _ => return Err(RenderError::Surface(high_level_err)), } @@ -436,11 +475,17 @@ impl RenderContext { render_pass, viewport, } => { - let pass = self.render_passes.get(render_pass).ok_or_else(|| { - RenderError::Configuration(format!( - "Unknown render pass {render_pass}" - )) - })?; + // Clone the render pass descriptor to avoid borrowing self while we + // need mutable access for MSAA texture creation. + let pass = self + .render_passes + .get(render_pass) + .ok_or_else(|| { + RenderError::Configuration(format!( + "Unknown render pass {render_pass}" + )) + })? + .clone(); // Build (begin) the platform render pass using the builder API. let mut rp_builder = platform::render_pass::RenderPassBuilder::new(); @@ -464,39 +509,32 @@ impl RenderContext { 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(); + + // Ensure MSAA texture exists if needed. let sample_count = pass.sample_count(); - if pass.uses_color() { - if sample_count > 1 { - let need_recreate = match &self.msaa_color { - Some(_) => self.msaa_sample_count != sample_count, - None => true, - }; - if need_recreate { - self.msaa_color = Some( - platform::texture::ColorAttachmentTextureBuilder::new( - self.config.format.to_platform(), - ) - .with_size(self.size.0.max(1), self.size.1.max(1)) - .with_sample_count(sample_count) - .with_label("lambda-msaa-color") - .build(self.gpu()), - ); - self.msaa_sample_count = sample_count; - } - let msaa_view = self - .msaa_color - .as_ref() - .expect("MSAA color attachment should be created") - .view_ref(); - color_attachments.push_msaa_color(msaa_view, view); - } else { - color_attachments.push_color(view); - } + let uses_color = pass.uses_color(); + if uses_color && sample_count > 1 { + self.ensure_msaa_color_texture(sample_count); } + // Create color attachments for the surface pass. The MSAA view is + // retrieved here after the mutable borrow for texture creation ends. + let msaa_view = if sample_count > 1 { + self + .msaa_color + .as_ref() + .map(|t| surface::TextureView::from_platform(t.view_ref())) + } else { + None + }; + let mut color_attachments = + color_attachments::RenderColorAttachments::for_surface_pass( + uses_color, + sample_count, + msaa_view, + view, + ); + // Depth/stencil attachment when either depth or stencil requested. let want_depth_attachment = Self::has_depth_attachment( pass.depth_operations(), @@ -583,7 +621,7 @@ impl RenderContext { let mut pass_encoder = rp_builder.build( &mut encoder, - &mut color_attachments, + color_attachments.as_platform_attachments_mut(), depth_view, depth_ops, stencil_ops, diff --git a/crates/lambda-rs/src/render/surface.rs b/crates/lambda-rs/src/render/surface.rs index ef0a6fc3..c6fc0cef 100644 --- a/crates/lambda-rs/src/render/surface.rs +++ b/crates/lambda-rs/src/render/surface.rs @@ -5,6 +5,84 @@ use super::texture::{ TextureUsages, }; +// --------------------------------------------------------------------------- +// TextureView +// --------------------------------------------------------------------------- + +/// High-level reference to a texture view for render pass attachments. +/// +/// This type wraps the platform `TextureViewRef` and provides a stable +/// engine-level API for referencing texture views without exposing `wgpu` +/// types at call sites. +#[derive(Clone, Copy)] +pub struct TextureView<'a> { + inner: platform_surface::TextureViewRef<'a>, +} + +impl<'a> TextureView<'a> { + /// Create a high-level texture view from a platform texture view reference. + #[inline] + pub(crate) fn from_platform( + view: platform_surface::TextureViewRef<'a>, + ) -> Self { + return TextureView { inner: view }; + } + + /// Convert to the platform texture view reference for internal use. + #[inline] + pub(crate) fn to_platform(&self) -> platform_surface::TextureViewRef<'a> { + return self.inner; + } +} + +impl<'a> std::fmt::Debug for TextureView<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return f.debug_struct("TextureView").finish_non_exhaustive(); + } +} + +// --------------------------------------------------------------------------- +// Frame +// --------------------------------------------------------------------------- + +/// A single acquired frame from the presentation surface. +/// +/// This type wraps the platform `Frame` and provides access to its texture +/// view for rendering. The frame must be presented after rendering is complete +/// by calling `present()`. +pub struct Frame { + inner: platform_surface::Frame, +} + +impl Frame { + /// Create a high-level frame from a platform frame. + #[inline] + pub(crate) fn from_platform(frame: platform_surface::Frame) -> Self { + return Frame { inner: frame }; + } + + /// Borrow the default texture view for rendering to this frame. + #[inline] + pub fn texture_view(&self) -> TextureView<'_> { + return TextureView::from_platform(self.inner.texture_view()); + } + + /// Present the frame to the swapchain. + /// + /// This consumes the frame and submits it for display. After calling this + /// method, the frame's texture is no longer valid for rendering. + #[inline] + pub fn present(self) { + self.inner.present(); + } +} + +impl std::fmt::Debug for Frame { + fn fmt(&self, frame: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return frame.debug_struct("Frame").finish_non_exhaustive(); + } +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum PresentMode { /// Vsync enabled; frames wait for vertical blanking interval. From 8aadd36b38177a9917167a3fa84768aa08a78ad4 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Thu, 11 Dec 2025 14:59:40 -0800 Subject: [PATCH 09/28] [add] ColorAttachmentTexture and DepthTextures to the high level API. --- crates/lambda-rs/src/render/mod.rs | 58 +++--- crates/lambda-rs/src/render/pipeline.rs | 10 +- crates/lambda-rs/src/render/render_pass.rs | 25 +-- crates/lambda-rs/src/render/texture.rs | 201 +++++++++++++++++++++ 4 files changed, 243 insertions(+), 51 deletions(-) diff --git a/crates/lambda-rs/src/render/mod.rs b/crates/lambda-rs/src/render/mod.rs index 1020766d..7701847c 100644 --- a/crates/lambda-rs/src/render/mod.rs +++ b/crates/lambda-rs/src/render/mod.rs @@ -161,14 +161,7 @@ impl RenderContextBuilder { let texture_usage = config.usage; // Initialize a depth texture matching the surface size. - let depth_format = platform::texture::DepthFormat::Depth32Float; - let depth_texture = Some( - platform::texture::DepthTextureBuilder::new() - .with_size(size.0.max(1), size.1.max(1)) - .with_format(depth_format) - .with_label("lambda-depth") - .build(&gpu), - ); + let depth_format = texture::DepthFormat::Depth32Float; return Ok(RenderContext { label: name, @@ -179,7 +172,7 @@ impl RenderContextBuilder { present_mode, texture_usage, size, - depth_texture, + depth_texture: None, depth_format, depth_sample_count: 1, msaa_color: None, @@ -219,10 +212,10 @@ pub struct RenderContext { present_mode: surface::PresentMode, texture_usage: texture::TextureUsages, size: (u32, u32), - depth_texture: Option, - depth_format: platform::texture::DepthFormat, + depth_texture: Option, + depth_format: texture::DepthFormat, depth_sample_count: u32, - msaa_color: Option, + msaa_color: Option, msaa_sample_count: u32, render_passes: Vec, render_pipelines: Vec, @@ -319,12 +312,12 @@ impl RenderContext { // Recreate depth texture to match new size. self.depth_texture = Some( - platform::texture::DepthTextureBuilder::new() + texture::DepthTextureBuilder::new() .with_size(self.size.0.max(1), self.size.1.max(1)) .with_format(self.depth_format) .with_sample_count(self.depth_sample_count) .with_label("lambda-depth") - .build(self.gpu()), + .build(self), ); // Drop MSAA color target so it is rebuilt on demand with the new size. self.msaa_color = None; @@ -352,7 +345,7 @@ impl RenderContext { return self.config.format; } - pub(crate) fn depth_format(&self) -> platform::texture::DepthFormat { + pub(crate) fn depth_format(&self) -> texture::DepthFormat { return self.depth_format; } @@ -368,12 +361,12 @@ impl RenderContext { pub(crate) fn supports_depth_sample_count( &self, - format: platform::texture::DepthFormat, + format: texture::DepthFormat, sample_count: u32, ) -> bool { return self .gpu - .supports_sample_count_for_depth(format, sample_count); + .supports_sample_count_for_depth(format.to_platform(), sample_count); } /// Device limit: maximum bytes that can be bound for a single uniform buffer binding. @@ -409,7 +402,7 @@ impl RenderContext { fn ensure_msaa_color_texture( &mut self, sample_count: u32, - ) -> platform::surface::TextureViewRef<'_> { + ) -> surface::TextureView<'_> { let need_recreate = match &self.msaa_color { Some(_) => self.msaa_sample_count != sample_count, None => true, @@ -417,13 +410,11 @@ impl RenderContext { if need_recreate { self.msaa_color = Some( - platform::texture::ColorAttachmentTextureBuilder::new( - self.config.format.to_platform(), - ) - .with_size(self.size.0.max(1), self.size.1.max(1)) - .with_sample_count(sample_count) - .with_label("lambda-msaa-color") - .build(self.gpu()), + texture::ColorAttachmentTextureBuilder::new(self.config.format) + .with_size(self.size.0.max(1), self.size.1.max(1)) + .with_sample_count(sample_count) + .with_label("lambda-msaa-color") + .build(self), ); self.msaa_sample_count = sample_count; } @@ -520,10 +511,7 @@ impl RenderContext { // Create color attachments for the surface pass. The MSAA view is // retrieved here after the mutable borrow for texture creation ends. let msaa_view = if sample_count > 1 { - self - .msaa_color - .as_ref() - .map(|t| surface::TextureView::from_platform(t.view_ref())) + self.msaa_color.as_ref().map(|t| t.view_ref()) } else { None }; @@ -547,8 +535,7 @@ impl RenderContext { // If stencil is requested on the pass, ensure we use a stencil-capable format. if pass.stencil_operations().is_some() - && self.depth_format - != platform::texture::DepthFormat::Depth24PlusStencil8 + && self.depth_format != texture::DepthFormat::Depth24PlusStencil8 { #[cfg(any( debug_assertions, @@ -558,8 +545,7 @@ impl RenderContext { "Render pass has stencil ops but depth format {:?} lacks stencil; upgrading to Depth24PlusStencil8", self.depth_format ); - self.depth_format = - platform::texture::DepthFormat::Depth24PlusStencil8; + self.depth_format = texture::DepthFormat::Depth24PlusStencil8; } let format_mismatch = self @@ -573,12 +559,12 @@ impl RenderContext { || format_mismatch { self.depth_texture = Some( - platform::texture::DepthTextureBuilder::new() + texture::DepthTextureBuilder::new() .with_size(self.size.0.max(1), self.size.1.max(1)) .with_format(self.depth_format) .with_sample_count(desired_samples) .with_label("lambda-depth") - .build(self.gpu()), + .build(self), ); self.depth_sample_count = desired_samples; } @@ -587,7 +573,7 @@ impl RenderContext { .depth_texture .as_ref() .expect("depth texture should be present") - .view_ref(); + .platform_view_ref(); // Map depth operations when explicitly provided; leave depth // untouched for stencil-only passes. diff --git a/crates/lambda-rs/src/render/pipeline.rs b/crates/lambda-rs/src/render/pipeline.rs index aff89869..223c0c47 100644 --- a/crates/lambda-rs/src/render/pipeline.rs +++ b/crates/lambda-rs/src/render/pipeline.rs @@ -580,7 +580,7 @@ impl RenderPipelineBuilder { // Derive the pass attachment depth format from pass configuration. let pass_has_stencil = _render_pass.stencil_operations().is_some(); let pass_depth_format = if pass_has_stencil { - platform_texture::DepthFormat::Depth24PlusStencil8 + texture::DepthFormat::Depth24PlusStencil8 } else { render_context.depth_format() }; @@ -588,7 +588,9 @@ impl RenderPipelineBuilder { // Align the pipeline depth format with the pass attachment format to // avoid hidden global state on the render context. When formats differ, // prefer the pass attachment format and log for easier debugging. - let final_depth_format = if requested_depth_format != pass_depth_format { + let final_depth_format = if requested_depth_format + != pass_depth_format.to_platform() + { #[cfg(any( debug_assertions, feature = "render-validation-depth", @@ -599,9 +601,9 @@ impl RenderPipelineBuilder { requested_depth_format, pass_depth_format ); - pass_depth_format + pass_depth_format.to_platform() } else { - pass_depth_format + pass_depth_format.to_platform() }; rp_builder = rp_builder.with_depth_stencil(final_depth_format); diff --git a/crates/lambda-rs/src/render/render_pass.rs b/crates/lambda-rs/src/render/render_pass.rs index 5412553d..88d9f833 100644 --- a/crates/lambda-rs/src/render/render_pass.rs +++ b/crates/lambda-rs/src/render/render_pass.rs @@ -8,7 +8,10 @@ use lambda_platform::wgpu as platform; use logging; -use super::RenderContext; +use super::{ + texture, + RenderContext, +}; use crate::render::validation; /// Color load operation for the first color attachment. @@ -303,13 +306,13 @@ impl RenderPassBuilder { &self, sample_count: u32, surface_format: platform::texture::TextureFormat, - depth_format: platform::texture::DepthFormat, + depth_format: texture::DepthFormat, supports_surface: FSurface, supports_depth: FDepth, ) -> u32 where FSurface: Fn(u32) -> bool, - FDepth: Fn(platform::texture::DepthFormat, u32) -> bool, + FDepth: Fn(texture::DepthFormat, u32) -> bool, { let mut resolved_sample_count = sample_count.max(1); @@ -330,7 +333,7 @@ impl RenderPassBuilder { self.depth_operations.is_some() || self.stencil_operations.is_some(); if wants_depth_or_stencil && resolved_sample_count > 1 { let validated_depth_format = if self.stencil_operations.is_some() { - platform::texture::DepthFormat::Depth24PlusStencil8 + texture::DepthFormat::Depth24PlusStencil8 } else { depth_format }; @@ -392,7 +395,7 @@ mod tests { let resolved = builder.resolve_sample_count( 4, surface_format(), - platform::texture::DepthFormat::Depth32Float, + texture::DepthFormat::Depth32Float, |_samples| { return false; }, @@ -412,7 +415,7 @@ mod tests { let resolved = builder.resolve_sample_count( 8, surface_format(), - platform::texture::DepthFormat::Depth32Float, + texture::DepthFormat::Depth32Float, |_samples| { return true; }, @@ -428,13 +431,13 @@ mod tests { #[test] fn stencil_support_uses_stencil_capable_depth_format() { let builder = RenderPassBuilder::new().with_stencil().with_multi_sample(2); - let requested_formats: RefCell> = + let requested_formats: RefCell> = RefCell::new(Vec::new()); let resolved = builder.resolve_sample_count( 2, surface_format(), - platform::texture::DepthFormat::Depth32Float, + texture::DepthFormat::Depth32Float, |_samples| { return true; }, @@ -447,7 +450,7 @@ mod tests { assert_eq!(resolved, 2); assert_eq!( requested_formats.borrow().first().copied(), - Some(platform::texture::DepthFormat::Depth24PlusStencil8) + Some(texture::DepthFormat::Depth24PlusStencil8) ); } @@ -459,7 +462,7 @@ mod tests { let resolved = builder.resolve_sample_count( 4, surface_format(), - platform::texture::DepthFormat::Depth32Float, + texture::DepthFormat::Depth32Float, |_samples| { return true; }, @@ -479,7 +482,7 @@ mod tests { let resolved = builder.resolve_sample_count( 0, surface_format(), - platform::texture::DepthFormat::Depth32Float, + texture::DepthFormat::Depth32Float, |_samples| { return true; }, diff --git a/crates/lambda-rs/src/render/texture.rs b/crates/lambda-rs/src/render/texture.rs index 4dce73ce..15626ab0 100644 --- a/crates/lambda-rs/src/render/texture.rs +++ b/crates/lambda-rs/src/render/texture.rs @@ -190,6 +190,207 @@ impl std::ops::BitOrAssign for TextureUsages { self.0 |= rhs.0; } } + +// --------------------------------------------------------------------------- +// ColorAttachmentTexture +// --------------------------------------------------------------------------- + +/// High-level wrapper for a multi-sampled color render target texture. +/// +/// This type is used for MSAA color attachments and other intermediate render +/// targets that need to be resolved to a single-sample texture before +/// presentation. +#[derive(Debug)] +pub struct ColorAttachmentTexture { + inner: platform::ColorAttachmentTexture, +} + +impl ColorAttachmentTexture { + /// Create a high-level color attachment texture from a platform texture. + pub(crate) fn from_platform( + texture: platform::ColorAttachmentTexture, + ) -> Self { + return ColorAttachmentTexture { inner: texture }; + } + + /// Borrow a texture view reference for use in render pass attachments. + pub(crate) fn view_ref(&self) -> crate::render::surface::TextureView<'_> { + return crate::render::surface::TextureView::from_platform( + self.inner.view_ref(), + ); + } +} + +/// Builder for creating a color attachment texture (commonly used for MSAA). +pub struct ColorAttachmentTextureBuilder { + label: Option, + format: TextureFormat, + width: u32, + height: u32, + sample_count: u32, +} + +impl ColorAttachmentTextureBuilder { + /// Create a builder with zero size and sample count 1. + pub fn new(format: TextureFormat) -> Self { + return Self { + label: None, + format, + width: 0, + height: 0, + sample_count: 1, + }; + } + + /// Set the 2D attachment size in pixels. + pub fn with_size(mut self, width: u32, height: u32) -> Self { + self.width = width; + self.height = height; + return self; + } + + /// Configure multisampling. Count MUST be >= 1. + pub fn with_sample_count(mut self, count: u32) -> Self { + self.sample_count = count.max(1); + return self; + } + + /// Attach a debug label for the created texture. + pub fn with_label(mut self, label: &str) -> Self { + self.label = Some(label.to_string()); + return self; + } + + /// Create the color attachment texture on the device. + pub fn build(self, render_context: &RenderContext) -> ColorAttachmentTexture { + let mut builder = + platform::ColorAttachmentTextureBuilder::new(self.format.to_platform()) + .with_size(self.width, self.height) + .with_sample_count(self.sample_count); + + if let Some(ref label) = self.label { + builder = builder.with_label(label); + } + + let texture = builder.build(render_context.gpu()); + return ColorAttachmentTexture::from_platform(texture); + } +} + +// --------------------------------------------------------------------------- +// DepthTexture +// --------------------------------------------------------------------------- + +/// High-level wrapper for a depth (and optional stencil) render attachment. +/// +/// This type manages a depth texture used for depth testing and stencil +/// operations in render passes. +#[derive(Debug)] +pub struct DepthTexture { + inner: platform::DepthTexture, +} + +impl DepthTexture { + /// Create a high-level depth texture from a platform texture. + pub(crate) fn from_platform(texture: platform::DepthTexture) -> Self { + return DepthTexture { inner: texture }; + } + + /// The depth format used by this attachment. + pub fn format(&self) -> DepthFormat { + return match self.inner.format() { + platform::DepthFormat::Depth32Float => DepthFormat::Depth32Float, + platform::DepthFormat::Depth24Plus => DepthFormat::Depth24Plus, + platform::DepthFormat::Depth24PlusStencil8 => { + DepthFormat::Depth24PlusStencil8 + } + }; + } + + /// Borrow a texture view reference for use in render pass attachments. + pub(crate) fn view_ref(&self) -> crate::render::surface::TextureView<'_> { + return crate::render::surface::TextureView::from_platform( + self.inner.view_ref(), + ); + } + + /// Access the underlying platform texture view reference directly. + /// + /// This is needed for the render pass builder which expects the platform + /// type. + pub(crate) fn platform_view_ref( + &self, + ) -> lambda_platform::wgpu::surface::TextureViewRef<'_> { + return self.inner.view_ref(); + } +} + +/// Builder for creating a depth texture attachment. +pub struct DepthTextureBuilder { + label: Option, + format: DepthFormat, + width: u32, + height: u32, + sample_count: u32, +} + +impl DepthTextureBuilder { + /// Create a builder with no size and `Depth32Float` format. + pub fn new() -> Self { + return Self { + label: None, + format: DepthFormat::Depth32Float, + width: 0, + height: 0, + sample_count: 1, + }; + } + + /// Set the 2D attachment size in pixels. + pub fn with_size(mut self, width: u32, height: u32) -> Self { + self.width = width; + self.height = height; + return self; + } + + /// Choose a depth format. + pub fn with_format(mut self, format: DepthFormat) -> Self { + self.format = format; + return self; + } + + /// Configure multi-sampling. + pub fn with_sample_count(mut self, count: u32) -> Self { + self.sample_count = count.max(1); + return self; + } + + /// Attach a debug label for the created texture. + pub fn with_label(mut self, label: &str) -> Self { + self.label = Some(label.to_string()); + return self; + } + + /// Create the depth texture on the device. + pub fn build(self, render_context: &RenderContext) -> DepthTexture { + let mut builder = platform::DepthTextureBuilder::new() + .with_size(self.width, self.height) + .with_format(self.format.to_platform()) + .with_sample_count(self.sample_count); + + if let Some(ref label) = self.label { + builder = builder.with_label(label); + } + + let texture = builder.build(render_context.gpu()); + return DepthTexture::from_platform(texture); + } +} + +// --------------------------------------------------------------------------- +// Texture (sampled) +// --------------------------------------------------------------------------- + #[derive(Debug, Clone)] /// High‑level texture wrapper that owns a platform texture. pub struct Texture { From c9e8162665330c352914f1228d7446905affe79a Mon Sep 17 00:00:00 2001 From: vmarcella Date: Thu, 11 Dec 2025 15:16:56 -0800 Subject: [PATCH 10/28] [fix] cfg flag. --- crates/lambda-rs/src/render/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/lambda-rs/src/render/mod.rs b/crates/lambda-rs/src/render/mod.rs index 7701847c..0adb6279 100644 --- a/crates/lambda-rs/src/render/mod.rs +++ b/crates/lambda-rs/src/render/mod.rs @@ -831,7 +831,7 @@ impl RenderContext { #[cfg(any( debug_assertions, - feature = "render-instancing-validation", + feature = "render-validation-instancing", ))] { bound_vertex_slots.insert(buffer); From 039c9f2b96f5443632409dfaa29d58c99e9d82c9 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Fri, 12 Dec 2025 14:00:24 -0800 Subject: [PATCH 11/28] [add] command encoder to begin abstracting the command processing away from the render context. --- crates/lambda-rs-platform/src/wgpu/command.rs | 11 +- .../src/wgpu/render_pass.rs | 26 +- crates/lambda-rs/src/render/encoder.rs | 757 ++++++++++++++++++ crates/lambda-rs/src/render/mod.rs | 8 +- crates/lambda-rs/src/render/pipeline.rs | 5 +- 5 files changed, 786 insertions(+), 21 deletions(-) create mode 100644 crates/lambda-rs/src/render/encoder.rs diff --git a/crates/lambda-rs-platform/src/wgpu/command.rs b/crates/lambda-rs-platform/src/wgpu/command.rs index 1328cfa9..ae675dcd 100644 --- a/crates/lambda-rs-platform/src/wgpu/command.rs +++ b/crates/lambda-rs-platform/src/wgpu/command.rs @@ -17,7 +17,8 @@ pub struct CommandBuffer { } impl CommandBuffer { - pub(crate) fn into_raw(self) -> wgpu::CommandBuffer { + /// Convert to the raw wgpu command buffer. + pub fn into_raw(self) -> wgpu::CommandBuffer { self.raw } } @@ -32,10 +33,10 @@ impl CommandEncoder { } /// 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> { + pub(crate) fn begin_render_pass_raw<'pass>( + &'pass mut self, + desc: &wgpu::RenderPassDescriptor<'pass>, + ) -> wgpu::RenderPass<'pass> { return self.raw.begin_render_pass(desc); } diff --git a/crates/lambda-rs-platform/src/wgpu/render_pass.rs b/crates/lambda-rs-platform/src/wgpu/render_pass.rs index b153f072..b6b9946e 100644 --- a/crates/lambda-rs-platform/src/wgpu/render_pass.rs +++ b/crates/lambda-rs-platform/src/wgpu/render_pass.rs @@ -7,10 +7,7 @@ //! `CommandEncoder` and texture view. The returned `RenderPass` borrows the //! encoder and remains valid until dropped. -use wgpu::{ - self, - RenderPassColorAttachment, -}; +use wgpu; use super::{ bind, @@ -301,7 +298,11 @@ impl RenderPassBuilder { } } - /// Attach a debug label to the render pass. + /// Attach a debug label to the render pass (used only for debugging during + /// builder setup, the actual label must be passed to `build`). + #[deprecated( + note = "The label must be passed directly to build() for proper lifetime management" + )] pub fn with_label(mut self, label: &str) -> Self { self.config.label = Some(label.to_string()); return self; @@ -337,13 +338,22 @@ impl RenderPassBuilder { /// 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. + /// + /// # Arguments + /// * `encoder` - The command encoder to begin the pass on. + /// * `attachments` - Color attachments for the pass. + /// * `depth_view` - Optional depth view. + /// * `depth_ops` - Optional depth operations. + /// * `stencil_ops` - Optional stencil operations. + /// * `label` - Optional debug label (must outlive the pass). pub fn build<'view>( - &'view self, + self, encoder: &'view mut command::CommandEncoder, attachments: &'view mut RenderColorAttachments<'view>, depth_view: Option>, depth_ops: Option, stencil_ops: Option, + label: Option<&'view str>, ) -> RenderPass<'view> { let operations = match self.config.color_operations.load { ColorLoadOp::Load => wgpu::Operations { @@ -417,8 +427,8 @@ impl RenderPassBuilder { } }); - let desc: wgpu::RenderPassDescriptor<'view> = wgpu::RenderPassDescriptor { - label: self.config.label.as_deref(), + let desc = wgpu::RenderPassDescriptor { + label, color_attachments: attachments.as_slice(), depth_stencil_attachment, timestamp_writes: None, diff --git a/crates/lambda-rs/src/render/encoder.rs b/crates/lambda-rs/src/render/encoder.rs new file mode 100644 index 00000000..3873ceaf --- /dev/null +++ b/crates/lambda-rs/src/render/encoder.rs @@ -0,0 +1,757 @@ +//! High-level command encoding for GPU work submission. +//! +//! This module provides `CommandEncoder` for recording GPU commands and +//! `RenderPassEncoder` for recording render pass commands. These types wrap +//! the platform layer and provide validation, high-level type safety, and a +//! clean API for the engine. +//! +//! Command encoders are created per-frame and not reused across frames, which +//! matches wgpu best practices and avoids stale state issues. +//! +//! # Usage +//! +//! The encoder uses a callback-based API for render passes to ensure proper +//! lifetime management (wgpu's `RenderPass` borrows from the encoder): +//! +//! ```ignore +//! let mut encoder = CommandEncoder::new(&render_context, "frame-encoder"); +//! encoder.with_render_pass(config, &mut attachments, depth, None, None, Some("main-pass"), |pass| { +//! pass.set_pipeline(&pipeline)?; +//! pass.draw(0..3, 0..1)?; +//! Ok(()) +//! })?; +//! encoder.finish(&render_context); +//! ``` + +use std::{ + collections::HashSet, + ops::Range, +}; + +use lambda_platform::wgpu as platform; +use logging; + +use super::{ + bind::BindGroup, + buffer::{ + Buffer, + BufferType, + }, + color_attachments::RenderColorAttachments, + command::IndexFormat, + pipeline, + pipeline::RenderPipeline, + render_pass::{ + ColorLoadOp, + ColorOperations, + DepthLoadOp, + DepthOperations, + StencilLoadOp, + StencilOperations, + StoreOp, + }, + texture::DepthTexture, + validation, + viewport::Viewport, + RenderContext, +}; +use crate::util; + +// --------------------------------------------------------------------------- +// CommandEncoder +// --------------------------------------------------------------------------- + +/// High-level command encoder for recording GPU work. +/// +/// Created per-frame via `CommandEncoder::new()`. Commands are recorded by +/// beginning render passes (and in the future, compute passes and copy ops). +/// Call `finish()` to submit the recorded work. +/// +/// The encoder owns the underlying platform encoder and manages its lifetime. +pub struct CommandEncoder { + inner: platform::command::CommandEncoder, +} + +impl CommandEncoder { + /// Create a new command encoder for recording GPU work. + /// + /// The encoder is tied to the current frame and should not be reused across + /// frames. + pub fn new(render_context: &RenderContext, label: &str) -> Self { + let inner = + platform::command::CommandEncoder::new(render_context.gpu(), Some(label)); + return CommandEncoder { inner }; + } + + /// Execute a render pass with the provided configuration. + /// + /// This method begins a render pass, executes the provided closure with a + /// `RenderPassEncoder`, and automatically ends the pass when the closure + /// returns. This ensures proper resource cleanup and lifetime management. + /// + /// # Arguments + /// * `config` - Configuration for the render pass (label, color/depth ops). + /// * `color_attachments` - Color attachment views for the pass. + /// * `depth_texture` - Optional depth texture for depth/stencil operations. + /// * `depth_ops` - Optional depth operations (load/store). + /// * `stencil_ops` - Optional stencil operations (load/store). + /// * `label` - Optional debug label for the pass (must outlive the pass). + /// * `f` - Closure that records commands to the render pass. + /// + /// # Returns + /// The result of the closure, or any render pass error encountered. + pub(crate) fn with_render_pass<'a, F, R>( + &'a mut self, + config: RenderPassConfig, + color_attachments: &'a mut RenderColorAttachments<'a>, + depth_texture: Option<&'a DepthTexture>, + depth_ops: Option, + stencil_ops: Option, + label: Option<&'a str>, + f: F, + ) -> Result + where + F: FnOnce(&mut RenderPassEncoder<'_>) -> Result, + { + let mut pass_encoder = RenderPassEncoder::new( + &mut self.inner, + config, + color_attachments, + depth_texture, + depth_ops, + stencil_ops, + label, + ); + + let result = f(&mut pass_encoder); + // Pass is automatically dropped here, ending the render pass + return result; + } + + /// Finish recording and submit the command buffer to the GPU. + /// + /// This consumes the encoder and submits the recorded commands for + /// execution. + pub fn finish(self, render_context: &RenderContext) { + let buffer = self.inner.finish(); + render_context.gpu().submit(std::iter::once(buffer)); + } +} + +impl std::fmt::Debug for CommandEncoder { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return f.debug_struct("CommandEncoder").finish_non_exhaustive(); + } +} + +// --------------------------------------------------------------------------- +// RenderPassConfig +// --------------------------------------------------------------------------- + +/// Configuration for beginning a render pass. +#[derive(Clone, Debug)] +pub struct RenderPassConfig { + /// Optional debug label for the pass. + pub label: Option, + /// Color operations (load/store) for color attachments. + pub color_operations: ColorOperations, + /// Initial viewport for the pass. + pub viewport: Viewport, + /// Whether the pass uses color attachments. + pub uses_color: bool, + /// MSAA sample count for the pass. + pub sample_count: u32, +} + +impl RenderPassConfig { + /// Create a new render pass configuration with default settings. + pub fn new() -> Self { + return Self { + label: None, + color_operations: ColorOperations::default(), + viewport: Viewport { + x: 0, + y: 0, + width: 1, + height: 1, + min_depth: 0.0, + max_depth: 1.0, + }, + uses_color: true, + sample_count: 1, + }; + } + + /// Set the debug label for the pass. + pub fn with_label(mut self, label: &str) -> Self { + self.label = Some(label.to_string()); + return self; + } + + /// Set the color operations for the pass. + pub fn with_color_operations(mut self, ops: ColorOperations) -> Self { + self.color_operations = ops; + return self; + } + + /// Set the initial viewport for the pass. + pub fn with_viewport(mut self, viewport: Viewport) -> Self { + self.viewport = viewport; + return self; + } + + /// Set whether the pass uses color attachments. + pub fn with_color(mut self, uses_color: bool) -> Self { + self.uses_color = uses_color; + return self; + } + + /// Set the MSAA sample count for the pass. + pub fn with_sample_count(mut self, sample_count: u32) -> Self { + self.sample_count = sample_count; + return self; + } +} + +impl Default for RenderPassConfig { + fn default() -> Self { + return Self::new(); + } +} + +// --------------------------------------------------------------------------- +// RenderPassEncoder +// --------------------------------------------------------------------------- + +/// High-level render pass encoder for recording render commands. +/// +/// Created by `CommandEncoder::with_render_pass()`. Records draw commands, +/// pipeline bindings, and resource bindings within the closure scope. +/// +/// The encoder borrows the command encoder for the duration of the pass and +/// performs validation on all operations. +pub struct RenderPassEncoder<'a> { + /// Platform render pass for issuing GPU commands. + pass: platform::render_pass::RenderPass<'a>, + /// Whether the pass uses color attachments. + uses_color: bool, + /// Whether the pass has a depth attachment. + has_depth_attachment: bool, + /// Whether the pass has stencil operations. + has_stencil: bool, + /// Sample count for MSAA validation. + sample_count: u32, + + // Validation state (compiled out in release without features) + #[cfg(any(debug_assertions, feature = "render-validation-encoder"))] + current_pipeline: Option, + #[cfg(any(debug_assertions, feature = "render-validation-encoder"))] + bound_index_buffer: Option, + #[cfg(any(debug_assertions, feature = "render-validation-instancing"))] + bound_vertex_slots: HashSet, + #[cfg(any( + debug_assertions, + feature = "render-validation-depth", + feature = "render-validation-stencil" + ))] + warned_no_stencil_for_pipeline: HashSet, + #[cfg(any( + debug_assertions, + feature = "render-validation-depth", + feature = "render-validation-stencil" + ))] + warned_no_depth_for_pipeline: HashSet, +} + +/// Tracks the currently bound pipeline for validation. +#[cfg(any(debug_assertions, feature = "render-validation-encoder"))] +#[derive(Clone)] +struct CurrentPipeline { + label: String, + has_color_targets: bool, + expects_depth_stencil: bool, + uses_stencil: bool, + per_instance_slots: Vec, +} + +/// Tracks the currently bound index buffer for validation. +#[cfg(any(debug_assertions, feature = "render-validation-encoder"))] +struct BoundIndexBuffer { + max_indices: u32, +} + +impl<'a> RenderPassEncoder<'a> { + /// Create a new render pass encoder (internal). + fn new( + encoder: &'a mut platform::command::CommandEncoder, + config: RenderPassConfig, + color_attachments: &'a mut RenderColorAttachments<'a>, + depth_texture: Option<&'a DepthTexture>, + depth_ops: Option, + stencil_ops: Option, + label: Option<&'a str>, + ) -> Self { + // Build the platform render pass + let mut rp_builder = platform::render_pass::RenderPassBuilder::new(); + + // Map color operations + rp_builder = match config.color_operations.load { + ColorLoadOp::Load => { + rp_builder.with_color_load_op(platform::render_pass::ColorLoadOp::Load) + } + ColorLoadOp::Clear(color) => rp_builder + .with_color_load_op(platform::render_pass::ColorLoadOp::Clear(color)), + }; + rp_builder = match config.color_operations.store { + StoreOp::Store => { + rp_builder.with_store_op(platform::render_pass::StoreOp::Store) + } + StoreOp::Discard => { + rp_builder.with_store_op(platform::render_pass::StoreOp::Discard) + } + }; + + // Map depth operations + let platform_depth_ops = + depth_ops.map(|dop| platform::render_pass::DepthOperations { + load: match dop.load { + DepthLoadOp::Load => platform::render_pass::DepthLoadOp::Load, + DepthLoadOp::Clear(v) => { + platform::render_pass::DepthLoadOp::Clear(v as f32) + } + }, + store: match dop.store { + StoreOp::Store => platform::render_pass::StoreOp::Store, + StoreOp::Discard => platform::render_pass::StoreOp::Discard, + }, + }); + + // Map stencil operations + let platform_stencil_ops = + stencil_ops.map(|sop| platform::render_pass::StencilOperations { + load: match sop.load { + StencilLoadOp::Load => platform::render_pass::StencilLoadOp::Load, + StencilLoadOp::Clear(v) => { + platform::render_pass::StencilLoadOp::Clear(v) + } + }, + store: match sop.store { + StoreOp::Store => platform::render_pass::StoreOp::Store, + StoreOp::Discard => platform::render_pass::StoreOp::Discard, + }, + }); + + let depth_view = depth_texture.map(|dt| dt.platform_view_ref()); + let has_depth_attachment = depth_texture.is_some(); + let has_stencil = stencil_ops.is_some(); + + let pass = rp_builder.build( + encoder, + color_attachments.as_platform_attachments_mut(), + depth_view, + platform_depth_ops, + platform_stencil_ops, + label, + ); + + let mut encoder_instance = RenderPassEncoder { + pass, + uses_color: config.uses_color, + has_depth_attachment, + has_stencil, + sample_count: config.sample_count, + #[cfg(any(debug_assertions, feature = "render-validation-encoder"))] + current_pipeline: None, + #[cfg(any(debug_assertions, feature = "render-validation-encoder"))] + bound_index_buffer: None, + #[cfg(any(debug_assertions, feature = "render-validation-instancing"))] + bound_vertex_slots: HashSet::new(), + #[cfg(any( + debug_assertions, + feature = "render-validation-depth", + feature = "render-validation-stencil" + ))] + warned_no_stencil_for_pipeline: HashSet::new(), + #[cfg(any( + debug_assertions, + feature = "render-validation-depth", + feature = "render-validation-stencil" + ))] + warned_no_depth_for_pipeline: HashSet::new(), + }; + + // Apply initial viewport + encoder_instance.set_viewport(&config.viewport); + + return encoder_instance; + } + + /// Set the active render pipeline. + /// + /// Returns an error if the pipeline is incompatible with the current pass + /// configuration (e.g., color target mismatch). + pub fn set_pipeline( + &mut self, + pipeline: &RenderPipeline, + ) -> Result<(), RenderPassError> { + // Validation + #[cfg(any( + debug_assertions, + feature = "render-validation-pass-compat", + feature = "render-validation-encoder" + ))] + { + let label = pipeline.pipeline().label().unwrap_or("unnamed"); + + if !self.uses_color && pipeline.has_color_targets() { + return Err(RenderPassError::PipelineIncompatible(format!( + "Render pipeline '{}' declares color targets but the current pass \ + has no color attachments", + label + ))); + } + if self.uses_color && !pipeline.has_color_targets() { + return Err(RenderPassError::PipelineIncompatible(format!( + "Render pipeline '{}' has no color targets but the current pass \ + declares color attachments", + label + ))); + } + if !self.has_depth_attachment && pipeline.expects_depth_stencil() { + return Err(RenderPassError::PipelineIncompatible(format!( + "Render pipeline '{}' expects a depth/stencil attachment but the \ + current pass has none", + label + ))); + } + } + + // Track current pipeline for draw validation + #[cfg(any(debug_assertions, feature = "render-validation-encoder"))] + { + let label = pipeline.pipeline().label().unwrap_or("unnamed").to_string(); + self.current_pipeline = Some(CurrentPipeline { + label: label.clone(), + has_color_targets: pipeline.has_color_targets(), + expects_depth_stencil: pipeline.expects_depth_stencil(), + uses_stencil: pipeline.uses_stencil(), + per_instance_slots: pipeline.per_instance_slots().clone(), + }); + } + + // Advisory warnings + #[cfg(any( + debug_assertions, + feature = "render-validation-depth", + feature = "render-validation-stencil" + ))] + { + let label = pipeline.pipeline().label().unwrap_or("unnamed"); + let pipeline_id = pipeline as *const _ as usize; + + if self.has_stencil + && !pipeline.uses_stencil() + && self.warned_no_stencil_for_pipeline.insert(pipeline_id) + { + let key = format!("stencil:no_test:{}", label); + let msg = format!( + "Pass provides stencil ops but pipeline '{}' has no stencil test; \ + stencil will not affect rendering", + label + ); + util::warn_once(&key, &msg); + } + + if !self.has_stencil && pipeline.uses_stencil() { + let key = format!("stencil:pass_no_operations:{}", label); + let msg = format!( + "Pipeline '{}' enables stencil but pass has no stencil ops \ + configured; stencil reference/tests may be ineffective", + label + ); + util::warn_once(&key, &msg); + } + + if self.has_depth_attachment + && !pipeline.expects_depth_stencil() + && self.warned_no_depth_for_pipeline.insert(pipeline_id) + { + let key = format!("depth:no_test:{}", label); + let msg = format!( + "Pass has depth attachment but pipeline '{}' does not enable depth \ + testing; depth values will not be tested/written", + label + ); + util::warn_once(&key, &msg); + } + } + + self.pass.set_pipeline(pipeline.pipeline()); + return Ok(()); + } + + /// Set the viewport for subsequent draw commands. + pub fn set_viewport(&mut self, viewport: &Viewport) { + let (x, y, width, height, min_depth, max_depth) = viewport.viewport_f32(); + self + .pass + .set_viewport(x, y, width, height, min_depth, max_depth); + let (sx, sy, sw, sh) = viewport.scissor_u32(); + self.pass.set_scissor_rect(sx, sy, sw, sh); + } + + /// Set only the scissor rectangle. + pub fn set_scissor(&mut self, viewport: &Viewport) { + let (x, y, width, height) = viewport.scissor_u32(); + self.pass.set_scissor_rect(x, y, width, height); + } + + /// Set the stencil reference value. + pub fn set_stencil_reference(&mut self, reference: u32) { + self.pass.set_stencil_reference(reference); + } + + /// Bind a bind group with optional dynamic offsets. + pub fn set_bind_group( + &mut self, + set: u32, + group: &BindGroup, + dynamic_offsets: &[u32], + min_uniform_buffer_offset_alignment: u32, + ) -> Result<(), RenderPassError> { + validation::validate_dynamic_offsets( + group.dynamic_binding_count(), + dynamic_offsets, + min_uniform_buffer_offset_alignment, + set, + ) + .map_err(RenderPassError::Validation)?; + + self + .pass + .set_bind_group(set, group.platform_group(), dynamic_offsets); + return Ok(()); + } + + /// Bind a vertex buffer to a slot. + pub fn set_vertex_buffer(&mut self, slot: u32, buffer: &Buffer) { + #[cfg(any(debug_assertions, feature = "render-validation-instancing"))] + { + self.bound_vertex_slots.insert(slot); + } + + self.pass.set_vertex_buffer(slot, buffer.raw()); + } + + /// Bind an index buffer with the specified format. + pub fn set_index_buffer( + &mut self, + buffer: &Buffer, + format: IndexFormat, + ) -> Result<(), RenderPassError> { + #[cfg(any(debug_assertions, feature = "render-validation-encoder"))] + { + if buffer.buffer_type() != BufferType::Index { + return Err(RenderPassError::Validation(format!( + "Binding buffer as index but logical type is {:?}; expected \ + BufferType::Index", + buffer.buffer_type() + ))); + } + + let element_size = match format { + IndexFormat::Uint16 => 2u64, + IndexFormat::Uint32 => 4u64, + }; + let stride = buffer.stride(); + if stride != element_size { + return Err(RenderPassError::Validation(format!( + "Index buffer has element stride {} bytes but format {:?} requires \ + {} bytes", + stride, format, element_size + ))); + } + + let buffer_size = buffer.raw().size(); + if buffer_size % element_size != 0 { + return Err(RenderPassError::Validation(format!( + "Index buffer size {} bytes is not a multiple of element size {} \ + for format {:?}", + buffer_size, element_size, format + ))); + } + + let max_indices = + (buffer_size / element_size).min(u32::MAX as u64) as u32; + self.bound_index_buffer = Some(BoundIndexBuffer { max_indices }); + } + + self + .pass + .set_index_buffer(buffer.raw(), format.to_platform()); + return Ok(()); + } + + /// Set push constants for a pipeline stage. + pub fn set_push_constants( + &mut self, + stage: pipeline::PipelineStage, + offset: u32, + data: &[u8], + ) { + self.pass.set_push_constants(stage, offset, data); + } + + /// Issue a non-indexed draw call. + pub fn draw( + &mut self, + vertices: Range, + instances: Range, + ) -> Result<(), RenderPassError> { + #[cfg(any(debug_assertions, feature = "render-validation-encoder"))] + { + if self.current_pipeline.is_none() { + return Err(RenderPassError::NoPipeline( + "Draw command encountered before any pipeline was set".to_string(), + )); + } + } + + #[cfg(any(debug_assertions, feature = "render-validation-instancing"))] + { + if let Some(ref pipeline) = self.current_pipeline { + validation::validate_instance_bindings( + &pipeline.label, + &pipeline.per_instance_slots, + &self.bound_vertex_slots, + ) + .map_err(RenderPassError::Validation)?; + + validation::validate_instance_range("Draw", &instances) + .map_err(RenderPassError::Validation)?; + } + + if instances.start == instances.end { + logging::debug!( + "Skipping Draw with empty instance range {}..{}", + instances.start, + instances.end + ); + return Ok(()); + } + } + + self.pass.draw(vertices, instances); + return Ok(()); + } + + /// Issue an indexed draw call. + pub fn draw_indexed( + &mut self, + indices: Range, + base_vertex: i32, + instances: Range, + ) -> Result<(), RenderPassError> { + #[cfg(any(debug_assertions, feature = "render-validation-encoder"))] + { + if self.current_pipeline.is_none() { + return Err(RenderPassError::NoPipeline( + "DrawIndexed command encountered before any pipeline was set" + .to_string(), + )); + } + + match &self.bound_index_buffer { + None => { + return Err(RenderPassError::NoIndexBuffer( + "DrawIndexed command encountered without a bound index buffer" + .to_string(), + )); + } + Some(bound) => { + if indices.start > indices.end { + return Err(RenderPassError::Validation(format!( + "DrawIndexed index range start {} is greater than end {}", + indices.start, indices.end + ))); + } + if indices.end > bound.max_indices { + return Err(RenderPassError::Validation(format!( + "DrawIndexed index range {}..{} exceeds index buffer capacity {}", + indices.start, indices.end, bound.max_indices + ))); + } + } + } + } + + #[cfg(any(debug_assertions, feature = "render-validation-instancing"))] + { + if let Some(ref pipeline) = self.current_pipeline { + validation::validate_instance_bindings( + &pipeline.label, + &pipeline.per_instance_slots, + &self.bound_vertex_slots, + ) + .map_err(RenderPassError::Validation)?; + + validation::validate_instance_range("DrawIndexed", &instances) + .map_err(RenderPassError::Validation)?; + } + + if instances.start == instances.end { + logging::debug!( + "Skipping DrawIndexed with empty instance range {}..{}", + instances.start, + instances.end + ); + return Ok(()); + } + } + + self.pass.draw_indexed(indices, base_vertex, instances); + return Ok(()); + } +} + +impl std::fmt::Debug for RenderPassEncoder<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return f + .debug_struct("RenderPassEncoder") + .field("uses_color", &self.uses_color) + .field("has_depth_attachment", &self.has_depth_attachment) + .field("has_stencil", &self.has_stencil) + .field("sample_count", &self.sample_count) + .finish_non_exhaustive(); + } +} + +// --------------------------------------------------------------------------- +// Errors +// --------------------------------------------------------------------------- + +/// Errors that can occur during render pass encoding. +#[derive(Debug, Clone)] +pub enum RenderPassError { + /// Pipeline is incompatible with the current pass configuration. + PipelineIncompatible(String), + /// No pipeline has been set before a draw call. + NoPipeline(String), + /// No index buffer has been bound before an indexed draw call. + NoIndexBuffer(String), + /// Validation error (offsets, ranges, types, etc.). + Validation(String), +} + +impl std::fmt::Display for RenderPassError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return match self { + RenderPassError::PipelineIncompatible(s) => write!(f, "{}", s), + RenderPassError::NoPipeline(s) => write!(f, "{}", s), + RenderPassError::NoIndexBuffer(s) => write!(f, "{}", s), + RenderPassError::Validation(s) => write!(f, "{}", s), + }; + } +} + +impl std::error::Error for RenderPassError {} diff --git a/crates/lambda-rs/src/render/mod.rs b/crates/lambda-rs/src/render/mod.rs index 0adb6279..f4b8ba42 100644 --- a/crates/lambda-rs/src/render/mod.rs +++ b/crates/lambda-rs/src/render/mod.rs @@ -32,6 +32,7 @@ pub mod bind; pub mod buffer; pub mod command; +pub mod encoder; pub mod mesh; pub mod pipeline; pub mod render_pass; @@ -480,9 +481,7 @@ impl RenderContext { // 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 pass_label = pass.label(); let ops = pass.color_operations(); rp_builder = match ops.load { self::render_pass::ColorLoadOp::Load => rp_builder @@ -611,6 +610,7 @@ impl RenderContext { depth_view, depth_ops, stencil_ops, + pass_label, ); self.encode_pass( @@ -726,7 +726,7 @@ impl RenderContext { #[cfg(any( debug_assertions, feature = "render-validation-encoder", - feature = "render-instancing-validation", + feature = "render-validation-instancing", ))] { current_pipeline = Some(pipeline); diff --git a/crates/lambda-rs/src/render/pipeline.rs b/crates/lambda-rs/src/render/pipeline.rs index 223c0c47..8e289649 100644 --- a/crates/lambda-rs/src/render/pipeline.rs +++ b/crates/lambda-rs/src/render/pipeline.rs @@ -30,10 +30,7 @@ use std::{ rc::Rc, }; -use lambda_platform::wgpu::{ - pipeline as platform_pipeline, - texture as platform_texture, -}; +use lambda_platform::wgpu::pipeline as platform_pipeline; use logging; use super::{ From 068c6b89aa917840bc5479ddc1f486e323cd8ec3 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Sat, 13 Dec 2025 15:49:07 -0800 Subject: [PATCH 12/28] [update] encoder to use render pass over the render pass config. --- crates/lambda-rs/src/render/encoder.rs | 194 +++++++------------------ 1 file changed, 56 insertions(+), 138 deletions(-) diff --git a/crates/lambda-rs/src/render/encoder.rs b/crates/lambda-rs/src/render/encoder.rs index 3873ceaf..69b75ba9 100644 --- a/crates/lambda-rs/src/render/encoder.rs +++ b/crates/lambda-rs/src/render/encoder.rs @@ -15,9 +15,9 @@ //! //! ```ignore //! let mut encoder = CommandEncoder::new(&render_context, "frame-encoder"); -//! encoder.with_render_pass(config, &mut attachments, depth, None, None, Some("main-pass"), |pass| { -//! pass.set_pipeline(&pipeline)?; -//! pass.draw(0..3, 0..1)?; +//! encoder.with_render_pass(&pass, &mut attachments, depth, |rp_encoder| { +//! rp_encoder.set_pipeline(&pipeline)?; +//! rp_encoder.draw(0..3, 0..1)?; //! Ok(()) //! })?; //! encoder.finish(&render_context); @@ -43,11 +43,9 @@ use super::{ pipeline::RenderPipeline, render_pass::{ ColorLoadOp, - ColorOperations, DepthLoadOp, - DepthOperations, + RenderPass, StencilLoadOp, - StencilOperations, StoreOp, }, texture::DepthTexture, @@ -90,42 +88,37 @@ impl CommandEncoder { /// returns. This ensures proper resource cleanup and lifetime management. /// /// # Arguments - /// * `config` - Configuration for the render pass (label, color/depth ops). + /// * `pass` - The high-level render pass configuration. /// * `color_attachments` - Color attachment views for the pass. /// * `depth_texture` - Optional depth texture for depth/stencil operations. - /// * `depth_ops` - Optional depth operations (load/store). - /// * `stencil_ops` - Optional stencil operations (load/store). - /// * `label` - Optional debug label for the pass (must outlive the pass). - /// * `f` - Closure that records commands to the render pass. + /// * `f` - Closure that records commands to the render pass encoder. + /// + /// # Type Parameters + /// * `'pass` - Lifetime of resources borrowed during the render pass. + /// * `PassFn` - The closure type that records commands to the pass. + /// * `Output` - The return type of the closure. /// /// # Returns /// The result of the closure, or any render pass error encountered. - pub(crate) fn with_render_pass<'a, F, R>( - &'a mut self, - config: RenderPassConfig, - color_attachments: &'a mut RenderColorAttachments<'a>, - depth_texture: Option<&'a DepthTexture>, - depth_ops: Option, - stencil_ops: Option, - label: Option<&'a str>, - f: F, - ) -> Result + pub(crate) fn with_render_pass<'pass, PassFn, Output>( + &'pass mut self, + pass: &'pass RenderPass, + color_attachments: &'pass mut RenderColorAttachments<'pass>, + depth_texture: Option<&'pass DepthTexture>, + f: PassFn, + ) -> Result where - F: FnOnce(&mut RenderPassEncoder<'_>) -> Result, + PassFn: + FnOnce(&mut RenderPassEncoder<'_>) -> Result, { - let mut pass_encoder = RenderPassEncoder::new( + let pass_encoder = RenderPassEncoder::new( &mut self.inner, - config, + pass, color_attachments, depth_texture, - depth_ops, - stencil_ops, - label, ); - let result = f(&mut pass_encoder); - // Pass is automatically dropped here, ending the render pass - return result; + return f(&mut { pass_encoder }); } /// Finish recording and submit the command buffer to the GPU. @@ -144,81 +137,6 @@ impl std::fmt::Debug for CommandEncoder { } } -// --------------------------------------------------------------------------- -// RenderPassConfig -// --------------------------------------------------------------------------- - -/// Configuration for beginning a render pass. -#[derive(Clone, Debug)] -pub struct RenderPassConfig { - /// Optional debug label for the pass. - pub label: Option, - /// Color operations (load/store) for color attachments. - pub color_operations: ColorOperations, - /// Initial viewport for the pass. - pub viewport: Viewport, - /// Whether the pass uses color attachments. - pub uses_color: bool, - /// MSAA sample count for the pass. - pub sample_count: u32, -} - -impl RenderPassConfig { - /// Create a new render pass configuration with default settings. - pub fn new() -> Self { - return Self { - label: None, - color_operations: ColorOperations::default(), - viewport: Viewport { - x: 0, - y: 0, - width: 1, - height: 1, - min_depth: 0.0, - max_depth: 1.0, - }, - uses_color: true, - sample_count: 1, - }; - } - - /// Set the debug label for the pass. - pub fn with_label(mut self, label: &str) -> Self { - self.label = Some(label.to_string()); - return self; - } - - /// Set the color operations for the pass. - pub fn with_color_operations(mut self, ops: ColorOperations) -> Self { - self.color_operations = ops; - return self; - } - - /// Set the initial viewport for the pass. - pub fn with_viewport(mut self, viewport: Viewport) -> Self { - self.viewport = viewport; - return self; - } - - /// Set whether the pass uses color attachments. - pub fn with_color(mut self, uses_color: bool) -> Self { - self.uses_color = uses_color; - return self; - } - - /// Set the MSAA sample count for the pass. - pub fn with_sample_count(mut self, sample_count: u32) -> Self { - self.sample_count = sample_count; - return self; - } -} - -impl Default for RenderPassConfig { - fn default() -> Self { - return Self::new(); - } -} - // --------------------------------------------------------------------------- // RenderPassEncoder // --------------------------------------------------------------------------- @@ -230,9 +148,13 @@ impl Default for RenderPassConfig { /// /// The encoder borrows the command encoder for the duration of the pass and /// performs validation on all operations. -pub struct RenderPassEncoder<'a> { +/// +/// # Type Parameters +/// * `'pass` - The lifetime of the render pass, tied to the borrowed encoder +/// and attachments. +pub struct RenderPassEncoder<'pass> { /// Platform render pass for issuing GPU commands. - pass: platform::render_pass::RenderPass<'a>, + pass: platform::render_pass::RenderPass<'pass>, /// Whether the pass uses color attachments. uses_color: bool, /// Whether the pass has a depth attachment. @@ -280,29 +202,27 @@ struct BoundIndexBuffer { max_indices: u32, } -impl<'a> RenderPassEncoder<'a> { +impl<'pass> RenderPassEncoder<'pass> { /// Create a new render pass encoder (internal). fn new( - encoder: &'a mut platform::command::CommandEncoder, - config: RenderPassConfig, - color_attachments: &'a mut RenderColorAttachments<'a>, - depth_texture: Option<&'a DepthTexture>, - depth_ops: Option, - stencil_ops: Option, - label: Option<&'a str>, + encoder: &'pass mut platform::command::CommandEncoder, + pass: &'pass RenderPass, + color_attachments: &'pass mut RenderColorAttachments<'pass>, + depth_texture: Option<&'pass DepthTexture>, ) -> Self { // Build the platform render pass let mut rp_builder = platform::render_pass::RenderPassBuilder::new(); - // Map color operations - rp_builder = match config.color_operations.load { + // Map color operations from the high-level RenderPass + let color_ops = pass.color_operations(); + rp_builder = match color_ops.load { ColorLoadOp::Load => { rp_builder.with_color_load_op(platform::render_pass::ColorLoadOp::Load) } ColorLoadOp::Clear(color) => rp_builder .with_color_load_op(platform::render_pass::ColorLoadOp::Clear(color)), }; - rp_builder = match config.color_operations.store { + rp_builder = match color_ops.store { StoreOp::Store => { rp_builder.with_store_op(platform::render_pass::StoreOp::Store) } @@ -311,9 +231,9 @@ impl<'a> RenderPassEncoder<'a> { } }; - // Map depth operations - let platform_depth_ops = - depth_ops.map(|dop| platform::render_pass::DepthOperations { + // Map depth operations from the high-level RenderPass + let platform_depth_ops = pass.depth_operations().map(|dop| { + platform::render_pass::DepthOperations { load: match dop.load { DepthLoadOp::Load => platform::render_pass::DepthLoadOp::Load, DepthLoadOp::Clear(v) => { @@ -324,11 +244,12 @@ impl<'a> RenderPassEncoder<'a> { StoreOp::Store => platform::render_pass::StoreOp::Store, StoreOp::Discard => platform::render_pass::StoreOp::Discard, }, - }); + } + }); - // Map stencil operations - let platform_stencil_ops = - stencil_ops.map(|sop| platform::render_pass::StencilOperations { + // Map stencil operations from the high-level RenderPass + let platform_stencil_ops = pass.stencil_operations().map(|sop| { + platform::render_pass::StencilOperations { load: match sop.load { StencilLoadOp::Load => platform::render_pass::StencilLoadOp::Load, StencilLoadOp::Clear(v) => { @@ -339,27 +260,28 @@ impl<'a> RenderPassEncoder<'a> { StoreOp::Store => platform::render_pass::StoreOp::Store, StoreOp::Discard => platform::render_pass::StoreOp::Discard, }, - }); + } + }); let depth_view = depth_texture.map(|dt| dt.platform_view_ref()); let has_depth_attachment = depth_texture.is_some(); - let has_stencil = stencil_ops.is_some(); + let has_stencil = pass.stencil_operations().is_some(); - let pass = rp_builder.build( + let platform_pass = rp_builder.build( encoder, color_attachments.as_platform_attachments_mut(), depth_view, platform_depth_ops, platform_stencil_ops, - label, + pass.label(), ); - let mut encoder_instance = RenderPassEncoder { - pass, - uses_color: config.uses_color, + return RenderPassEncoder { + pass: platform_pass, + uses_color: pass.uses_color(), has_depth_attachment, has_stencil, - sample_count: config.sample_count, + sample_count: pass.sample_count(), #[cfg(any(debug_assertions, feature = "render-validation-encoder"))] current_pipeline: None, #[cfg(any(debug_assertions, feature = "render-validation-encoder"))] @@ -379,11 +301,6 @@ impl<'a> RenderPassEncoder<'a> { ))] warned_no_depth_for_pipeline: HashSet::new(), }; - - // Apply initial viewport - encoder_instance.set_viewport(&config.viewport); - - return encoder_instance; } /// Set the active render pipeline. @@ -496,6 +413,7 @@ impl<'a> RenderPassEncoder<'a> { self .pass .set_viewport(x, y, width, height, min_depth, max_depth); + let (sx, sy, sw, sh) = viewport.scissor_u32(); self.pass.set_scissor_rect(sx, sy, sw, sh); } From ccb573d345c5602936e2ca763fd01cfa260e1b3e Mon Sep 17 00:00:00 2001 From: vmarcella Date: Sat, 13 Dec 2025 16:08:46 -0800 Subject: [PATCH 13/28] [update] render context to use the new render pass encoder. --- crates/lambda-rs/src/render/mod.rs | 511 +++++------------------------ 1 file changed, 91 insertions(+), 420 deletions(-) diff --git a/crates/lambda-rs/src/render/mod.rs b/crates/lambda-rs/src/render/mod.rs index f4b8ba42..c0c23775 100644 --- a/crates/lambda-rs/src/render/mod.rs +++ b/crates/lambda-rs/src/render/mod.rs @@ -50,7 +50,6 @@ mod color_attachments; use std::{ collections::HashSet, - iter, rc::Rc, }; @@ -59,10 +58,14 @@ use logging; use self::{ command::RenderCommand, + encoder::{ + CommandEncoder, + RenderPassEncoder, + RenderPassError, + }, pipeline::RenderPipeline, render_pass::RenderPass as RenderPassDesc, }; -use crate::util; /// Builder for configuring a `RenderContext` tied to one window. /// @@ -455,10 +458,8 @@ impl RenderContext { }; let view = frame.texture_view(); - let mut encoder = platform::command::CommandEncoder::new( - self.gpu(), - Some("lambda-render-command-encoder"), - ); + let mut encoder = + CommandEncoder::new(self, "lambda-render-command-encoder"); let mut command_iter = commands.into_iter(); while let Some(command) = command_iter.next() { @@ -479,27 +480,6 @@ impl RenderContext { })? .clone(); - // Build (begin) the platform render pass using the builder API. - let mut rp_builder = platform::render_pass::RenderPassBuilder::new(); - let pass_label = pass.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) - } - }; - // Ensure MSAA texture exists if needed. let sample_count = pass.sample_count(); let uses_color = pass.uses_color(); @@ -528,11 +508,13 @@ impl RenderContext { pass.stencil_operations(), ); - let (depth_view, depth_ops) = if want_depth_attachment { + // Prepare depth texture if needed. + if want_depth_attachment { // Ensure depth texture exists, with proper sample count and format. let desired_samples = sample_count.max(1); - // If stencil is requested on the pass, ensure we use a stencil-capable format. + // If stencil is requested on the pass, ensure we use a + // stencil-capable format. if pass.stencil_operations().is_some() && self.depth_format != texture::DepthFormat::Depth24PlusStencil8 { @@ -541,7 +523,8 @@ impl RenderContext { feature = "render-validation-stencil", ))] logging::error!( - "Render pass has stencil ops but depth format {:?} lacks stencil; upgrading to Depth24PlusStencil8", + "Render pass has stencil ops but depth format {:?} lacks \ + stencil; upgrading to Depth24PlusStencil8", self.depth_format ); self.depth_format = texture::DepthFormat::Depth24PlusStencil8; @@ -567,59 +550,36 @@ impl RenderContext { ); self.depth_sample_count = desired_samples; } + } - let view_ref = self - .depth_texture - .as_ref() - .expect("depth texture should be present") - .platform_view_ref(); - - // Map depth operations when explicitly provided; leave depth - // untouched for stencil-only passes. - let depth_ops = Self::map_depth_ops(pass.depth_operations()); - (Some(view_ref), depth_ops) + let depth_texture_ref = if want_depth_attachment { + self.depth_texture.as_ref() } else { - (None, None) + None }; - // Optional stencil operations - let stencil_ops = pass.stencil_operations().map(|sop| { - platform::render_pass::StencilOperations { - load: match sop.load { - render_pass::StencilLoadOp::Load => { - platform::render_pass::StencilLoadOp::Load - } - render_pass::StencilLoadOp::Clear(v) => { - platform::render_pass::StencilLoadOp::Clear(v) - } - }, - store: match sop.store { - render_pass::StoreOp::Store => { - platform::render_pass::StoreOp::Store - } - render_pass::StoreOp::Discard => { - platform::render_pass::StoreOp::Discard - } - }, - } - }); - - let mut pass_encoder = rp_builder.build( - &mut encoder, - color_attachments.as_platform_attachments_mut(), - depth_view, - depth_ops, - stencil_ops, - pass_label, - ); - - self.encode_pass( - &mut pass_encoder, - pass.uses_color(), - want_depth_attachment, - pass.stencil_operations().is_some(), - viewport, - &mut command_iter, + // Use the high-level encoder's with_render_pass callback API. + let min_uniform_buffer_offset_alignment = + self.limit_min_uniform_buffer_offset_alignment(); + let render_pipelines = &self.render_pipelines; + let bind_groups = &self.bind_groups; + let buffers = &self.buffers; + + encoder.with_render_pass( + &pass, + &mut color_attachments, + depth_texture_ref, + |rp_encoder| { + Self::encode_pass_commands( + rp_encoder, + render_pipelines, + bind_groups, + buffers, + min_uniform_buffer_offset_alignment, + viewport, + &mut command_iter, + ) + }, )?; } other => { @@ -631,164 +591,53 @@ impl RenderContext { } } - self.gpu.submit(iter::once(encoder.finish())); + encoder.finish(self); frame.present(); return Ok(()); } - /// Encode a single render pass and consume commands until `EndRenderPass`. - fn encode_pass( - &self, - pass: &mut platform::render_pass::RenderPass<'_>, - uses_color: bool, - pass_has_depth_attachment: bool, - pass_has_stencil: bool, + /// Encode commands to a render pass encoder using the high-level API. + /// + /// This method processes `RenderCommand` items from the iterator until + /// `EndRenderPass` is encountered. Commands are translated to calls on the + /// `RenderPassEncoder`, which performs validation and issues GPU commands. + fn encode_pass_commands( + encoder: &mut RenderPassEncoder<'_>, + render_pipelines: &[RenderPipeline], + bind_groups: &[bind::BindGroup], + buffers: &[Rc], + min_uniform_buffer_offset_alignment: u32, initial_viewport: viewport::Viewport, commands: &mut Commands, - ) -> Result<(), RenderError> + ) -> Result<(), RenderPassError> where Commands: Iterator, { - Self::apply_viewport(pass, &initial_viewport); - - #[cfg(any(debug_assertions, feature = "render-validation-encoder",))] - let mut current_pipeline: Option = None; - - #[cfg(any(debug_assertions, feature = "render-validation-encoder",))] - let mut bound_index_buffer: Option<(usize, u32)> = None; - - #[cfg(any(debug_assertions, feature = "render-validation-instancing",))] - let mut bound_vertex_slots: HashSet = HashSet::new(); - - // De-duplicate advisories within this pass - #[cfg(any( - debug_assertions, - feature = "render-validation-depth", - feature = "render-validation-stencil", - ))] - let mut warned_no_stencil_for_pipeline: HashSet = HashSet::new(); - - #[cfg(any( - debug_assertions, - feature = "render-validation-depth", - feature = "render-validation-stencil", - ))] - let mut warned_no_depth_for_pipeline: HashSet = HashSet::new(); + encoder.set_viewport(&initial_viewport); while let Some(command) = commands.next() { match command { RenderCommand::EndRenderPass => return Ok(()), RenderCommand::SetStencilReference { reference } => { - pass.set_stencil_reference(reference); + encoder.set_stencil_reference(reference); } RenderCommand::SetPipeline { pipeline } => { let pipeline_ref = - self.render_pipelines.get(pipeline).ok_or_else(|| { - return RenderError::Configuration(format!( + render_pipelines.get(pipeline).ok_or_else(|| { + RenderPassError::Validation(format!( "Unknown pipeline {pipeline}" - )); + )) })?; - - // Validate pass/pipeline compatibility before deferring to the platform. - #[cfg(any( - debug_assertions, - feature = "render-validation-pass-compat", - feature = "render-validation-encoder", - ))] - { - if !uses_color && pipeline_ref.has_color_targets() { - let label = pipeline_ref.pipeline().label().unwrap_or("unnamed"); - return Err(RenderError::Configuration(format!( - "Render pipeline '{}' declares color targets but the current pass has no color attachments", - label - ))); - } - if uses_color && !pipeline_ref.has_color_targets() { - let label = pipeline_ref.pipeline().label().unwrap_or("unnamed"); - return Err(RenderError::Configuration(format!( - "Render pipeline '{}' has no color targets but the current pass declares color attachments", - label - ))); - } - if !pass_has_depth_attachment - && pipeline_ref.expects_depth_stencil() - { - let label = pipeline_ref.pipeline().label().unwrap_or("unnamed"); - return Err(RenderError::Configuration(format!( - "Render pipeline '{}' expects a depth/stencil attachment but the current pass has none", - label - ))); - } - } - - // Keep track of the current pipeline to ensure that draw calls - // happen only after a pipeline is set when validation is enabled. - #[cfg(any( - debug_assertions, - feature = "render-validation-encoder", - feature = "render-validation-instancing", - ))] - { - current_pipeline = Some(pipeline); - } - - // Advisory checks to help reason about stencil/depth behavior. - #[cfg(any( - debug_assertions, - feature = "render-validation-depth", - feature = "render-validation-stencil", - ))] - { - if pass_has_stencil - && !pipeline_ref.uses_stencil() - && warned_no_stencil_for_pipeline.insert(pipeline) - { - let label = pipeline_ref.pipeline().label().unwrap_or("unnamed"); - let key = format!("stencil:no_test:{}", label); - let msg = format!( - "Pass provides stencil ops but pipeline '{}' has no stencil test; stencil will not affect rendering", - label - ); - util::warn_once(&key, &msg); - } - - // Warn if pipeline uses stencil but pass has no stencil ops. - if !pass_has_stencil && pipeline_ref.uses_stencil() { - let label = pipeline_ref.pipeline().label().unwrap_or("unnamed"); - let key = format!("stencil:pass_no_operations:{}", label); - let msg = format!( - "Pipeline '{}' enables stencil but pass has no stencil ops configured; stencil reference/tests may be ineffective", - label - ); - util::warn_once(&key, &msg); - } - - // Warn if pass has depth attachment but pipeline does not test/write depth. - if pass_has_depth_attachment - && !pipeline_ref.expects_depth_stencil() - && warned_no_depth_for_pipeline.insert(pipeline) - { - let label = pipeline_ref.pipeline().label().unwrap_or("unnamed"); - let key = format!("depth:no_test:{}", label); - let msg = format!( - "Pass has depth attachment but pipeline '{}' does not enable depth testing; depth values will not be tested/written", - label - ); - util::warn_once(&key, &msg); - } - } - - pass.set_pipeline(pipeline_ref.pipeline()); + encoder.set_pipeline(pipeline_ref)?; } RenderCommand::SetViewports { viewports, .. } => { for viewport in viewports { - Self::apply_viewport(pass, &viewport); + encoder.set_viewport(&viewport); } } RenderCommand::SetScissors { viewports, .. } => { for viewport in viewports { - let (x, y, width, height) = viewport.scissor_u32(); - pass.set_scissor_rect(x, y, width, height); + encoder.set_scissor(&viewport); } } RenderCommand::SetBindGroup { @@ -796,94 +645,40 @@ impl RenderContext { group, dynamic_offsets, } => { - let group_ref = self.bind_groups.get(group).ok_or_else(|| { - return RenderError::Configuration(format!( - "Unknown bind group {group}" - )); + let group_ref = bind_groups.get(group).ok_or_else(|| { + RenderPassError::Validation(format!("Unknown bind group {group}")) })?; - // Validate dynamic offsets count and alignment before binding. - validation::validate_dynamic_offsets( - group_ref.dynamic_binding_count(), - &dynamic_offsets, - self.limit_min_uniform_buffer_offset_alignment(), - set, - ) - .map_err(RenderError::Configuration)?; - pass.set_bind_group( + encoder.set_bind_group( set, - group_ref.platform_group(), + group_ref, &dynamic_offsets, - ); + min_uniform_buffer_offset_alignment, + )?; } RenderCommand::BindVertexBuffer { pipeline, buffer } => { let pipeline_ref = - self.render_pipelines.get(pipeline).ok_or_else(|| { - return RenderError::Configuration(format!( + render_pipelines.get(pipeline).ok_or_else(|| { + RenderPassError::Validation(format!( "Unknown pipeline {pipeline}" - )); + )) })?; let buffer_ref = pipeline_ref.buffers().get(buffer as usize).ok_or_else(|| { - return RenderError::Configuration(format!( - "Vertex buffer index {buffer} not found for pipeline {pipeline}" - )); + RenderPassError::Validation(format!( + "Vertex buffer index {buffer} not found for pipeline \ + {pipeline}" + )) })?; - - #[cfg(any( - debug_assertions, - feature = "render-validation-instancing", - ))] - { - bound_vertex_slots.insert(buffer); - } - - pass.set_vertex_buffer(buffer as u32, buffer_ref.raw()); + encoder.set_vertex_buffer(buffer as u32, buffer_ref); } RenderCommand::BindIndexBuffer { buffer, format } => { - let buffer_ref = self.buffers.get(buffer).ok_or_else(|| { - return RenderError::Configuration(format!( + let buffer_ref = buffers.get(buffer).ok_or_else(|| { + RenderPassError::Validation(format!( "Index buffer id {} not found", buffer - )); + )) })?; - #[cfg(any(debug_assertions, feature = "render-validation-encoder",))] - { - if buffer_ref.buffer_type() != buffer::BufferType::Index { - return Err(RenderError::Configuration(format!( - "Binding buffer id {} as index but logical type is {:?}; expected BufferType::Index", - buffer, - buffer_ref.buffer_type() - ))); - } - let element_size = match format { - command::IndexFormat::Uint16 => 2u64, - command::IndexFormat::Uint32 => 4u64, - }; - let stride = buffer_ref.stride(); - if stride != element_size { - return Err(RenderError::Configuration(format!( - "Index buffer id {} has element stride {} bytes but BindIndexBuffer specified format {:?} ({} bytes)", - buffer, - stride, - format, - element_size - ))); - } - let buffer_size = buffer_ref.raw().size(); - if buffer_size % element_size != 0 { - return Err(RenderError::Configuration(format!( - "Index buffer id {} has size {} bytes which is not a multiple of element size {} for format {:?}", - buffer, - buffer_size, - element_size, - format - ))); - } - let max_indices = - (buffer_size / element_size).min(u32::MAX as u64) as u32; - bound_index_buffer = Some((buffer, max_indices)); - } - pass.set_index_buffer(buffer_ref.raw(), format.to_platform()); + encoder.set_index_buffer(buffer_ref, format)?; } RenderCommand::PushConstants { pipeline, @@ -891,10 +686,8 @@ impl RenderContext { offset, bytes, } => { - let _ = self.render_pipelines.get(pipeline).ok_or_else(|| { - return RenderError::Configuration(format!( - "Unknown pipeline {pipeline}" - )); + let _ = render_pipelines.get(pipeline).ok_or_else(|| { + RenderPassError::Validation(format!("Unknown pipeline {pipeline}")) })?; let slice = unsafe { std::slice::from_raw_parts( @@ -902,162 +695,34 @@ impl RenderContext { bytes.len() * std::mem::size_of::(), ) }; - pass.set_push_constants(stage, offset, slice); + encoder.set_push_constants(stage, offset, slice); } RenderCommand::Draw { vertices, instances, } => { - #[cfg(any(debug_assertions, feature = "render-validation-encoder",))] - { - if current_pipeline.is_none() { - return Err(RenderError::Configuration( - "Draw command encountered before any pipeline was set in this render pass" - .to_string(), - )); - } - } - - #[cfg(any( - debug_assertions, - feature = "render-validation-instancing", - ))] - { - let pipeline_index = current_pipeline - .expect("current_pipeline must be set when validation is active"); - let pipeline_ref = &self.render_pipelines[pipeline_index]; - - validation::validate_instance_bindings( - pipeline_ref.pipeline().label().unwrap_or("unnamed"), - pipeline_ref.per_instance_slots(), - &bound_vertex_slots, - ) - .map_err(RenderError::Configuration)?; - - if let Err(msg) = - validation::validate_instance_range("Draw", &instances) - { - return Err(RenderError::Configuration(msg)); - } - } - #[cfg(any( - debug_assertions, - feature = "render-validation-instancing", - ))] - { - if instances.start == instances.end { - logging::debug!( - "Skipping Draw with empty instance range {}..{}", - instances.start, - instances.end - ); - continue; - } - } - pass.draw(vertices, instances); + encoder.draw(vertices, instances)?; } RenderCommand::DrawIndexed { indices, base_vertex, instances, } => { - #[cfg(any(debug_assertions, feature = "render-validation-encoder",))] - { - if current_pipeline.is_none() { - return Err(RenderError::Configuration( - "DrawIndexed command encountered before any pipeline was set in this render pass" - .to_string(), - )); - } - let (buffer_id, max_indices) = match bound_index_buffer { - Some(state) => state, - None => { - return Err(RenderError::Configuration( - "DrawIndexed command encountered without a bound index buffer in this render pass" - .to_string(), - )); - } - }; - if indices.start > indices.end { - return Err(RenderError::Configuration(format!( - "DrawIndexed index range start {} is greater than end {} for index buffer id {}", - indices.start, - indices.end, - buffer_id - ))); - } - if indices.end > max_indices { - return Err(RenderError::Configuration(format!( - "DrawIndexed index range {}..{} exceeds bound index buffer id {} capacity {}", - indices.start, - indices.end, - buffer_id, - max_indices - ))); - } - } - #[cfg(any( - debug_assertions, - feature = "render-validation-instancing", - ))] - { - let pipeline_index = current_pipeline - .expect("current_pipeline must be set when validation is active"); - let pipeline_ref = &self.render_pipelines[pipeline_index]; - - validation::validate_instance_bindings( - pipeline_ref.pipeline().label().unwrap_or("unnamed"), - pipeline_ref.per_instance_slots(), - &bound_vertex_slots, - ) - .map_err(RenderError::Configuration)?; - - if let Err(msg) = - validation::validate_instance_range("DrawIndexed", &instances) - { - return Err(RenderError::Configuration(msg)); - } - } - #[cfg(any( - debug_assertions, - feature = "render-validation-instancing", - ))] - { - if instances.start == instances.end { - logging::debug!( - "Skipping DrawIndexed with empty instance range {}..{}", - instances.start, - instances.end - ); - continue; - } - } - pass.draw_indexed(indices, base_vertex, instances); + encoder.draw_indexed(indices, base_vertex, instances)?; } RenderCommand::BeginRenderPass { .. } => { - return Err(RenderError::Configuration( + return Err(RenderPassError::Validation( "Nested render passes are not supported.".to_string(), )); } } } - return Err(RenderError::Configuration( + return Err(RenderPassError::Validation( "Render pass did not terminate with EndRenderPass".to_string(), )); } - /// Apply both viewport and scissor state to the active pass. - fn apply_viewport( - pass: &mut platform::render_pass::RenderPass<'_>, - viewport: &viewport::Viewport, - ) { - let (x, y, width, height, min_depth, max_depth) = viewport.viewport_f32(); - pass.set_viewport(x, y, width, height, min_depth, max_depth); - let (sx, sy, sw, sh) = viewport.scissor_u32(); - pass.set_scissor_rect(sx, sy, sw, sh); - } - /// Reconfigure the presentation surface using current present mode/usage. fn reconfigure_surface( &mut self, @@ -1129,6 +794,12 @@ impl From for RenderError { } } +impl From for RenderError { + fn from(error: RenderPassError) -> Self { + return RenderError::Configuration(error.to_string()); + } +} + /// Errors encountered while creating a `RenderContext`. #[derive(Debug)] /// From 700f51fa4ae898826a28648b21613e15351c2668 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Sat, 13 Dec 2025 16:09:45 -0800 Subject: [PATCH 14/28] [remove] depth op converstions from render context. --- crates/lambda-rs/src/render/mod.rs | 40 ------------------------------ 1 file changed, 40 deletions(-) diff --git a/crates/lambda-rs/src/render/mod.rs b/crates/lambda-rs/src/render/mod.rs index c0c23775..1953c5bc 100644 --- a/crates/lambda-rs/src/render/mod.rs +++ b/crates/lambda-rs/src/render/mod.rs @@ -752,29 +752,6 @@ impl RenderContext { ) -> bool { return depth_ops.is_some() || stencil_ops.is_some(); } - - /// Map high-level depth operations to platform depth operations, returning - /// `None` when no depth operations were requested. - fn map_depth_ops( - depth_ops: Option, - ) -> Option { - return depth_ops.map(|dops| platform::render_pass::DepthOperations { - load: match dops.load { - render_pass::DepthLoadOp::Load => { - platform::render_pass::DepthLoadOp::Load - } - render_pass::DepthLoadOp::Clear(value) => { - platform::render_pass::DepthLoadOp::Clear(value as f32) - } - }, - store: match dops.store { - render_pass::StoreOp::Store => platform::render_pass::StoreOp::Store, - render_pass::StoreOp::Discard => { - platform::render_pass::StoreOp::Discard - } - }, - }); - } } /// Errors reported while preparing or presenting a frame. @@ -850,21 +827,4 @@ mod tests { let has_attachment = RenderContext::has_depth_attachment(None, stencil_ops); assert!(has_attachment); } - - #[test] - fn map_depth_ops_none_when_no_depth_operations() { - let mapped = RenderContext::map_depth_ops(None); - assert!(mapped.is_none()); - } - - #[test] - fn map_depth_ops_maps_clear_and_store() { - let depth_ops = render_pass::DepthOperations { - load: render_pass::DepthLoadOp::Clear(0.5), - store: render_pass::StoreOp::Store, - }; - let mapped = RenderContext::map_depth_ops(Some(depth_ops)).expect("mapped"); - assert_eq!(mapped.load, platform::render_pass::DepthLoadOp::Clear(0.5)); - assert_eq!(mapped.store, platform::render_pass::StoreOp::Store); - } } From b5c8aca110639da1eb1c13a89166de6e1a960c40 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Sat, 13 Dec 2025 16:13:21 -0800 Subject: [PATCH 15/28] [remove] encode_pass function and handle all command processing inside of the callback. --- crates/lambda-rs/src/render/mod.rs | 252 +++++++++++++---------------- 1 file changed, 116 insertions(+), 136 deletions(-) diff --git a/crates/lambda-rs/src/render/mod.rs b/crates/lambda-rs/src/render/mod.rs index 1953c5bc..a02e75fd 100644 --- a/crates/lambda-rs/src/render/mod.rs +++ b/crates/lambda-rs/src/render/mod.rs @@ -570,15 +570,122 @@ impl RenderContext { &mut color_attachments, depth_texture_ref, |rp_encoder| { - Self::encode_pass_commands( - rp_encoder, - render_pipelines, - bind_groups, - buffers, - min_uniform_buffer_offset_alignment, - viewport, - &mut command_iter, - ) + rp_encoder.set_viewport(&viewport); + + while let Some(cmd) = command_iter.next() { + match cmd { + RenderCommand::EndRenderPass => return Ok(()), + RenderCommand::SetStencilReference { reference } => { + rp_encoder.set_stencil_reference(reference); + } + RenderCommand::SetPipeline { pipeline } => { + let pipeline_ref = + render_pipelines.get(pipeline).ok_or_else(|| { + RenderPassError::Validation(format!( + "Unknown pipeline {pipeline}" + )) + })?; + rp_encoder.set_pipeline(pipeline_ref)?; + } + RenderCommand::SetViewports { viewports, .. } => { + for vp in viewports { + rp_encoder.set_viewport(&vp); + } + } + RenderCommand::SetScissors { viewports, .. } => { + for vp in viewports { + rp_encoder.set_scissor(&vp); + } + } + RenderCommand::SetBindGroup { + set, + group, + dynamic_offsets, + } => { + let group_ref = + bind_groups.get(group).ok_or_else(|| { + RenderPassError::Validation(format!( + "Unknown bind group {group}" + )) + })?; + rp_encoder.set_bind_group( + set, + group_ref, + &dynamic_offsets, + min_uniform_buffer_offset_alignment, + )?; + } + RenderCommand::BindVertexBuffer { pipeline, buffer } => { + let pipeline_ref = + render_pipelines.get(pipeline).ok_or_else(|| { + RenderPassError::Validation(format!( + "Unknown pipeline {pipeline}" + )) + })?; + let buffer_ref = pipeline_ref + .buffers() + .get(buffer as usize) + .ok_or_else(|| { + RenderPassError::Validation(format!( + "Vertex buffer index {buffer} not found for \ + pipeline {pipeline}" + )) + })?; + rp_encoder.set_vertex_buffer(buffer as u32, buffer_ref); + } + RenderCommand::BindIndexBuffer { buffer, format } => { + let buffer_ref = buffers.get(buffer).ok_or_else(|| { + RenderPassError::Validation(format!( + "Index buffer id {} not found", + buffer + )) + })?; + rp_encoder.set_index_buffer(buffer_ref, format)?; + } + RenderCommand::PushConstants { + pipeline, + stage, + offset, + bytes, + } => { + let _ = + render_pipelines.get(pipeline).ok_or_else(|| { + RenderPassError::Validation(format!( + "Unknown pipeline {pipeline}" + )) + })?; + let slice = unsafe { + std::slice::from_raw_parts( + bytes.as_ptr() as *const u8, + bytes.len() * std::mem::size_of::(), + ) + }; + rp_encoder.set_push_constants(stage, offset, slice); + } + RenderCommand::Draw { + vertices, + instances, + } => { + rp_encoder.draw(vertices, instances)?; + } + RenderCommand::DrawIndexed { + indices, + base_vertex, + instances, + } => { + rp_encoder.draw_indexed(indices, base_vertex, instances)?; + } + RenderCommand::BeginRenderPass { .. } => { + return Err(RenderPassError::Validation( + "Nested render passes are not supported.".to_string(), + )); + } + } + } + + return Err(RenderPassError::Validation( + "Render pass did not terminate with EndRenderPass".to_string(), + )); }, )?; } @@ -596,133 +703,6 @@ impl RenderContext { return Ok(()); } - /// Encode commands to a render pass encoder using the high-level API. - /// - /// This method processes `RenderCommand` items from the iterator until - /// `EndRenderPass` is encountered. Commands are translated to calls on the - /// `RenderPassEncoder`, which performs validation and issues GPU commands. - fn encode_pass_commands( - encoder: &mut RenderPassEncoder<'_>, - render_pipelines: &[RenderPipeline], - bind_groups: &[bind::BindGroup], - buffers: &[Rc], - min_uniform_buffer_offset_alignment: u32, - initial_viewport: viewport::Viewport, - commands: &mut Commands, - ) -> Result<(), RenderPassError> - where - Commands: Iterator, - { - encoder.set_viewport(&initial_viewport); - - while let Some(command) = commands.next() { - match command { - RenderCommand::EndRenderPass => return Ok(()), - RenderCommand::SetStencilReference { reference } => { - encoder.set_stencil_reference(reference); - } - RenderCommand::SetPipeline { pipeline } => { - let pipeline_ref = - render_pipelines.get(pipeline).ok_or_else(|| { - RenderPassError::Validation(format!( - "Unknown pipeline {pipeline}" - )) - })?; - encoder.set_pipeline(pipeline_ref)?; - } - RenderCommand::SetViewports { viewports, .. } => { - for viewport in viewports { - encoder.set_viewport(&viewport); - } - } - RenderCommand::SetScissors { viewports, .. } => { - for viewport in viewports { - encoder.set_scissor(&viewport); - } - } - RenderCommand::SetBindGroup { - set, - group, - dynamic_offsets, - } => { - let group_ref = bind_groups.get(group).ok_or_else(|| { - RenderPassError::Validation(format!("Unknown bind group {group}")) - })?; - encoder.set_bind_group( - set, - group_ref, - &dynamic_offsets, - min_uniform_buffer_offset_alignment, - )?; - } - RenderCommand::BindVertexBuffer { pipeline, buffer } => { - let pipeline_ref = - render_pipelines.get(pipeline).ok_or_else(|| { - RenderPassError::Validation(format!( - "Unknown pipeline {pipeline}" - )) - })?; - let buffer_ref = - pipeline_ref.buffers().get(buffer as usize).ok_or_else(|| { - RenderPassError::Validation(format!( - "Vertex buffer index {buffer} not found for pipeline \ - {pipeline}" - )) - })?; - encoder.set_vertex_buffer(buffer as u32, buffer_ref); - } - RenderCommand::BindIndexBuffer { buffer, format } => { - let buffer_ref = buffers.get(buffer).ok_or_else(|| { - RenderPassError::Validation(format!( - "Index buffer id {} not found", - buffer - )) - })?; - encoder.set_index_buffer(buffer_ref, format)?; - } - RenderCommand::PushConstants { - pipeline, - stage, - offset, - bytes, - } => { - let _ = render_pipelines.get(pipeline).ok_or_else(|| { - RenderPassError::Validation(format!("Unknown pipeline {pipeline}")) - })?; - let slice = unsafe { - std::slice::from_raw_parts( - bytes.as_ptr() as *const u8, - bytes.len() * std::mem::size_of::(), - ) - }; - encoder.set_push_constants(stage, offset, slice); - } - RenderCommand::Draw { - vertices, - instances, - } => { - encoder.draw(vertices, instances)?; - } - RenderCommand::DrawIndexed { - indices, - base_vertex, - instances, - } => { - encoder.draw_indexed(indices, base_vertex, instances)?; - } - RenderCommand::BeginRenderPass { .. } => { - return Err(RenderPassError::Validation( - "Nested render passes are not supported.".to_string(), - )); - } - } - } - - return Err(RenderPassError::Validation( - "Render pass did not terminate with EndRenderPass".to_string(), - )); - } - /// Reconfigure the presentation surface using current present mode/usage. fn reconfigure_surface( &mut self, From 87931940f6cc91b8e311660927fefdc29845ba69 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Sat, 13 Dec 2025 16:13:59 -0800 Subject: [PATCH 16/28] [remove] import. --- crates/lambda-rs/src/render/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/lambda-rs/src/render/mod.rs b/crates/lambda-rs/src/render/mod.rs index a02e75fd..683b9d00 100644 --- a/crates/lambda-rs/src/render/mod.rs +++ b/crates/lambda-rs/src/render/mod.rs @@ -60,7 +60,6 @@ use self::{ command::RenderCommand, encoder::{ CommandEncoder, - RenderPassEncoder, RenderPassError, }, pipeline::RenderPipeline, From 1ff0eac2d533ad7da565e2e728af036125be9ab3 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Sat, 13 Dec 2025 16:15:51 -0800 Subject: [PATCH 17/28] [update] name. --- crates/lambda-rs/src/render/encoder.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/lambda-rs/src/render/encoder.rs b/crates/lambda-rs/src/render/encoder.rs index 69b75ba9..9f08612e 100644 --- a/crates/lambda-rs/src/render/encoder.rs +++ b/crates/lambda-rs/src/render/encoder.rs @@ -91,7 +91,7 @@ impl CommandEncoder { /// * `pass` - The high-level render pass configuration. /// * `color_attachments` - Color attachment views for the pass. /// * `depth_texture` - Optional depth texture for depth/stencil operations. - /// * `f` - Closure that records commands to the render pass encoder. + /// * `func` - Closure that records commands to the render pass encoder. /// /// # Type Parameters /// * `'pass` - Lifetime of resources borrowed during the render pass. @@ -105,7 +105,7 @@ impl CommandEncoder { pass: &'pass RenderPass, color_attachments: &'pass mut RenderColorAttachments<'pass>, depth_texture: Option<&'pass DepthTexture>, - f: PassFn, + func: PassFn, ) -> Result where PassFn: @@ -118,7 +118,7 @@ impl CommandEncoder { depth_texture, ); - return f(&mut { pass_encoder }); + return func(&mut { pass_encoder }); } /// Finish recording and submit the command buffer to the GPU. From 36d63634f8b2ed55cde648b6abde4d48c2c27f0e Mon Sep 17 00:00:00 2001 From: vmarcella Date: Sat, 13 Dec 2025 16:19:00 -0800 Subject: [PATCH 18/28] [remove] duplicate present mode. --- crates/lambda-rs/src/render/mod.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/crates/lambda-rs/src/render/mod.rs b/crates/lambda-rs/src/render/mod.rs index 683b9d00..4fc079cb 100644 --- a/crates/lambda-rs/src/render/mod.rs +++ b/crates/lambda-rs/src/render/mod.rs @@ -160,7 +160,6 @@ impl RenderContextBuilder { ) })?; let config = surface::SurfaceConfig::from_platform(config); - let present_mode = config.present_mode; let texture_usage = config.usage; // Initialize a depth texture matching the surface size. @@ -172,7 +171,6 @@ impl RenderContextBuilder { surface, gpu, config, - present_mode, texture_usage, size, depth_texture: None, @@ -212,7 +210,6 @@ pub struct RenderContext { surface: platform::surface::Surface<'static>, gpu: platform::gpu::Gpu, config: surface::SurfaceConfig, - present_mode: surface::PresentMode, texture_usage: texture::TextureUsages, size: (u32, u32), depth_texture: Option, @@ -717,7 +714,6 @@ impl RenderContext { })?; let config = surface::SurfaceConfig::from_platform(platform_config); - self.present_mode = config.present_mode; self.texture_usage = config.usage; self.config = config; return Ok(()); From 5b5c64f6a3ae20e71b6c70877ebb4294a2eb36a9 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Sat, 13 Dec 2025 16:25:33 -0800 Subject: [PATCH 19/28] [add] platform conversions to depth/stencil ops directly. --- crates/lambda-rs/src/render/encoder.rs | 66 +++--------------- crates/lambda-rs/src/render/render_pass.rs | 78 ++++++++++++++++++++++ 2 files changed, 89 insertions(+), 55 deletions(-) diff --git a/crates/lambda-rs/src/render/encoder.rs b/crates/lambda-rs/src/render/encoder.rs index 9f08612e..8d9ad566 100644 --- a/crates/lambda-rs/src/render/encoder.rs +++ b/crates/lambda-rs/src/render/encoder.rs @@ -41,13 +41,7 @@ use super::{ command::IndexFormat, pipeline, pipeline::RenderPipeline, - render_pass::{ - ColorLoadOp, - DepthLoadOp, - RenderPass, - StencilLoadOp, - StoreOp, - }, + render_pass::RenderPass, texture::DepthTexture, validation, viewport::Viewport, @@ -214,54 +208,16 @@ impl<'pass> RenderPassEncoder<'pass> { let mut rp_builder = platform::render_pass::RenderPassBuilder::new(); // Map color operations from the high-level RenderPass - let color_ops = pass.color_operations(); - rp_builder = match color_ops.load { - ColorLoadOp::Load => { - rp_builder.with_color_load_op(platform::render_pass::ColorLoadOp::Load) - } - ColorLoadOp::Clear(color) => rp_builder - .with_color_load_op(platform::render_pass::ColorLoadOp::Clear(color)), - }; - rp_builder = match color_ops.store { - StoreOp::Store => { - rp_builder.with_store_op(platform::render_pass::StoreOp::Store) - } - StoreOp::Discard => { - rp_builder.with_store_op(platform::render_pass::StoreOp::Discard) - } - }; - - // Map depth operations from the high-level RenderPass - let platform_depth_ops = pass.depth_operations().map(|dop| { - platform::render_pass::DepthOperations { - load: match dop.load { - DepthLoadOp::Load => platform::render_pass::DepthLoadOp::Load, - DepthLoadOp::Clear(v) => { - platform::render_pass::DepthLoadOp::Clear(v as f32) - } - }, - store: match dop.store { - StoreOp::Store => platform::render_pass::StoreOp::Store, - StoreOp::Discard => platform::render_pass::StoreOp::Discard, - }, - } - }); - - // Map stencil operations from the high-level RenderPass - let platform_stencil_ops = pass.stencil_operations().map(|sop| { - platform::render_pass::StencilOperations { - load: match sop.load { - StencilLoadOp::Load => platform::render_pass::StencilLoadOp::Load, - StencilLoadOp::Clear(v) => { - platform::render_pass::StencilLoadOp::Clear(v) - } - }, - store: match sop.store { - StoreOp::Store => platform::render_pass::StoreOp::Store, - StoreOp::Discard => platform::render_pass::StoreOp::Discard, - }, - } - }); + let (color_load_op, color_store_op) = pass.color_operations().to_platform(); + rp_builder = rp_builder + .with_color_load_op(color_load_op) + .with_store_op(color_store_op); + + // Map depth and stencil operations from the high-level RenderPass + let platform_depth_ops = + pass.depth_operations().map(|dop| dop.to_platform()); + let platform_stencil_ops = + pass.stencil_operations().map(|sop| sop.to_platform()); let depth_view = depth_texture.map(|dt| dt.platform_view_ref()); let has_depth_attachment = depth_texture.is_some(); diff --git a/crates/lambda-rs/src/render/render_pass.rs b/crates/lambda-rs/src/render/render_pass.rs index 88d9f833..158069d1 100644 --- a/crates/lambda-rs/src/render/render_pass.rs +++ b/crates/lambda-rs/src/render/render_pass.rs @@ -23,6 +23,18 @@ pub enum ColorLoadOp { Clear([f64; 4]), } +impl ColorLoadOp { + /// Convert to the platform color load operation. + pub(crate) fn to_platform(self) -> platform::render_pass::ColorLoadOp { + return match self { + ColorLoadOp::Load => platform::render_pass::ColorLoadOp::Load, + ColorLoadOp::Clear(color) => { + platform::render_pass::ColorLoadOp::Clear(color) + } + }; + } +} + /// Store operation for the first color attachment. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum StoreOp { @@ -32,6 +44,16 @@ pub enum StoreOp { Discard, } +impl StoreOp { + /// Convert to the platform store operation. + pub(crate) fn to_platform(self) -> platform::render_pass::StoreOp { + return match self { + StoreOp::Store => platform::render_pass::StoreOp::Store, + StoreOp::Discard => platform::render_pass::StoreOp::Discard, + }; + } +} + /// Combined color operations for the first color attachment. #[derive(Debug, Clone, Copy, PartialEq)] pub struct ColorOperations { @@ -48,6 +70,18 @@ impl Default for ColorOperations { } } +impl ColorOperations { + /// Convert to the platform color load and store operations. + pub(crate) fn to_platform( + self, + ) -> ( + platform::render_pass::ColorLoadOp, + platform::render_pass::StoreOp, + ) { + return (self.load.to_platform(), self.store.to_platform()); + } +} + /// Depth load operation for the depth attachment. #[derive(Debug, Clone, Copy, PartialEq)] pub enum DepthLoadOp { @@ -57,6 +91,18 @@ pub enum DepthLoadOp { Clear(f64), } +impl DepthLoadOp { + /// Convert to the platform depth load operation. + pub(crate) fn to_platform(self) -> platform::render_pass::DepthLoadOp { + return match self { + DepthLoadOp::Load => platform::render_pass::DepthLoadOp::Load, + DepthLoadOp::Clear(value) => { + platform::render_pass::DepthLoadOp::Clear(value as f32) + } + }; + } +} + /// Depth operations for the first depth attachment. #[derive(Debug, Clone, Copy, PartialEq)] pub struct DepthOperations { @@ -73,6 +119,16 @@ impl Default for DepthOperations { } } +impl DepthOperations { + /// Convert to the platform depth operations. + pub(crate) fn to_platform(self) -> platform::render_pass::DepthOperations { + return platform::render_pass::DepthOperations { + load: self.load.to_platform(), + store: self.store.to_platform(), + }; + } +} + /// Immutable parameters used when beginning a render pass. #[derive(Debug, Clone)] /// @@ -361,6 +417,18 @@ pub enum StencilLoadOp { Clear(u32), } +impl StencilLoadOp { + /// Convert to the platform stencil load operation. + pub(crate) fn to_platform(self) -> platform::render_pass::StencilLoadOp { + return match self { + StencilLoadOp::Load => platform::render_pass::StencilLoadOp::Load, + StencilLoadOp::Clear(value) => { + platform::render_pass::StencilLoadOp::Clear(value) + } + }; + } +} + /// Stencil operations for the first stencil attachment. #[derive(Debug, Clone, Copy, PartialEq)] pub struct StencilOperations { @@ -377,6 +445,16 @@ impl Default for StencilOperations { } } +impl StencilOperations { + /// Convert to the platform stencil operations. + pub(crate) fn to_platform(self) -> platform::render_pass::StencilOperations { + return platform::render_pass::StencilOperations { + load: self.load.to_platform(), + store: self.store.to_platform(), + }; + } +} + #[cfg(test)] mod tests { use std::cell::RefCell; From 1020d1e97fac05c1d81cb8a7af765a3fa1fd0878 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Sat, 13 Dec 2025 16:38:11 -0800 Subject: [PATCH 20/28] [add] first high level implementation for render targets and gpu. --- crates/lambda-rs/src/render/gpu.rs | 291 +++++++++++++++++++ crates/lambda-rs/src/render/mod.rs | 2 + crates/lambda-rs/src/render/render_target.rs | 221 ++++++++++++++ 3 files changed, 514 insertions(+) create mode 100644 crates/lambda-rs/src/render/gpu.rs create mode 100644 crates/lambda-rs/src/render/render_target.rs diff --git a/crates/lambda-rs/src/render/gpu.rs b/crates/lambda-rs/src/render/gpu.rs new file mode 100644 index 00000000..82d0023f --- /dev/null +++ b/crates/lambda-rs/src/render/gpu.rs @@ -0,0 +1,291 @@ +//! High-level GPU abstraction for resource creation and command submission. +//! +//! The `Gpu` type wraps the platform GPU device and queue, providing a stable +//! engine-facing API for creating resources and submitting work. This +//! abstraction enables future support for multiple render targets and +//! backend flexibility. +//! +//! # Usage +//! +//! The `Gpu` is typically created during render context initialization and +//! shared across resource builders: +//! +//! ```ignore +//! let gpu = GpuBuilder::new() +//! .with_label("My GPU") +//! .build(&instance, Some(&surface))?; +//! +//! // Use gpu for resource creation +//! let buffer = BufferBuilder::new() +//! .with_size(1024) +//! .build(&gpu); +//! ``` + +use lambda_platform::wgpu as platform; + +use super::texture::{ + DepthFormat, + TextureFormat, +}; + +// --------------------------------------------------------------------------- +// GpuLimits +// --------------------------------------------------------------------------- + +/// Device limits exposed to the engine layer. +/// +/// These limits are queried from the physical device and constrain resource +/// creation and binding. The engine uses these to validate configurations +/// before creating GPU resources. +#[derive(Clone, Copy, Debug)] +pub struct GpuLimits { + /// Maximum bytes that can be bound for a single uniform buffer binding. + pub max_uniform_buffer_binding_size: u64, + /// Maximum number of bind groups that can be used by a pipeline layout. + pub max_bind_groups: u32, + /// Maximum number of vertex buffers that can be bound. + pub max_vertex_buffers: u32, + /// Maximum number of vertex attributes that can be declared. + pub max_vertex_attributes: u32, + /// Required alignment in bytes for dynamic uniform buffer offsets. + pub min_uniform_buffer_offset_alignment: u32, +} + +impl GpuLimits { + /// Create limits from the platform GPU limits. + pub(crate) fn from_platform(limits: platform::gpu::GpuLimits) -> Self { + return GpuLimits { + max_uniform_buffer_binding_size: limits.max_uniform_buffer_binding_size, + max_bind_groups: limits.max_bind_groups, + max_vertex_buffers: limits.max_vertex_buffers, + max_vertex_attributes: limits.max_vertex_attributes, + min_uniform_buffer_offset_alignment: limits + .min_uniform_buffer_offset_alignment, + }; + } +} + +// --------------------------------------------------------------------------- +// Gpu +// --------------------------------------------------------------------------- + +/// High-level GPU device and queue wrapper. +/// +/// The `Gpu` provides a stable interface for: +/// - Submitting command buffers to the GPU queue +/// - Querying device limits for resource validation +/// - Checking format and sample count support +/// +/// This type does not expose platform internals directly, allowing the +/// engine to evolve independently of the underlying graphics API. +pub struct Gpu { + inner: platform::gpu::Gpu, + limits: GpuLimits, +} + +impl Gpu { + /// Create a new high-level GPU from a platform GPU. + pub(crate) fn from_platform(gpu: platform::gpu::Gpu) -> Self { + let limits = GpuLimits::from_platform(gpu.limits()); + return Gpu { inner: gpu, limits }; + } + + /// Borrow the underlying platform GPU for internal use. + /// + /// This is crate-visible to allow resource builders and other internal + /// code to access the platform device without exposing it publicly. + #[inline] + pub(crate) fn platform(&self) -> &platform::gpu::Gpu { + return &self.inner; + } + + /// Query the device limits. + #[inline] + pub fn limits(&self) -> &GpuLimits { + return &self.limits; + } + + /// Submit command buffers to the GPU queue. + /// + /// The submitted buffers are executed in order. This method does not block; + /// use fences or map callbacks for synchronization. + #[inline] + pub fn submit(&self, buffers: I) + where + I: IntoIterator, + { + self.inner.submit(buffers); + } + + /// Check if the GPU supports the given sample count for a texture format. + /// + /// Returns `true` if the format can be used as a render attachment with + /// the specified sample count for MSAA. + pub fn supports_sample_count_for_format( + &self, + format: TextureFormat, + sample_count: u32, + ) -> bool { + return self + .inner + .supports_sample_count_for_format(format.to_platform(), sample_count); + } + + /// Check if the GPU supports the given sample count for a depth format. + /// + /// Returns `true` if the depth format can be used as a depth/stencil + /// attachment with the specified sample count for MSAA. + pub fn supports_sample_count_for_depth( + &self, + format: DepthFormat, + sample_count: u32, + ) -> bool { + return self + .inner + .supports_sample_count_for_depth(format.to_platform(), sample_count); + } + + /// Maximum bytes that can be bound for a single uniform buffer binding. + #[inline] + pub fn limit_max_uniform_buffer_binding_size(&self) -> u64 { + return self.limits.max_uniform_buffer_binding_size; + } + + /// Number of bind groups that can be used by a pipeline layout. + #[inline] + pub fn limit_max_bind_groups(&self) -> u32 { + return self.limits.max_bind_groups; + } + + /// Maximum number of vertex buffers that can be bound. + #[inline] + pub fn limit_max_vertex_buffers(&self) -> u32 { + return self.limits.max_vertex_buffers; + } + + /// Maximum number of vertex attributes that can be declared. + #[inline] + pub fn limit_max_vertex_attributes(&self) -> u32 { + return self.limits.max_vertex_attributes; + } + + /// Required alignment in bytes for dynamic uniform buffer offsets. + #[inline] + pub fn limit_min_uniform_buffer_offset_alignment(&self) -> u32 { + return self.limits.min_uniform_buffer_offset_alignment; + } +} + +impl std::fmt::Debug for Gpu { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return f + .debug_struct("Gpu") + .field("limits", &self.limits) + .finish_non_exhaustive(); + } +} + +// --------------------------------------------------------------------------- +// GpuBuilder +// --------------------------------------------------------------------------- + +/// Builder for creating a `Gpu` with configurable options. +/// +/// The builder configures adapter selection, required features, and memory +/// hints before requesting the logical device. +pub struct GpuBuilder { + inner: platform::gpu::GpuBuilder, +} + +impl GpuBuilder { + /// Create a new builder with default settings. + /// + /// Defaults: + /// - High performance power preference + /// - Push constants enabled + /// - Performance-oriented memory hints + pub fn new() -> Self { + return GpuBuilder { + inner: platform::gpu::GpuBuilder::new(), + }; + } + + /// Attach a label for debugging and profiling. + pub fn with_label(mut self, label: &str) -> Self { + self.inner = self.inner.with_label(label); + return self; + } + + /// Build the GPU using the provided instance and optional surface. + /// + /// The surface is used to ensure the adapter is compatible with + /// presentation. Pass `None` for headless/compute-only contexts. + pub fn build( + self, + instance: &platform::instance::Instance, + surface: Option<&platform::surface::Surface<'_>>, + ) -> Result { + let platform_gpu = self + .inner + .build(instance, surface) + .map_err(GpuBuildError::from_platform)?; + return Ok(Gpu::from_platform(platform_gpu)); + } +} + +impl Default for GpuBuilder { + fn default() -> Self { + return Self::new(); + } +} + +// --------------------------------------------------------------------------- +// GpuBuildError +// --------------------------------------------------------------------------- + +/// Errors that can occur when building a `Gpu`. +#[derive(Debug)] +pub enum GpuBuildError { + /// No compatible GPU adapter was found. + AdapterUnavailable, + /// Required features are not supported by the adapter. + MissingFeatures(String), + /// Device creation failed. + DeviceCreationFailed(String), +} + +impl GpuBuildError { + fn from_platform(error: platform::gpu::GpuBuildError) -> Self { + return match error { + platform::gpu::GpuBuildError::AdapterUnavailable => { + GpuBuildError::AdapterUnavailable + } + platform::gpu::GpuBuildError::MissingFeatures { + requested, + available, + } => GpuBuildError::MissingFeatures(format!( + "Requested features {:?} not available; adapter supports {:?}", + requested, available + )), + platform::gpu::GpuBuildError::RequestDevice(msg) => { + GpuBuildError::DeviceCreationFailed(msg) + } + }; + } +} + +impl std::fmt::Display for GpuBuildError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return match self { + GpuBuildError::AdapterUnavailable => { + write!(f, "No compatible GPU adapter found") + } + GpuBuildError::MissingFeatures(msg) => write!(f, "{}", msg), + GpuBuildError::DeviceCreationFailed(msg) => { + write!(f, "Device creation failed: {}", msg) + } + }; + } +} + +impl std::error::Error for GpuBuildError {} diff --git a/crates/lambda-rs/src/render/mod.rs b/crates/lambda-rs/src/render/mod.rs index 4fc079cb..58a56372 100644 --- a/crates/lambda-rs/src/render/mod.rs +++ b/crates/lambda-rs/src/render/mod.rs @@ -33,9 +33,11 @@ pub mod bind; pub mod buffer; pub mod command; pub mod encoder; +pub mod gpu; pub mod mesh; pub mod pipeline; pub mod render_pass; +pub mod render_target; pub mod scene_math; pub mod shader; pub mod surface; diff --git a/crates/lambda-rs/src/render/render_target.rs b/crates/lambda-rs/src/render/render_target.rs new file mode 100644 index 00000000..a53be7a3 --- /dev/null +++ b/crates/lambda-rs/src/render/render_target.rs @@ -0,0 +1,221 @@ +//! Render target abstraction for different presentation backends. +//! +//! The `RenderTarget` trait defines the interface for acquiring frames and +//! presenting rendered content. Implementations include: +//! +//! - `WindowSurface`: Renders to a window's swapchain (the common case) +//! - Future: `OffscreenTarget` for headless rendering to textures +//! +//! # Usage +//! +//! Render targets are used by the render context to acquire frames: +//! +//! ```ignore +//! let frame = render_target.acquire_frame()?; +//! // ... encode and submit commands ... +//! frame.present(); +//! ``` + +use lambda_platform::wgpu as platform; + +use super::{ + gpu::Gpu, + surface::{ + Frame, + PresentMode, + SurfaceConfig, + SurfaceError, + }, + texture::{ + TextureFormat, + TextureUsages, + }, + window::Window, +}; + +// --------------------------------------------------------------------------- +// RenderTarget trait +// --------------------------------------------------------------------------- + +/// Trait for render targets that can acquire and present frames. +/// +/// This abstraction enables different rendering backends: +/// - Window surfaces for on-screen rendering +/// - Offscreen textures for headless/screenshot rendering +/// - Custom targets for specialized use cases +pub trait RenderTarget { + /// Acquire the next frame for rendering. + /// + /// Returns a `Frame` that can be rendered to and then presented. The frame + /// owns the texture view for the duration of rendering. + fn acquire_frame(&mut self) -> Result; + + /// Resize the render target to the specified dimensions. + /// + /// This reconfigures the underlying resources (swapchain, textures) to + /// match the new size. Pass the `Gpu` for resource recreation. + fn resize(&mut self, gpu: &Gpu, size: (u32, u32)) -> Result<(), String>; + + /// Get the texture format used by this render target. + fn format(&self) -> TextureFormat; + + /// Get the current dimensions of the render target. + fn size(&self) -> (u32, u32); + + /// Get the current configuration, if available. + fn configuration(&self) -> Option<&SurfaceConfig>; +} + +// --------------------------------------------------------------------------- +// WindowSurface +// --------------------------------------------------------------------------- + +/// Render target for window-based presentation. +/// +/// Wraps a platform surface bound to a window, providing frame acquisition +/// and presentation through the GPU's swapchain. +pub struct WindowSurface { + inner: platform::surface::Surface<'static>, + config: Option, + size: (u32, u32), +} + +impl WindowSurface { + /// Create a new window surface bound to the given window. + /// + /// The surface must be configured before use by calling + /// `configure_with_defaults` or `resize`. + pub fn new( + instance: &platform::instance::Instance, + window: &Window, + ) -> Result { + let surface = platform::surface::SurfaceBuilder::new() + .with_label("Lambda Window Surface") + .build(instance, window.window_handle()) + .map_err(|_| { + WindowSurfaceError::CreationFailed( + "Failed to create window surface".to_string(), + ) + })?; + + return Ok(WindowSurface { + inner: surface, + config: None, + size: window.dimensions(), + }); + } + + /// Configure the surface with sensible defaults for the given GPU. + /// + /// This selects an sRGB format if available, uses the specified present + /// mode (falling back to Fifo if unsupported), and enables render + /// attachment usage. + pub fn configure_with_defaults( + &mut self, + gpu: &Gpu, + size: (u32, u32), + present_mode: PresentMode, + usage: TextureUsages, + ) -> Result<(), String> { + self + .inner + .configure_with_defaults( + gpu.platform(), + size, + present_mode.to_platform(), + usage.to_platform(), + ) + .map_err(|e| e)?; + + // Cache the configuration + if let Some(platform_config) = self.inner.configuration() { + self.config = Some(SurfaceConfig::from_platform(platform_config)); + } + self.size = size; + + return Ok(()); + } + + /// Borrow the underlying platform surface for internal use. + #[inline] + pub(crate) fn platform(&self) -> &platform::surface::Surface<'static> { + return &self.inner; + } + + /// Mutably borrow the underlying platform surface for internal use. + #[inline] + pub(crate) fn platform_mut( + &mut self, + ) -> &mut platform::surface::Surface<'static> { + return &mut self.inner; + } +} + +impl RenderTarget for WindowSurface { + fn acquire_frame(&mut self) -> Result { + let platform_frame = self + .inner + .acquire_next_frame() + .map_err(SurfaceError::from)?; + return Ok(Frame::from_platform(platform_frame)); + } + + fn resize(&mut self, gpu: &Gpu, size: (u32, u32)) -> Result<(), String> { + self.inner.resize(gpu.platform(), size)?; + + // Update cached configuration + if let Some(platform_config) = self.inner.configuration() { + self.config = Some(SurfaceConfig::from_platform(platform_config)); + } + self.size = size; + + return Ok(()); + } + + fn format(&self) -> TextureFormat { + return self + .config + .as_ref() + .map(|c| c.format) + .unwrap_or(TextureFormat::Bgra8UnormSrgb); + } + + fn size(&self) -> (u32, u32) { + return self.size; + } + + fn configuration(&self) -> Option<&SurfaceConfig> { + return self.config.as_ref(); + } +} + +impl std::fmt::Debug for WindowSurface { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return f + .debug_struct("WindowSurface") + .field("size", &self.size) + .field("config", &self.config) + .finish_non_exhaustive(); + } +} + +// --------------------------------------------------------------------------- +// WindowSurfaceError +// --------------------------------------------------------------------------- + +/// Errors that can occur when creating a window surface. +#[derive(Debug)] +pub enum WindowSurfaceError { + /// Surface creation failed. + CreationFailed(String), +} + +impl std::fmt::Display for WindowSurfaceError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return match self { + WindowSurfaceError::CreationFailed(msg) => write!(f, "{}", msg), + }; + } +} + +impl std::error::Error for WindowSurfaceError {} From 71256389b9efe247a59aabffe9de58147b30669d Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 15 Dec 2025 12:35:03 -0800 Subject: [PATCH 21/28] [update] builders to accept the gpu and other resources directly to prepare for generalizing render contexts. --- .../examples/indexed_multi_vertex_buffers.rs | 16 ++++--- crates/lambda-rs/examples/instanced_quads.rs | 16 ++++--- crates/lambda-rs/examples/push_constants.rs | 17 +++++-- crates/lambda-rs/examples/reflective_room.rs | 36 ++++++++++----- crates/lambda-rs/examples/textured_cube.rs | 27 ++++++++--- crates/lambda-rs/examples/textured_quad.rs | 25 ++++++++--- crates/lambda-rs/examples/triangle.rs | 11 +++-- crates/lambda-rs/examples/triangles.rs | 11 +++-- .../examples/uniform_buffer_triangle.rs | 20 ++++++--- crates/lambda-rs/src/render/bind.rs | 16 +++---- crates/lambda-rs/src/render/buffer.rs | 28 +++++------- crates/lambda-rs/src/render/encoder.rs | 11 +++-- crates/lambda-rs/src/render/mod.rs | 45 ++++++++++--------- crates/lambda-rs/src/render/pipeline.rs | 38 +++++++++------- crates/lambda-rs/src/render/render_pass.rs | 24 +++++++--- crates/lambda-rs/src/render/texture.rs | 23 +++++----- 16 files changed, 232 insertions(+), 132 deletions(-) diff --git a/crates/lambda-rs/examples/indexed_multi_vertex_buffers.rs b/crates/lambda-rs/examples/indexed_multi_vertex_buffers.rs index ba5283a6..c2f47134 100644 --- a/crates/lambda-rs/examples/indexed_multi_vertex_buffers.rs +++ b/crates/lambda-rs/examples/indexed_multi_vertex_buffers.rs @@ -111,7 +111,11 @@ impl Component for IndexedMultiBufferExample { &mut self, render_context: &mut RenderContext, ) -> Result { - let render_pass = RenderPassBuilder::new().build(render_context); + let render_pass = RenderPassBuilder::new().build( + render_context.gpu(), + render_context.surface_format(), + render_context.depth_format(), + ); // Quad composed from two triangles in clip space. let positions: Vec = vec![ @@ -153,7 +157,7 @@ impl Component for IndexedMultiBufferExample { .with_properties(Properties::DEVICE_LOCAL) .with_buffer_type(BufferType::Vertex) .with_label("indexed-positions") - .build(render_context, positions) + .build(render_context.gpu(), positions) .map_err(|e| e.to_string())?; let color_buffer = BufferBuilder::new() @@ -161,7 +165,7 @@ impl Component for IndexedMultiBufferExample { .with_properties(Properties::DEVICE_LOCAL) .with_buffer_type(BufferType::Vertex) .with_label("indexed-colors") - .build(render_context, colors) + .build(render_context.gpu(), colors) .map_err(|e| e.to_string())?; // Build a 16-bit index buffer. @@ -170,7 +174,7 @@ impl Component for IndexedMultiBufferExample { .with_properties(Properties::DEVICE_LOCAL) .with_buffer_type(BufferType::Index) .with_label("indexed-indices") - .build(render_context, indices) + .build(render_context.gpu(), indices) .map_err(|e| e.to_string())?; let pipeline = RenderPipelineBuilder::new() @@ -198,7 +202,9 @@ impl Component for IndexedMultiBufferExample { }], ) .build( - render_context, + render_context.gpu(), + render_context.surface_format(), + render_context.depth_format(), &render_pass, &self.vertex_shader, Some(&self.fragment_shader), diff --git a/crates/lambda-rs/examples/instanced_quads.rs b/crates/lambda-rs/examples/instanced_quads.rs index 50b9455b..fcd49afb 100644 --- a/crates/lambda-rs/examples/instanced_quads.rs +++ b/crates/lambda-rs/examples/instanced_quads.rs @@ -116,7 +116,11 @@ impl Component for InstancedQuadsExample { &mut self, render_context: &mut RenderContext, ) -> Result { - let render_pass = RenderPassBuilder::new().build(render_context); + let render_pass = RenderPassBuilder::new().build( + render_context.gpu(), + render_context.surface_format(), + render_context.depth_format(), + ); // Quad geometry in clip space centered at the origin. let quad_vertices: Vec = vec![ @@ -168,7 +172,7 @@ impl Component for InstancedQuadsExample { .with_properties(Properties::DEVICE_LOCAL) .with_buffer_type(BufferType::Vertex) .with_label("instanced-quads-vertices") - .build(render_context, quad_vertices) + .build(render_context.gpu(), quad_vertices) .map_err(|error| error.to_string())?; let instance_buffer = BufferBuilder::new() @@ -176,7 +180,7 @@ impl Component for InstancedQuadsExample { .with_properties(Properties::DEVICE_LOCAL) .with_buffer_type(BufferType::Vertex) .with_label("instanced-quads-instances") - .build(render_context, instances) + .build(render_context.gpu(), instances) .map_err(|error| error.to_string())?; let index_buffer = BufferBuilder::new() @@ -184,7 +188,7 @@ impl Component for InstancedQuadsExample { .with_properties(Properties::DEVICE_LOCAL) .with_buffer_type(BufferType::Index) .with_label("instanced-quads-indices") - .build(render_context, indices) + .build(render_context.gpu(), indices) .map_err(|error| error.to_string())?; // Vertex attributes for per-vertex positions in slot 0. @@ -222,7 +226,9 @@ impl Component for InstancedQuadsExample { .with_buffer(vertex_buffer, vertex_attributes) .with_instance_buffer(instance_buffer, instance_attributes) .build( - render_context, + render_context.gpu(), + render_context.surface_format(), + render_context.depth_format(), &render_pass, &self.vertex_shader, Some(&self.fragment_shader), diff --git a/crates/lambda-rs/examples/push_constants.rs b/crates/lambda-rs/examples/push_constants.rs index 15ea6712..06869e52 100644 --- a/crates/lambda-rs/examples/push_constants.rs +++ b/crates/lambda-rs/examples/push_constants.rs @@ -126,7 +126,11 @@ impl Component for PushConstantsExample { &mut self, render_context: &mut lambda::render::RenderContext, ) -> Result { - let render_pass = RenderPassBuilder::new().build(render_context); + let render_pass = RenderPassBuilder::new().build( + render_context.gpu(), + render_context.surface_format(), + render_context.depth_format(), + ); let push_constant_size = std::mem::size_of::() as u32; // Create triangle mesh. @@ -188,11 +192,18 @@ impl Component for PushConstantsExample { .with_culling(lambda::render::pipeline::CullingMode::None) .with_push_constant(PipelineStage::VERTEX, push_constant_size) .with_buffer( - BufferBuilder::build_from_mesh(&mesh, render_context) + BufferBuilder::build_from_mesh(&mesh, render_context.gpu()) .expect("Failed to create buffer"), mesh.attributes().to_vec(), ) - .build(render_context, &render_pass, &self.shader, Some(&self.fs)); + .build( + render_context.gpu(), + render_context.surface_format(), + render_context.depth_format(), + &render_pass, + &self.shader, + Some(&self.fs), + ); self.render_pass = Some(render_context.attach_render_pass(render_pass)); self.render_pipeline = Some(render_context.attach_pipeline(pipeline)); diff --git a/crates/lambda-rs/examples/reflective_room.rs b/crates/lambda-rs/examples/reflective_room.rs index c7ac60fa..08c91a2c 100644 --- a/crates/lambda-rs/examples/reflective_room.rs +++ b/crates/lambda-rs/examples/reflective_room.rs @@ -587,7 +587,11 @@ impl ReflectiveRoomExample { .with_stencil_clear(0) .with_multi_sample(self.msaa_samples) .without_color() - .build(render_context), + .build( + render_context.gpu(), + render_context.surface_format(), + render_context.depth_format(), + ), ) } else { None @@ -606,7 +610,11 @@ impl ReflectiveRoomExample { if self.stencil_enabled { rp_color_builder = rp_color_builder.with_stencil_load(); } - let rp_color_desc = rp_color_builder.build(render_context); + let rp_color_desc = rp_color_builder.build( + render_context.gpu(), + render_context.surface_format(), + render_context.depth_format(), + ); // Floor mask pipeline (stencil write) self.pipe_floor_mask = if self.stencil_enabled { @@ -626,7 +634,7 @@ impl ReflectiveRoomExample { .with_usage(Usage::VERTEX) .with_properties(Properties::DEVICE_LOCAL) .with_buffer_type(BufferType::Vertex) - .build(render_context, floor_mesh.vertices().to_vec()) + .build(render_context.gpu(), floor_mesh.vertices().to_vec()) .map_err(|e| format!("Failed to create floor buffer: {}", e))?, floor_mesh.attributes().to_vec(), ) @@ -648,7 +656,9 @@ impl ReflectiveRoomExample { }) .with_multi_sample(self.msaa_samples) .build( - render_context, + render_context.gpu(), + render_context.surface_format(), + render_context.depth_format(), rp_mask_desc .as_ref() .expect("mask pass missing for stencil"), @@ -676,7 +686,7 @@ impl ReflectiveRoomExample { .with_usage(Usage::VERTEX) .with_properties(Properties::DEVICE_LOCAL) .with_buffer_type(BufferType::Vertex) - .build(render_context, cube_mesh.vertices().to_vec()) + .build(render_context.gpu(), cube_mesh.vertices().to_vec()) .map_err(|e| format!("Failed to create cube buffer: {}", e))?, cube_mesh.attributes().to_vec(), ) @@ -702,7 +712,9 @@ impl ReflectiveRoomExample { .with_depth_write(false) .with_depth_compare(CompareFunction::Always); let p = builder.build( - render_context, + render_context.gpu(), + render_context.surface_format(), + render_context.depth_format(), &rp_color_desc, &self.shader_vs, Some(&self.shader_fs_lit), @@ -727,7 +739,7 @@ impl ReflectiveRoomExample { .with_usage(Usage::VERTEX) .with_properties(Properties::DEVICE_LOCAL) .with_buffer_type(BufferType::Vertex) - .build(render_context, floor_mesh.vertices().to_vec()) + .build(render_context.gpu(), floor_mesh.vertices().to_vec()) .map_err(|e| format!("Failed to create floor buffer: {}", e))?, floor_mesh.attributes().to_vec(), ) @@ -743,7 +755,9 @@ impl ReflectiveRoomExample { }); } let floor_pipe = floor_builder.build( - render_context, + render_context.gpu(), + render_context.surface_format(), + render_context.depth_format(), &rp_color_desc, &self.shader_vs, Some(&self.shader_fs_floor), @@ -763,7 +777,7 @@ impl ReflectiveRoomExample { .with_usage(Usage::VERTEX) .with_properties(Properties::DEVICE_LOCAL) .with_buffer_type(BufferType::Vertex) - .build(render_context, cube_mesh.vertices().to_vec()) + .build(render_context.gpu(), cube_mesh.vertices().to_vec()) .map_err(|e| format!("Failed to create cube buffer: {}", e))?, cube_mesh.attributes().to_vec(), ) @@ -779,7 +793,9 @@ impl ReflectiveRoomExample { }); } let normal_pipe = normal_builder.build( - render_context, + render_context.gpu(), + render_context.surface_format(), + render_context.depth_format(), &rp_color_desc, &self.shader_vs, Some(&self.shader_fs_lit), diff --git a/crates/lambda-rs/examples/textured_cube.rs b/crates/lambda-rs/examples/textured_cube.rs index 4e012f56..8255aed9 100644 --- a/crates/lambda-rs/examples/textured_cube.rs +++ b/crates/lambda-rs/examples/textured_cube.rs @@ -172,7 +172,11 @@ impl Component for TexturedCubeExample { let render_pass = RenderPassBuilder::new() .with_label("textured-cube-pass") .with_depth() - .build(render_context); + .build( + render_context.gpu(), + render_context.surface_format(), + render_context.depth_format(), + ); let mut shader_builder = ShaderBuilder::new(); let shader_vs = shader_builder.build(VirtualShader::Source { @@ -310,19 +314,21 @@ impl Component for TexturedCubeExample { .with_size(tex_w, tex_h) .with_data(&pixels) .with_label("checkerboard") - .build(render_context) + .build(render_context.gpu()) .expect("Failed to create 2D texture"); - let sampler = SamplerBuilder::new().linear_clamp().build(render_context); + let sampler = SamplerBuilder::new() + .linear_clamp() + .build(render_context.gpu()); let layout = BindGroupLayoutBuilder::new() .with_sampled_texture(1) .with_sampler(2) - .build(render_context); + .build(render_context.gpu()); let bind_group = BindGroupBuilder::new() .with_layout(&layout) .with_texture(1, &texture2d) .with_sampler(2, &sampler) - .build(render_context); + .build(render_context.gpu()); let push_constants_size = std::mem::size_of::() as u32; let pipeline = RenderPipelineBuilder::new() @@ -330,12 +336,19 @@ impl Component for TexturedCubeExample { .with_depth() .with_push_constant(PipelineStage::VERTEX, push_constants_size) .with_buffer( - BufferBuilder::build_from_mesh(&mesh, render_context) + BufferBuilder::build_from_mesh(&mesh, render_context.gpu()) .expect("Failed to create vertex buffer"), mesh.attributes().to_vec(), ) .with_layouts(&[&layout]) - .build(render_context, &render_pass, &shader_vs, Some(&shader_fs)); + .build( + render_context.gpu(), + render_context.surface_format(), + render_context.depth_format(), + &render_pass, + &shader_vs, + Some(&shader_fs), + ); self.render_pass = Some(render_context.attach_render_pass(render_pass)); self.render_pipeline = Some(render_context.attach_pipeline(pipeline)); diff --git a/crates/lambda-rs/examples/textured_quad.rs b/crates/lambda-rs/examples/textured_quad.rs index e4da291f..86e5f126 100644 --- a/crates/lambda-rs/examples/textured_quad.rs +++ b/crates/lambda-rs/examples/textured_quad.rs @@ -106,7 +106,11 @@ impl Component for TexturedQuadExample { // Build render pass and shaders let render_pass = RenderPassBuilder::new() .with_label("textured-quad-pass") - .build(render_context); + .build( + render_context.gpu(), + render_context.surface_format(), + render_context.depth_format(), + ); let mut shader_builder = ShaderBuilder::new(); let shader_vs = shader_builder.build(VirtualShader::Source { @@ -209,35 +213,42 @@ impl Component for TexturedQuadExample { .with_size(tex_w, tex_h) .with_data(&pixels) .with_label("checkerboard") - .build(render_context) + .build(render_context.gpu()) .expect("Failed to create texture"); let sampler = SamplerBuilder::new() .linear_clamp() .with_label("linear-clamp") - .build(render_context); + .build(render_context.gpu()); // Layout: binding(1) texture2D, binding(2) sampler let layout = BindGroupLayoutBuilder::new() .with_sampled_texture(1) .with_sampler(2) - .build(render_context); + .build(render_context.gpu()); let bind_group = BindGroupBuilder::new() .with_layout(&layout) .with_texture(1, &texture) .with_sampler(2, &sampler) - .build(render_context); + .build(render_context.gpu()); let pipeline = RenderPipelineBuilder::new() .with_culling(CullingMode::None) .with_layouts(&[&layout]) .with_buffer( - BufferBuilder::build_from_mesh(&mesh, render_context) + BufferBuilder::build_from_mesh(&mesh, render_context.gpu()) .expect("Failed to create vertex buffer"), mesh.attributes().to_vec(), ) - .build(render_context, &render_pass, &shader_vs, Some(&shader_fs)); + .build( + render_context.gpu(), + render_context.surface_format(), + render_context.depth_format(), + &render_pass, + &shader_vs, + Some(&shader_fs), + ); self.render_pass = Some(render_context.attach_render_pass(render_pass)); self.render_pipeline = Some(render_context.attach_pipeline(pipeline)); diff --git a/crates/lambda-rs/examples/triangle.rs b/crates/lambda-rs/examples/triangle.rs index 650a26a3..8cb77e1a 100644 --- a/crates/lambda-rs/examples/triangle.rs +++ b/crates/lambda-rs/examples/triangle.rs @@ -42,11 +42,16 @@ impl Component for DemoComponent { render_context: &mut RenderContext, ) -> Result { logging::info!("Attached the demo component to the renderer"); - let render_pass = - render_pass::RenderPassBuilder::new().build(&render_context); + let render_pass = render_pass::RenderPassBuilder::new().build( + render_context.gpu(), + render_context.surface_format(), + render_context.depth_format(), + ); let pipeline = pipeline::RenderPipelineBuilder::new().build( - render_context, + render_context.gpu(), + render_context.surface_format(), + render_context.depth_format(), &render_pass, &self.vertex_shader, Some(&self.fragment_shader), diff --git a/crates/lambda-rs/examples/triangles.rs b/crates/lambda-rs/examples/triangles.rs index 0b4b8e6b..1c88157c 100644 --- a/crates/lambda-rs/examples/triangles.rs +++ b/crates/lambda-rs/examples/triangles.rs @@ -46,14 +46,19 @@ impl Component for TrianglesComponent { &mut self, render_context: &mut RenderContext, ) -> Result { - let render_pass = - render_pass::RenderPassBuilder::new().build(&render_context); + let render_pass = render_pass::RenderPassBuilder::new().build( + render_context.gpu(), + render_context.surface_format(), + render_context.depth_format(), + ); let push_constants_size = std::mem::size_of::() as u32; let pipeline = pipeline::RenderPipelineBuilder::new() .with_push_constant(PipelineStage::VERTEX, push_constants_size) .build( - render_context, + render_context.gpu(), + render_context.surface_format(), + render_context.depth_format(), &render_pass, &self.vertex_shader, Some(&self.triangle_vertex), diff --git a/crates/lambda-rs/examples/uniform_buffer_triangle.rs b/crates/lambda-rs/examples/uniform_buffer_triangle.rs index ab74c6cf..8f9d34b5 100644 --- a/crates/lambda-rs/examples/uniform_buffer_triangle.rs +++ b/crates/lambda-rs/examples/uniform_buffer_triangle.rs @@ -119,7 +119,11 @@ impl Component for UniformBufferExample { &mut self, render_context: &mut lambda::render::RenderContext, ) -> Result { - let render_pass = RenderPassBuilder::new().build(render_context); + let render_pass = RenderPassBuilder::new().build( + render_context.gpu(), + render_context.surface_format(), + render_context.depth_format(), + ); // Create triangle mesh. let vertices = [ @@ -179,7 +183,7 @@ impl Component for UniformBufferExample { // Create a bind group layout with a single uniform buffer at binding 0. let layout = BindGroupLayoutBuilder::new() .with_uniform(0, BindingVisibility::Vertex) - .build(&render_context); + .build(render_context.gpu()); // Create the uniform buffer with an initial matrix. let camera = SimpleCamera { @@ -209,25 +213,27 @@ impl Component for UniformBufferExample { .with_usage(Usage::UNIFORM) .with_properties(Properties::CPU_VISIBLE) .with_label("globals-uniform") - .build(render_context, vec![initial_uniform]) + .build(render_context.gpu(), vec![initial_uniform]) .expect("Failed to create uniform buffer"); // Create the bind group using the layout and uniform buffer. let bind_group = BindGroupBuilder::new() .with_layout(&layout) .with_uniform(0, &uniform_buffer, 0, None) - .build(render_context); + .build(render_context.gpu()); let pipeline = RenderPipelineBuilder::new() .with_culling(lambda::render::pipeline::CullingMode::None) .with_layouts(&[&layout]) .with_buffer( - BufferBuilder::build_from_mesh(&mesh, render_context) + BufferBuilder::build_from_mesh(&mesh, render_context.gpu()) .expect("Failed to create buffer"), mesh.attributes().to_vec(), ) .build( - render_context, + render_context.gpu(), + render_context.surface_format(), + render_context.depth_format(), &render_pass, &self.shader, Some(&self.fragment_shader), @@ -310,7 +316,7 @@ impl Component for UniformBufferExample { let value = GlobalsUniform { render_matrix: render_matrix.transpose(), }; - uniform_buffer.write_value(render_context, 0, &value); + uniform_buffer.write_value(render_context.gpu(), 0, &value); } // Create viewport. diff --git a/crates/lambda-rs/src/render/bind.rs b/crates/lambda-rs/src/render/bind.rs index bc318344..0282331b 100644 --- a/crates/lambda-rs/src/render/bind.rs +++ b/crates/lambda-rs/src/render/bind.rs @@ -22,12 +22,12 @@ use std::rc::Rc; use super::{ buffer::Buffer, + gpu::Gpu, texture::{ Sampler, Texture, ViewDimension, }, - RenderContext, }; /// Visibility of a binding across shader stages (engine‑facing). @@ -200,8 +200,8 @@ impl BindGroupLayoutBuilder { return self; } - /// Build the layout using the `RenderContext` device. - pub fn build(self, render_context: &RenderContext) -> BindGroupLayout { + /// Build the layout using the provided GPU device. + pub fn build(self, gpu: &Gpu) -> BindGroupLayout { let mut builder = lambda_platform::wgpu::bind::BindGroupLayoutBuilder::new(); @@ -272,7 +272,7 @@ impl BindGroupLayoutBuilder { builder = builder.with_sampler(binding, visibility.to_platform()); } - let layout = builder.build(render_context.gpu()); + let layout = builder.build(gpu.platform()); return BindGroupLayout { layout: Rc::new(layout), @@ -350,8 +350,8 @@ impl<'a> BindGroupBuilder<'a> { return self; } - /// Build the bind group on the current device. - pub fn build(self, render_context: &RenderContext) -> BindGroup { + /// Build the bind group on the provided GPU device. + pub fn build(self, gpu: &Gpu) -> BindGroup { let layout = self .layout .expect("BindGroupBuilder requires a layout before build"); @@ -363,7 +363,7 @@ impl<'a> BindGroupBuilder<'a> { platform = platform.with_label(label); } - let max_binding = render_context.limit_max_uniform_buffer_binding_size(); + let max_binding = gpu.limit_max_uniform_buffer_binding_size(); for (binding, buffer, offset, size) in self.entries.into_iter() { if let Some(sz) = size { @@ -397,7 +397,7 @@ impl<'a> BindGroupBuilder<'a> { platform = platform.with_sampler(*binding, sampler_handle.as_ref()); } - let group = platform.build(render_context.gpu()); + let group = platform.build(gpu.platform()); 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 6b996ae7..770a8b6b 100644 --- a/crates/lambda-rs/src/render/buffer.rs +++ b/crates/lambda-rs/src/render/buffer.rs @@ -23,6 +23,7 @@ use std::rc::Rc; use lambda_platform::wgpu::buffer as platform_buffer; use super::{ + gpu::Gpu, mesh::Mesh, vertex::Vertex, RenderContext, @@ -141,12 +142,7 @@ impl Buffer { /// Write a single plain-old-data value into this buffer at the specified /// byte offset. This is intended for updating uniform buffer contents from /// the CPU. The `data` type must be trivially copyable. - pub fn write_value( - &self, - render_context: &RenderContext, - offset: u64, - data: &T, - ) { + pub fn write_value(&self, gpu: &Gpu, offset: u64, data: &T) { let bytes = unsafe { std::slice::from_raw_parts( (data as *const T) as *const u8, @@ -154,7 +150,7 @@ impl Buffer { ) }; - self.buffer.write_bytes(render_context.gpu(), offset, bytes); + self.buffer.write_bytes(gpu.platform(), offset, bytes); } } @@ -184,7 +180,7 @@ pub struct UniformBuffer { impl UniformBuffer { /// Create a new uniform buffer initialized with `initial`. pub fn new( - render_context: &mut RenderContext, + gpu: &Gpu, initial: &T, label: Option<&str>, ) -> Result { @@ -197,7 +193,7 @@ impl UniformBuffer { builder = builder.with_label(l); } - let inner = builder.build(render_context, vec![*initial])?; + let inner = builder.build(gpu, vec![*initial])?; return Ok(Self { inner, _phantom: core::marker::PhantomData, @@ -210,8 +206,8 @@ impl UniformBuffer { } /// Write a new value to the GPU buffer at offset 0. - pub fn write(&self, render_context: &RenderContext, value: &T) { - self.inner.write_value(render_context, 0, value); + pub fn write(&self, gpu: &Gpu, value: &T) { + self.inner.write_value(gpu, 0, value); } } @@ -288,7 +284,7 @@ impl BufferBuilder { /// Returns an error if the resolved length would be zero. pub fn build( &self, - render_context: &mut RenderContext, + gpu: &Gpu, data: Vec, ) -> Result { let element_size = std::mem::size_of::(); @@ -312,7 +308,7 @@ impl BufferBuilder { builder = builder.with_label(label); } - let buffer = builder.build_init(render_context.gpu(), bytes); + let buffer = builder.build_init(gpu.platform(), bytes); return Ok(Buffer { buffer: Rc::new(buffer), @@ -324,15 +320,15 @@ impl BufferBuilder { /// Convenience: create a vertex buffer from a `Mesh`'s vertices. pub fn build_from_mesh( mesh: &Mesh, - render_context: &mut RenderContext, + gpu: &Gpu, ) -> Result { - let mut builder = Self::new(); + let builder = Self::new(); return builder .with_length(mesh.vertices().len() * std::mem::size_of::()) .with_usage(Usage::VERTEX) .with_properties(Properties::CPU_VISIBLE) .with_buffer_type(BufferType::Vertex) - .build(render_context, mesh.vertices().to_vec()); + .build(gpu, mesh.vertices().to_vec()); } } diff --git a/crates/lambda-rs/src/render/encoder.rs b/crates/lambda-rs/src/render/encoder.rs index 8d9ad566..75df7a16 100644 --- a/crates/lambda-rs/src/render/encoder.rs +++ b/crates/lambda-rs/src/render/encoder.rs @@ -70,8 +70,10 @@ impl CommandEncoder { /// The encoder is tied to the current frame and should not be reused across /// frames. pub fn new(render_context: &RenderContext, label: &str) -> Self { - let inner = - platform::command::CommandEncoder::new(render_context.gpu(), Some(label)); + let inner = platform::command::CommandEncoder::new( + render_context.gpu().platform(), + Some(label), + ); return CommandEncoder { inner }; } @@ -121,7 +123,10 @@ impl CommandEncoder { /// execution. pub fn finish(self, render_context: &RenderContext) { let buffer = self.inner.finish(); - render_context.gpu().submit(std::iter::once(buffer)); + render_context + .gpu() + .platform() + .submit(std::iter::once(buffer)); } } diff --git a/crates/lambda-rs/src/render/mod.rs b/crates/lambda-rs/src/render/mod.rs index 58a56372..625c2538 100644 --- a/crates/lambda-rs/src/render/mod.rs +++ b/crates/lambda-rs/src/render/mod.rs @@ -131,7 +131,7 @@ impl RenderContextBuilder { )) })?; - let gpu = platform::gpu::GpuBuilder::new() + let gpu = gpu::GpuBuilder::new() .with_label(&format!("{} Device", name)) .build(&instance, Some(&surface)) .map_err(|e| { @@ -144,7 +144,7 @@ impl RenderContextBuilder { let size = window.dimensions(); surface .configure_with_defaults( - &gpu, + gpu.platform(), size, surface::PresentMode::default().to_platform(), texture::TextureUsages::RENDER_ATTACHMENT.to_platform(), @@ -210,7 +210,7 @@ pub struct RenderContext { label: String, instance: platform::instance::Instance, surface: platform::surface::Surface<'static>, - gpu: platform::gpu::Gpu, + gpu: gpu::Gpu, config: surface::SurfaceConfig, texture_usage: texture::TextureUsages, size: (u32, u32), @@ -319,7 +319,7 @@ impl RenderContext { .with_format(self.depth_format) .with_sample_count(self.depth_sample_count) .with_label("lambda-depth") - .build(self), + .build(&self.gpu), ); // Drop MSAA color target so it is rebuilt on demand with the new size. self.msaa_color = None; @@ -339,15 +339,21 @@ impl RenderContext { return &self.render_pipelines[id]; } - pub(crate) fn gpu(&self) -> &platform::gpu::Gpu { + /// Access the GPU device for resource creation. + /// + /// Use this to pass to resource builders (buffers, textures, bind groups, + /// etc.) when creating GPU resources. + pub fn gpu(&self) -> &gpu::Gpu { return &self.gpu; } - pub(crate) fn surface_format(&self) -> texture::TextureFormat { + /// The texture format of the render surface. + pub fn surface_format(&self) -> texture::TextureFormat { return self.config.format; } - pub(crate) fn depth_format(&self) -> texture::DepthFormat { + /// The depth texture format used for depth/stencil operations. + pub fn depth_format(&self) -> texture::DepthFormat { return self.depth_format; } @@ -355,10 +361,9 @@ impl RenderContext { &self, sample_count: u32, ) -> bool { - return self.gpu.supports_sample_count_for_format( - self.config.format.to_platform(), - sample_count, - ); + return self + .gpu + .supports_sample_count_for_format(self.config.format, sample_count); } pub(crate) fn supports_depth_sample_count( @@ -368,32 +373,32 @@ impl RenderContext { ) -> bool { return self .gpu - .supports_sample_count_for_depth(format.to_platform(), sample_count); + .supports_sample_count_for_depth(format, sample_count); } /// 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; + return self.gpu.limit_max_uniform_buffer_binding_size(); } /// Device limit: number of bind groups that can be used by a pipeline layout. pub fn limit_max_bind_groups(&self) -> u32 { - return self.gpu.limits().max_bind_groups; + return self.gpu.limit_max_bind_groups(); } /// Device limit: maximum number of vertex buffers that can be bound. pub fn limit_max_vertex_buffers(&self) -> u32 { - return self.gpu.limits().max_vertex_buffers; + return self.gpu.limit_max_vertex_buffers(); } /// Device limit: maximum number of vertex attributes that can be declared. pub fn limit_max_vertex_attributes(&self) -> u32 { - return self.gpu.limits().max_vertex_attributes; + return self.gpu.limit_max_vertex_attributes(); } /// Device limit: required alignment in bytes for dynamic uniform buffer offsets. pub fn limit_min_uniform_buffer_offset_alignment(&self) -> u32 { - return self.gpu.limits().min_uniform_buffer_offset_alignment; + return self.gpu.limit_min_uniform_buffer_offset_alignment(); } /// Ensure the MSAA color attachment texture exists with the given sample @@ -416,7 +421,7 @@ impl RenderContext { .with_size(self.size.0.max(1), self.size.1.max(1)) .with_sample_count(sample_count) .with_label("lambda-msaa-color") - .build(self), + .build(&self.gpu), ); self.msaa_sample_count = sample_count; } @@ -544,7 +549,7 @@ impl RenderContext { .with_format(self.depth_format) .with_sample_count(desired_samples) .with_label("lambda-depth") - .build(self), + .build(&self.gpu), ); self.depth_sample_count = desired_samples; } @@ -708,7 +713,7 @@ impl RenderContext { ) -> Result<(), RenderError> { self .surface - .resize(&self.gpu, size) + .resize(self.gpu.platform(), size) .map_err(RenderError::Configuration)?; let platform_config = self.surface.configuration().ok_or_else(|| { diff --git a/crates/lambda-rs/src/render/pipeline.rs b/crates/lambda-rs/src/render/pipeline.rs index 8e289649..bff32e07 100644 --- a/crates/lambda-rs/src/render/pipeline.rs +++ b/crates/lambda-rs/src/render/pipeline.rs @@ -39,6 +39,7 @@ use super::{ Buffer, BufferType, }, + gpu::Gpu, render_pass::RenderPass, shader::Shader, texture, @@ -422,24 +423,32 @@ impl RenderPipelineBuilder { /// Build a graphics pipeline using the provided shader modules and /// previously registered vertex inputs and push constants. + /// + /// # Arguments + /// * `gpu` - The GPU device to create the pipeline on. + /// * `surface_format` - The texture format of the render target surface. + /// * `depth_format` - The depth format for depth/stencil operations. + /// * `render_pass` - The render pass this pipeline will be used with. + /// * `vertex_shader` - The vertex shader module. + /// * `fragment_shader` - Optional fragment shader module. pub fn build( self, - render_context: &mut RenderContext, - _render_pass: &RenderPass, + gpu: &Gpu, + surface_format: texture::TextureFormat, + depth_format: texture::DepthFormat, + render_pass: &RenderPass, vertex_shader: &Shader, fragment_shader: Option<&Shader>, ) -> RenderPipeline { - let surface_format = render_context.surface_format(); - // Shader modules let vertex_module = platform_pipeline::ShaderModule::from_spirv( - render_context.gpu(), + gpu.platform(), vertex_shader.binary(), Some("lambda-vertex-shader"), ); let fragment_module = fragment_shader.map(|shader| { platform_pipeline::ShaderModule::from_spirv( - render_context.gpu(), + gpu.platform(), shader.binary(), Some("lambda-fragment-shader"), ) @@ -456,7 +465,7 @@ impl RenderPipelineBuilder { .collect(); // Bind group layouts limit check - let max_bind_groups = render_context.limit_max_bind_groups() as usize; + let max_bind_groups = gpu.limit_max_bind_groups() as usize; if self.bind_group_layouts.len() > max_bind_groups { logging::error!( "Pipeline declares {} bind group layouts, exceeds device max {}", @@ -472,7 +481,7 @@ impl RenderPipelineBuilder { ); // Vertex buffer slot and attribute count limit checks. - let max_vertex_buffers = render_context.limit_max_vertex_buffers() as usize; + let max_vertex_buffers = gpu.limit_max_vertex_buffers() as usize; if self.bindings.len() > max_vertex_buffers { logging::error!( "Pipeline declares {} vertex buffers, exceeds device max {}", @@ -492,8 +501,7 @@ impl RenderPipelineBuilder { .iter() .map(|binding| binding.attributes.len()) .sum(); - let max_vertex_attributes = - render_context.limit_max_vertex_attributes() as usize; + let max_vertex_attributes = gpu.limit_max_vertex_attributes() as usize; if total_vertex_attributes > max_vertex_attributes { logging::error!( "Pipeline declares {} vertex attributes across all vertex buffers, exceeds device max {}", @@ -518,7 +526,7 @@ impl RenderPipelineBuilder { .with_label("lambda-pipeline-layout") .with_layouts(&bgl_platform) .with_push_constants(push_constant_ranges) - .build(render_context.gpu()); + .build(gpu.platform()); // Vertex buffers and attributes let mut buffers = Vec::with_capacity(self.bindings.len()); @@ -575,11 +583,11 @@ impl RenderPipelineBuilder { let requested_depth_format = dfmt.to_platform(); // Derive the pass attachment depth format from pass configuration. - let pass_has_stencil = _render_pass.stencil_operations().is_some(); + let pass_has_stencil = render_pass.stencil_operations().is_some(); let pass_depth_format = if pass_has_stencil { texture::DepthFormat::Depth24PlusStencil8 } else { - render_context.depth_format() + depth_format }; // Align the pipeline depth format with the pass attachment format to @@ -618,7 +626,7 @@ impl RenderPipelineBuilder { // Apply multi-sampling to the pipeline. // Always align to the pass sample count; gate logs. let mut pipeline_samples = self.sample_count; - let pass_samples = _render_pass.sample_count(); + let pass_samples = render_pass.sample_count(); if pipeline_samples != pass_samples { #[cfg(any(debug_assertions, feature = "render-validation-msaa",))] logging::error!( @@ -638,7 +646,7 @@ impl RenderPipelineBuilder { rp_builder = rp_builder.with_sample_count(pipeline_samples); let pipeline = rp_builder.build( - render_context.gpu(), + gpu.platform(), &vertex_module, fragment_module.as_ref(), ); diff --git a/crates/lambda-rs/src/render/render_pass.rs b/crates/lambda-rs/src/render/render_pass.rs index 158069d1..d3ff7815 100644 --- a/crates/lambda-rs/src/render/render_pass.rs +++ b/crates/lambda-rs/src/render/render_pass.rs @@ -9,8 +9,8 @@ use lambda_platform::wgpu as platform; use logging; use super::{ + gpu::Gpu, texture, - RenderContext, }; use crate::render::validation; @@ -146,7 +146,7 @@ pub struct RenderPass { impl RenderPass { /// Destroy the pass. Kept for symmetry with other resources. - pub fn destroy(self, _render_context: &RenderContext) {} + pub fn destroy(self, _gpu: &Gpu) {} pub(crate) fn clear_color(&self) -> [f64; 4] { return self.clear_color; @@ -336,13 +336,23 @@ impl RenderPassBuilder { } /// Build the description used when beginning a render pass. - pub fn build(self, render_context: &RenderContext) -> RenderPass { + /// + /// # Arguments + /// * `gpu` - The GPU device for sample count validation. + /// * `surface_format` - The surface texture format for sample count validation. + /// * `depth_format` - The depth texture format for sample count validation. + pub fn build( + self, + gpu: &Gpu, + surface_format: texture::TextureFormat, + depth_format: texture::DepthFormat, + ) -> RenderPass { let sample_count = self.resolve_sample_count( self.sample_count, - render_context.surface_format().to_platform(), - render_context.depth_format(), - |count| render_context.supports_surface_sample_count(count), - |format, count| render_context.supports_depth_sample_count(format, count), + surface_format.to_platform(), + depth_format, + |count| gpu.supports_sample_count_for_format(surface_format, count), + |format, count| gpu.supports_sample_count_for_depth(format, count), ); return RenderPass { diff --git a/crates/lambda-rs/src/render/texture.rs b/crates/lambda-rs/src/render/texture.rs index 15626ab0..dd97694c 100644 --- a/crates/lambda-rs/src/render/texture.rs +++ b/crates/lambda-rs/src/render/texture.rs @@ -7,7 +7,7 @@ use std::rc::Rc; use lambda_platform::wgpu::texture as platform; -use super::RenderContext; +use super::gpu::Gpu; #[derive(Debug, Clone, Copy, PartialEq, Eq)] /// Engine-level depth texture formats. @@ -262,7 +262,7 @@ impl ColorAttachmentTextureBuilder { } /// Create the color attachment texture on the device. - pub fn build(self, render_context: &RenderContext) -> ColorAttachmentTexture { + pub fn build(self, gpu: &Gpu) -> ColorAttachmentTexture { let mut builder = platform::ColorAttachmentTextureBuilder::new(self.format.to_platform()) .with_size(self.width, self.height) @@ -272,7 +272,7 @@ impl ColorAttachmentTextureBuilder { builder = builder.with_label(label); } - let texture = builder.build(render_context.gpu()); + let texture = builder.build(gpu.platform()); return ColorAttachmentTexture::from_platform(texture); } } @@ -372,7 +372,7 @@ impl DepthTextureBuilder { } /// Create the depth texture on the device. - pub fn build(self, render_context: &RenderContext) -> DepthTexture { + pub fn build(self, gpu: &Gpu) -> DepthTexture { let mut builder = platform::DepthTextureBuilder::new() .with_size(self.width, self.height) .with_format(self.format.to_platform()) @@ -382,7 +382,7 @@ impl DepthTextureBuilder { builder = builder.with_label(label); } - let texture = builder.build(render_context.gpu()); + let texture = builder.build(gpu.platform()); return DepthTexture::from_platform(texture); } } @@ -484,10 +484,7 @@ impl TextureBuilder { } /// Create the texture and upload initial data if provided. - pub fn build( - self, - render_context: &mut RenderContext, - ) -> Result { + pub fn build(self, gpu: &Gpu) -> Result { let mut builder = if self.depth <= 1 { platform::TextureBuilder::new_2d(self.format.to_platform()) @@ -505,7 +502,7 @@ impl TextureBuilder { builder = builder.with_data(pixels); } - return match builder.build(render_context.gpu()) { + return match builder.build(gpu.platform()) { Ok(texture) => Ok(Texture { inner: Rc::new(texture), }), @@ -598,9 +595,9 @@ impl SamplerBuilder { return self; } - /// Create the sampler on the current device. - pub fn build(self, render_context: &mut RenderContext) -> Sampler { - let sampler = self.inner.build(render_context.gpu()); + /// Create the sampler on the provided GPU device. + pub fn build(self, gpu: &Gpu) -> Sampler { + let sampler = self.inner.build(gpu.platform()); return Sampler { inner: Rc::new(sampler), }; From d401b8bf65f7c9dfecf9e1591b3691b124697130 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 15 Dec 2025 13:15:38 -0800 Subject: [PATCH 22/28] [update] documentation to reflect new changes to the render context. --- docs/feature_roadmap_and_snippets.md | 70 +++++++++++++++---- docs/game_roadmap_and_prototype.md | 60 +++++++++++----- docs/rendering.md | 24 ++++++- docs/specs/depth-stencil-msaa.md | 22 ++++-- ...dexed-draws-and-multiple-vertex-buffers.md | 22 ++++-- docs/specs/textures-and-samplers.md | 23 +++--- docs/specs/uniform-buffers-and-bind-groups.md | 24 ++++--- ...dexed-draws-and-multiple-vertex-buffers.md | 23 +++--- docs/tutorials/instanced-quads.md | 23 +++--- docs/tutorials/reflective-room.md | 55 ++++++++++++--- docs/tutorials/textured-cube.md | 32 ++++++--- docs/tutorials/textured-quad.md | 32 ++++++--- docs/tutorials/uniform-buffers.md | 51 +++++++++++--- 13 files changed, 341 insertions(+), 120 deletions(-) diff --git a/docs/feature_roadmap_and_snippets.md b/docs/feature_roadmap_and_snippets.md index d2e5b7a0..78f82a82 100644 --- a/docs/feature_roadmap_and_snippets.md +++ b/docs/feature_roadmap_and_snippets.md @@ -1,3 +1,20 @@ +--- +title: "Lambda RS: Immediate Feature Ideas and Example APIs" +document_id: "feature-roadmap-snippets-2025-09-24" +status: "living" +created: "2025-09-24T00:00:00Z" +last_updated: "2025-12-15T00:00:00Z" +version: "0.2.0" +engine_workspace_version: "2023.1.30" +wgpu_version: "26.0.1" +shader_backend_default: "naga" +winit_version: "0.29.10" +repo_commit: "71256389b9efe247a59aabffe9de58147b30669d" +owners: ["lambda-sh"] +reviewers: ["engine", "rendering"] +tags: ["roadmap", "features", "api-design", "rendering"] +--- + # Lambda RS: Immediate Feature Ideas + Example APIs This document proposes high‑impact features to add next to the Lambda RS @@ -23,7 +40,7 @@ use lambda::render::{ // Layout: set(0) has a uniform buffer at binding(0) let layout = BindGroupLayoutBuilder::new() .with_uniform(binding = 0, visibility = PipelineStage::VERTEX) - .build(&mut rc); + .build(rc.gpu()); // Create and upload a uniform buffer let ubo = BufferBuilder::new() @@ -31,19 +48,26 @@ let ubo = BufferBuilder::new() .with_usage(Usage::UNIFORM) .with_properties(Properties::CPU_VISIBLE) .with_label("globals") - .build(&mut rc, vec![initial_globals])?; + .build(rc.gpu(), vec![initial_globals])?; // Bind group that points the layout(0)@binding(0) to our UBO let group0 = BindGroupBuilder::new() .with_layout(&layout) .with_uniform(binding = 0, &ubo) - .build(&mut rc); + .build(rc.gpu()); // Pipeline accepts optional bind group layouts let pipe = RenderPipelineBuilder::new() .with_layouts(&[&layout]) .with_buffer(vbo, attributes) - .build(&mut rc, &pass, &vs, Some(&fs)); + .build( + rc.gpu(), + rc.surface_format(), + rc.depth_format(), + &pass, + &vs, + Some(&fs), + ); // Commands inside a render pass RC::SetPipeline { pipeline: pipe_id }, @@ -64,25 +88,25 @@ let texture = TextureBuilder::new_2d(TextureFormat::Rgba8UnormSrgb) .with_size(512, 512) .with_data(&pixels) .with_label("albedo") - .build(&mut rc); + .build(rc.gpu()); let sampler = SamplerBuilder::new() .linear_clamp() - .build(&mut rc); + .build(rc.gpu()); // Layout: binding(0) uniform buffer, binding(1) sampled texture, binding(2) sampler let layout = BindGroupLayoutBuilder::new() .with_uniform(0, PipelineStage::VERTEX | PipelineStage::FRAGMENT) .with_sampled_texture(1) .with_sampler(2) - .build(&mut rc); + .build(rc.gpu()); let group = BindGroupBuilder::new() .with_layout(&layout) .with_uniform(0, &ubo) .with_texture(1, &texture) .with_sampler(2, &sampler) - .build(&mut rc); + .build(rc.gpu()); RC::BindGroup { set: 0, group: group_id, offsets: &[] }, ``` @@ -105,12 +129,23 @@ let pass = RenderPassBuilder::new() depth_compare = wgpu::CompareFunction::Less, ) .with_msaa(samples = 4) - .build(&rc); + .build( + rc.gpu(), + rc.surface_format(), + rc.depth_format(), + ); let pipe = RenderPipelineBuilder::new() .with_msaa(samples = 4) .with_depth_format(wgpu::TextureFormat::Depth32Float) - .build(&mut rc, &pass, &vs, Some(&fs)); + .build( + rc.gpu(), + rc.surface_format(), + rc.depth_format(), + &pass, + &vs, + Some(&fs), + ); ``` ## 4) Indexed Draw + Multiple Vertex Buffers @@ -140,13 +175,21 @@ let offscreen = RenderTargetBuilder::new() .with_color(TextureFormat::Rgba8UnormSrgb, width, height) .with_depth(TextureFormat::Depth32Float) .with_label("offscreen") - .build(&mut rc); + .build(rc.gpu()); // Pass 1: draw scene into `offscreen` -let p1 = RenderPassBuilder::new().with_target(&offscreen).build(&rc); +let p1 = RenderPassBuilder::new().with_target(&offscreen).build( + rc.gpu(), + rc.surface_format(), + rc.depth_format(), +); // Pass 2: sample offscreen color into swapchain -let p2 = RenderPassBuilder::new().build(&rc); +let p2 = RenderPassBuilder::new().build( + rc.gpu(), + rc.surface_format(), + rc.depth_format(), +); // Commands RC::BeginRenderPass { render_pass: p1_id, viewport }, @@ -246,4 +289,3 @@ These proposals aim to keep Lambda’s surface area small while unlocking common workflows (texturing, uniforms, depth/MSAA, compute, multipass). I can begin implementing any of them next; uniform buffers/bind groups and depth/MSAA are usually the quickest wins for examples and demos. - diff --git a/docs/game_roadmap_and_prototype.md b/docs/game_roadmap_and_prototype.md index 769334d9..0bdcf6f3 100644 --- a/docs/game_roadmap_and_prototype.md +++ b/docs/game_roadmap_and_prototype.md @@ -3,13 +3,13 @@ title: "Lambda RS: Gaps, Roadmap, and Prototype Plan" document_id: "game-roadmap-2025-09-24" status: "living" created: "2025-09-24T05:09:25Z" -last_updated: "2025-09-26T19:37:55Z" -version: "0.2.0" +last_updated: "2025-12-15T00:00:00Z" +version: "0.3.0" engine_workspace_version: "2023.1.30" wgpu_version: "26.0.1" shader_backend_default: "naga" winit_version: "0.29.10" -repo_commit: "2e7a3abcf60a780fa6bf089ca8a6f4124e60f660" +repo_commit: "71256389b9efe247a59aabffe9de58147b30669d" owners: ["lambda-sh"] reviewers: ["engine", "rendering"] tags: ["roadmap","games","2d","3d","desktop"] @@ -53,22 +53,29 @@ Bind groups and uniforms (value: larger, structured GPU data; portable across ad // Layout with one uniform buffer at set(0) binding(0) let layout = BindGroupLayoutBuilder::new() .with_uniform(0, PipelineStage::VERTEX) - .build(&mut rc); + .build(rc.gpu()); let ubo = BufferBuilder::new() .with_length(std::mem::size_of::()) .with_usage(Usage::UNIFORM) .with_properties(Properties::CPU_VISIBLE) - .build(&mut rc, vec![initial_globals])?; + .build(rc.gpu(), vec![initial_globals])?; let group = BindGroupBuilder::new(&layout) .with_uniform(0, &ubo) - .build(&mut rc); + .build(rc.gpu()); let pipe = RenderPipelineBuilder::new() .with_layouts(&[&layout]) .with_buffer(vbo, attrs) - .build(&mut rc, &pass, &vs, Some(&fs)); + .build( + rc.gpu(), + rc.surface_format(), + rc.depth_format(), + &pass, + &vs, + Some(&fs), + ); // Commands inside a pass RC::SetPipeline { pipeline: pipe_id }; @@ -85,17 +92,17 @@ Textures and samplers (value: sprites, materials, UI images; sRGB correctness): let tex = TextureBuilder::new_2d(TextureFormat::Rgba8UnormSrgb) .with_size(w, h) .with_data(&pixels) - .build(&mut rc); -let samp = SamplerBuilder::linear_clamp().build(&mut rc); + .build(rc.gpu()); +let samp = SamplerBuilder::linear_clamp().build(rc.gpu()); let tex_layout = BindGroupLayoutBuilder::new() .with_sampled_texture(0) .with_sampler(1) - .build(&mut rc); + .build(rc.gpu()); let tex_group = BindGroupBuilder::new(&tex_layout) .with_texture(0, &tex) .with_sampler(1, &samp) - .build(&mut rc); + .build(rc.gpu()); // In fragment shader, sample with: sampler2D + UVs; ensure vertex inputs provide UVs. // Upload path should convert source assets to sRGB formats when appropriate. @@ -128,11 +135,22 @@ let pass = RenderPassBuilder::new() .with_clear_color(wgpu::Color::BLACK) .with_depth_stencil(wgpu::TextureFormat::Depth32Float, 1.0, true, wgpu::CompareFunction::Less) .with_msaa(4) - .build(&rc); + .build( + rc.gpu(), + rc.surface_format(), + rc.depth_format(), + ); let pipe = RenderPipelineBuilder::new() .with_depth_format(wgpu::TextureFormat::Depth32Float) - .build(&mut rc, &pass, &vs, Some(&fs)); + .build( + rc.gpu(), + rc.surface_format(), + rc.depth_format(), + &pass, + &vs, + Some(&fs), + ); ``` Notes @@ -144,10 +162,18 @@ Offscreen render targets (value: post‑processing, shadow maps, UI composition, let offscreen = RenderTargetBuilder::new() .with_color(TextureFormat::Rgba8UnormSrgb, width, height) .with_depth(TextureFormat::Depth32Float) - .build(&mut rc); - -let pass1 = RenderPassBuilder::new().with_target(&offscreen).build(&rc); -let pass2 = RenderPassBuilder::new().build(&rc); // backbuffer + .build(rc.gpu()); + +let pass1 = RenderPassBuilder::new().with_target(&offscreen).build( + rc.gpu(), + rc.surface_format(), + rc.depth_format(), +); +let pass2 = RenderPassBuilder::new().build( + rc.gpu(), + rc.surface_format(), + rc.depth_format(), +); // backbuffer // Pass 1: draw scene RC::BeginRenderPass { render_pass: pass1_id, viewport }; diff --git a/docs/rendering.md b/docs/rendering.md index 9bc0644a..79445a13 100644 --- a/docs/rendering.md +++ b/docs/rendering.md @@ -1,3 +1,20 @@ +--- +title: "Lambda RS Rendering Guide" +document_id: "rendering-guide-2025-09-24" +status: "living" +created: "2025-09-24T00:00:00Z" +last_updated: "2025-12-15T00:00:00Z" +version: "0.2.0" +engine_workspace_version: "2023.1.30" +wgpu_version: "26.0.1" +shader_backend_default: "naga" +winit_version: "0.29.10" +repo_commit: "71256389b9efe247a59aabffe9de58147b30669d" +owners: ["lambda-sh"] +reviewers: ["engine", "rendering"] +tags: ["guide", "rendering", "wgpu", "shaders", "pipelines"] +--- + # Lambda RS Rendering Guide (wgpu backend) This guide shows how to build windows, compile shaders, create pipelines, @@ -117,7 +134,11 @@ use lambda_platform::wgpu::types as wgpu; let pass = RenderPassBuilder::new() .with_clear_color(wgpu::Color { r: 0.02, g: 0.02, b: 0.06, a: 1.0 }) - .build(&render_context); + .build( + render_context.gpu(), + render_context.surface_format(), + render_context.depth_format(), + ); ``` ## Vertex Data: Mesh and Buffer @@ -212,4 +233,3 @@ if let Events::Window { event: WindowEvent::Resize { width, height }, .. } = e { the feature flag `with-shaderc`. - All examples live under `crates/lambda-rs/examples/` and are runnable via: `cargo run -p lambda-rs --example ` - diff --git a/docs/specs/depth-stencil-msaa.md b/docs/specs/depth-stencil-msaa.md index 99be7d10..169124ff 100644 --- a/docs/specs/depth-stencil-msaa.md +++ b/docs/specs/depth-stencil-msaa.md @@ -3,13 +3,13 @@ title: "Depth/Stencil and Multi-Sample Rendering" document_id: "depth-stencil-msaa-2025-11-11" status: "draft" created: "2025-11-11T00:00:00Z" -last_updated: "2025-11-21T22:00:00Z" -version: "0.4.1" +last_updated: "2025-12-15T00:00:00Z" +version: "0.5.0" engine_workspace_version: "2023.1.30" wgpu_version: "26.0.1" shader_backend_default: "naga" winit_version: "0.29.10" -repo_commit: "415167f4238c21debb385eef1192e2da7476c586" +repo_commit: "71256389b9efe247a59aabffe9de58147b30669d" owners: ["lambda-sh"] reviewers: ["engine", "rendering"] tags: ["spec", "rendering", "depth", "stencil", "msaa"] @@ -94,13 +94,24 @@ App Code .with_clear_color([0.0, 0.0, 0.0, 1.0]) .with_depth_clear(1.0) .with_multi_sample(4) - .build(&render_context); + .build( + render_context.gpu(), + render_context.surface_format(), + render_context.depth_format(), + ); let pipeline = RenderPipelineBuilder::new() .with_multi_sample(4) .with_depth_format(DepthFormat::Depth32Float) .with_depth_compare(CompareFunction::Less) - .build(&mut render_context, &pass, &vertex_shader, Some(&fragment_shader)); + .build( + render_context.gpu(), + render_context.surface_format(), + render_context.depth_format(), + &pass, + &vertex_shader, + Some(&fragment_shader), + ); ``` - Behavior - Defaults @@ -243,6 +254,7 @@ Always-on safeguards (release and debug) defaults (no depth, no multi-sampling) unless explicitly configured. ## Changelog +- 2025-12-15 (v0.5.0) — Update example code to use `render_context.gpu()` and add `surface_format`/`depth_format` parameters to `RenderPassBuilder` and `RenderPipelineBuilder`. - 2025-11-21 (v0.4.1) — Clarify depth attachment and clear behavior for stencil-only passes; align specification with engine behavior that preserves depth when only stencil operations are configured. diff --git a/docs/specs/indexed-draws-and-multiple-vertex-buffers.md b/docs/specs/indexed-draws-and-multiple-vertex-buffers.md index 0f14c382..c880cc11 100644 --- a/docs/specs/indexed-draws-and-multiple-vertex-buffers.md +++ b/docs/specs/indexed-draws-and-multiple-vertex-buffers.md @@ -3,13 +3,13 @@ title: "Indexed Draws and Multiple Vertex Buffers" document_id: "indexed-draws-multiple-vertex-buffers-2025-11-22" status: "draft" created: "2025-11-22T00:00:00Z" -last_updated: "2025-11-23T00:00:00Z" -version: "0.1.2" +last_updated: "2025-12-15T00:00:00Z" +version: "0.2.0" engine_workspace_version: "2023.1.30" wgpu_version: "26.0.1" shader_backend_default: "naga" winit_version: "0.29.10" -repo_commit: "db7fa78d143e5ff69028413fe86c948be9ba76ee" +repo_commit: "71256389b9efe247a59aabffe9de58147b30669d" owners: ["lambda-sh"] reviewers: ["engine", "rendering"] tags: ["spec", "rendering", "vertex-input", "indexed-draws"] @@ -134,24 +134,31 @@ let vertex_buffer_positions = BufferBuilder::new() .with_usage(Usage::VERTEX) .with_buffer_type(BufferType::Vertex) .with_label("positions") - .build(&mut render_context, position_vertices)?; + .build(render_context.gpu(), position_vertices)?; let vertex_buffer_colors = BufferBuilder::new() .with_usage(Usage::VERTEX) .with_buffer_type(BufferType::Vertex) .with_label("colors") - .build(&mut render_context, color_vertices)?; + .build(render_context.gpu(), color_vertices)?; let index_buffer = BufferBuilder::new() .with_usage(Usage::INDEX) .with_buffer_type(BufferType::Index) .with_label("indices") - .build(&mut render_context, indices)?; + .build(render_context.gpu(), indices)?; let pipeline = RenderPipelineBuilder::new() .with_buffer(vertex_buffer_positions, position_attributes) .with_buffer(vertex_buffer_colors, color_attributes) - .build(&mut render_context, &render_pass, &vertex_shader, Some(&fragment_shader)); + .build( + render_context.gpu(), + render_context.surface_format(), + render_context.depth_format(), + &render_pass, + &vertex_shader, + Some(&fragment_shader), + ); let commands = vec![ RenderCommand::BeginRenderPass { render_pass: render_pass_id, viewport }, @@ -277,6 +284,7 @@ let commands = vec![ ## Changelog +- 2025-12-15 (v0.2.0) — Update example code to use `render_context.gpu()` and add `surface_format`/`depth_format` parameters to `RenderPipelineBuilder`. - 2025-11-22 (v0.1.0) — Initial draft specifying indexed draws and multiple vertex buffers, including API surface, behavior, validation hooks, performance guidance, and verification plan. - 2025-11-22 (v0.1.1) — Added engine-level `IndexFormat`, instance ranges to `Draw`/`DrawIndexed`, encoder-side validation for pipeline and index buffer bindings, and updated requirements checklist. - 2025-11-23 (v0.1.2) — Added index buffer stride and range validation, device limit checks for vertex buffer slots and attributes, an example scene with indexed draws and multiple vertex buffers, and updated the requirements checklist. diff --git a/docs/specs/textures-and-samplers.md b/docs/specs/textures-and-samplers.md index 80ce5f12..93f5e979 100644 --- a/docs/specs/textures-and-samplers.md +++ b/docs/specs/textures-and-samplers.md @@ -3,13 +3,13 @@ title: "Textures and Samplers" document_id: "texture-sampler-spec-2025-10-30" status: "draft" created: "2025-10-30T00:00:00Z" -last_updated: "2025-11-10T00:00:00Z" -version: "0.3.1" +last_updated: "2025-12-15T00:00:00Z" +version: "0.4.0" engine_workspace_version: "2023.1.30" wgpu_version: "26.0.1" shader_backend_default: "naga" winit_version: "0.29.10" -repo_commit: "fc5eb52c74eb0835225959f941db8e991112b87d" +repo_commit: "71256389b9efe247a59aabffe9de58147b30669d" owners: ["lambda-sh"] reviewers: ["engine", "rendering"] tags: ["spec", "rendering", "textures", "samplers", "wgpu"] @@ -314,25 +314,25 @@ let texture2d = TextureBuilder::new_2d(TextureFormat::Rgba8UnormSrgb) .with_size(512, 512) .with_data(&pixels) .with_label("albedo") - .build(&mut render_context)?; + .build(render_context.gpu())?; let sampler = SamplerBuilder::new() .linear_clamp() .with_label("albedo-sampler") - .build(&mut render_context); + .build(render_context.gpu()); let layout2d = BindGroupLayoutBuilder::new() .with_uniform(0, BindingVisibility::VertexAndFragment) .with_sampled_texture(1) // 2D shorthand .with_sampler(2) - .build(&mut render_context); + .build(render_context.gpu()); let group2d = BindGroupBuilder::new() .with_layout(&layout2d) .with_uniform(0, &uniform_buffer) .with_texture(1, &texture2d) .with_sampler(2, &sampler) - .build(&mut render_context); + .build(render_context.gpu()); RC::SetBindGroup { set: 0, group: group_id, dynamic_offsets: vec![] }; ``` @@ -360,18 +360,18 @@ let texture3d = TextureBuilder::new_3d(TextureFormat::Rgba8Unorm) .with_size_3d(128, 128, 64) .with_data(&voxels) .with_label("volume") - .build(&mut render_context)?; + .build(render_context.gpu())?; let layout3d = BindGroupLayoutBuilder::new() .with_sampled_texture_dim(1, ViewDimension::D3, BindingVisibility::Fragment) .with_sampler(2) - .build(&mut render_context); + .build(render_context.gpu()); let group3d = BindGroupBuilder::new() .with_layout(&layout3d) .with_texture(1, &texture3d) .with_sampler(2, &sampler) - .build(&mut render_context); + .build(render_context.gpu()); ``` WGSL snippet (3D) @@ -389,7 +389,8 @@ fn fs_main(in_uv: vec2) -> @location(0) vec4 { ## Changelog -- 2025-11-10 (v0.3.1) — Merge “Future Extensions” into the Requirements +- 2025-12-15 (v0.4.0) — Update example code to use `render_context.gpu()` for all builder calls. +- 2025-11-10 (v0.3.1) — Merge "Future Extensions" into the Requirements Checklist and mark implemented status; metadata updated. - 2025-11-09 (v0.3.0) — Clarify layout visibility parameters; make sampler build infallible; correct `BindingVisibility` usage in examples; diff --git a/docs/specs/uniform-buffers-and-bind-groups.md b/docs/specs/uniform-buffers-and-bind-groups.md index 04d18599..c4c4ca5c 100644 --- a/docs/specs/uniform-buffers-and-bind-groups.md +++ b/docs/specs/uniform-buffers-and-bind-groups.md @@ -3,13 +3,13 @@ title: "Uniform Buffers and Bind Groups" document_id: "ubo-spec-2025-10-11" status: "living" created: "2025-10-11T00:00:00Z" -last_updated: "2025-10-17T00:00:00Z" -version: "0.4.0" +last_updated: "2025-12-15T00:00:00Z" +version: "0.5.0" engine_workspace_version: "2023.1.30" wgpu_version: "26.0.1" shader_backend_default: "naga" winit_version: "0.29.10" -repo_commit: "00aababeb76370ebdeb67fc12ab4393aac5e4193" +repo_commit: "71256389b9efe247a59aabffe9de58147b30669d" owners: ["lambda-sh"] reviewers: ["engine", "rendering"] tags: ["spec", "rendering", "uniforms", "bind-groups", "wgpu"] @@ -157,7 +157,7 @@ struct Globals { view_proj: [[f32; 4]; 4] } // Layout: set(0)@binding(0) uniform visible to vertex stage let layout = BindGroupLayoutBuilder::new() .with_uniform(0, BindingVisibility::Vertex) - .build(&mut rc); + .build(rc.gpu()); // Create UBO let ubo = BufferBuilder::new() @@ -165,19 +165,26 @@ let ubo = BufferBuilder::new() .with_usage(Usage::UNIFORM) .with_properties(Properties::CPU_VISIBLE) .with_label("globals-ubo") - .build(&mut rc, vec![Globals { view_proj }])?; + .build(rc.gpu(), vec![Globals { view_proj }])?; // Bind group that points binding(0) at our UBO let group0 = BindGroupBuilder::new() .with_layout(&layout) .with_uniform(0, &ubo, 0, None) - .build(&mut rc); + .build(rc.gpu()); // Pipeline includes the layout let pipe = RenderPipelineBuilder::new() .with_layouts(&[&layout]) .with_buffer(vbo, attributes) - .build(&mut rc, &pass, &vs, Some(&fs)); + .build( + rc.gpu(), + rc.surface_format(), + rc.depth_format(), + &pass, + &vs, + Some(&fs), + ); // Encode commands let cmds = vec![ @@ -211,7 +218,7 @@ Dynamic offsets ```rust let dyn_layout = BindGroupLayoutBuilder::new() .with_uniform_dynamic(0, BindingVisibility::Vertex) - .build(&mut rc); + .build(rc.gpu()); let align = rc.limit_min_uniform_buffer_offset_alignment() as u64; let size = core::mem::size_of::() as u64; @@ -290,6 +297,7 @@ RC::SetBindGroup { set: 0, group: dyn_group_id, dynamic_offsets: offsets }; ## Changelog +- 2025-12-15 (v0.5.0) — Update example code to use `rc.gpu()` and add `surface_format`/`depth_format` parameters to `RenderPipelineBuilder`. - 2025-10-17 (v0.4.0) — Restructure to match spec template: add Summary, Scope, Terminology, Design (API/Behavior/Validation), Constraints and Rules, Requirements Checklist, Verification and Testing, and Compatibility. Remove diff --git a/docs/tutorials/indexed-draws-and-multiple-vertex-buffers.md b/docs/tutorials/indexed-draws-and-multiple-vertex-buffers.md index 94bca1e5..b372838e 100644 --- a/docs/tutorials/indexed-draws-and-multiple-vertex-buffers.md +++ b/docs/tutorials/indexed-draws-and-multiple-vertex-buffers.md @@ -3,13 +3,13 @@ title: "Indexed Draws and Multiple Vertex Buffers" document_id: "indexed-draws-multiple-vertex-buffers-tutorial-2025-11-22" status: "draft" created: "2025-11-22T00:00:00Z" -last_updated: "2025-11-23T00:00:00Z" -version: "0.2.0" +last_updated: "2025-12-15T00:00:00Z" +version: "0.3.0" engine_workspace_version: "2023.1.30" wgpu_version: "26.0.1" shader_backend_default: "naga" winit_version: "0.29.10" -repo_commit: "db7fa78d143e5ff69028413fe86c948be9ba76ee" +repo_commit: "71256389b9efe247a59aabffe9de58147b30669d" owners: ["lambda-sh"] reviewers: ["engine", "rendering"] tags: ["tutorial", "graphics", "indexed-draws", "vertex-buffers", "rust", "wgpu"] @@ -219,7 +219,11 @@ fn on_attach( &mut self, render_context: &mut RenderContext, ) -> Result { - let render_pass = RenderPassBuilder::new().build(render_context); + let render_pass = RenderPassBuilder::new().build( + render_context.gpu(), + render_context.surface_format(), + render_context.depth_format(), + ); let positions: Vec = vec![ PositionVertex { @@ -259,7 +263,7 @@ fn on_attach( .with_properties(Properties::DEVICE_LOCAL) .with_buffer_type(BufferType::Vertex) .with_label("indexed-positions") - .build(render_context, positions) + .build(render_context.gpu(), positions) .map_err(|error| error.to_string())?; let color_buffer = BufferBuilder::new() @@ -267,7 +271,7 @@ fn on_attach( .with_properties(Properties::DEVICE_LOCAL) .with_buffer_type(BufferType::Vertex) .with_label("indexed-colors") - .build(render_context, colors) + .build(render_context.gpu(), colors) .map_err(|error| error.to_string())?; let index_buffer = BufferBuilder::new() @@ -275,7 +279,7 @@ fn on_attach( .with_properties(Properties::DEVICE_LOCAL) .with_buffer_type(BufferType::Index) .with_label("indexed-indices") - .build(render_context, indices) + .build(render_context.gpu(), indices) .map_err(|error| error.to_string())?; let pipeline = RenderPipelineBuilder::new() @@ -303,7 +307,9 @@ fn on_attach( }], ) .build( - render_context, + render_context.gpu(), + render_context.surface_format(), + render_context.depth_format(), &render_pass, &self.vertex_shader, Some(&self.fragment_shader), @@ -484,5 +490,6 @@ This tutorial demonstrates how indexed draws and multiple vertex buffers combine ## Changelog +- 2025-12-15 (v0.3.0) — Update builder API calls to use `render_context.gpu()` and add `surface_format`/`depth_format` parameters to `RenderPassBuilder` and `RenderPipelineBuilder`. - 2025-11-23 (v0.2.0) — Filled in the implementation steps for the indexed draws and multiple vertex buffers tutorial and aligned the narrative with the `indexed_multi_vertex_buffers` example. - 2025-11-22 (v0.1.0) — Initial skeleton for the indexed draws and multiple vertex buffers tutorial; content placeholders added for future implementation. diff --git a/docs/tutorials/instanced-quads.md b/docs/tutorials/instanced-quads.md index 8aa339d5..936daefb 100644 --- a/docs/tutorials/instanced-quads.md +++ b/docs/tutorials/instanced-quads.md @@ -3,13 +3,13 @@ title: "Instanced Rendering: Grid of Colored Quads" document_id: "instanced-quads-tutorial-2025-11-25" status: "draft" created: "2025-11-25T00:00:00Z" -last_updated: "2025-11-25T02:20:00Z" -version: "0.1.1" +last_updated: "2025-12-15T00:00:00Z" +version: "0.2.0" engine_workspace_version: "2023.1.30" wgpu_version: "26.0.1" shader_backend_default: "naga" winit_version: "0.29.10" -repo_commit: "c8f727f3774029135ed1f7a7224288faf7b9e442" +repo_commit: "71256389b9efe247a59aabffe9de58147b30669d" owners: ["lambda-sh"] reviewers: ["engine", "rendering"] tags: ["tutorial", "graphics", "instancing", "vertex-buffers", "rust", "wgpu"] @@ -217,7 +217,11 @@ fn on_attach( &mut self, render_context: &mut RenderContext, ) -> Result { - let render_pass = RenderPassBuilder::new().build(render_context); + let render_pass = RenderPassBuilder::new().build( + render_context.gpu(), + render_context.surface_format(), + render_context.depth_format(), + ); // Quad geometry in clip space centered at the origin. let quad_vertices: Vec = vec![ @@ -269,7 +273,7 @@ fn on_attach( .with_properties(Properties::DEVICE_LOCAL) .with_buffer_type(BufferType::Vertex) .with_label("instanced-quads-vertices") - .build(render_context, quad_vertices) + .build(render_context.gpu(), quad_vertices) .map_err(|error| error.to_string())?; let instance_buffer = BufferBuilder::new() @@ -277,7 +281,7 @@ fn on_attach( .with_properties(Properties::DEVICE_LOCAL) .with_buffer_type(BufferType::Vertex) .with_label("instanced-quads-instances") - .build(render_context, instances) + .build(render_context.gpu(), instances) .map_err(|error| error.to_string())?; let index_buffer = BufferBuilder::new() @@ -285,7 +289,7 @@ fn on_attach( .with_properties(Properties::DEVICE_LOCAL) .with_buffer_type(BufferType::Index) .with_label("instanced-quads-indices") - .build(render_context, indices) + .build(render_context.gpu(), indices) .map_err(|error| error.to_string())?; // Vertex attributes for per-vertex positions in slot 0. @@ -323,7 +327,9 @@ fn on_attach( .with_buffer(vertex_buffer, vertex_attributes) .with_instance_buffer(instance_buffer, instance_attributes) .build( - render_context, + render_context.gpu(), + render_context.surface_format(), + render_context.depth_format(), &render_pass, &self.vertex_shader, Some(&self.fragment_shader), @@ -497,5 +503,6 @@ This tutorial demonstrates how the `lambda-rs` crate uses per-vertex and per-ins ## Changelog +- 2025-12-15 (v0.2.0) — Update builder API calls to use `render_context.gpu()` and add `surface_format`/`depth_format` parameters to `RenderPassBuilder` and `RenderPipelineBuilder`. - 2025-11-25 (v0.1.1) — Align feature naming with `render-validation-instancing` and update metadata. - 2025-11-25 (v0.1.0) — Initial instanced quads tutorial describing per-vertex and per-instance buffers and the `instanced_quads` example. diff --git a/docs/tutorials/reflective-room.md b/docs/tutorials/reflective-room.md index 7cdc74a2..847a1754 100644 --- a/docs/tutorials/reflective-room.md +++ b/docs/tutorials/reflective-room.md @@ -3,13 +3,13 @@ title: "Reflective Floor: Stencil‑Masked Planar Reflections" document_id: "reflective-room-tutorial-2025-11-17" status: "draft" created: "2025-11-17T00:00:00Z" -last_updated: "2025-11-21T00:00:00Z" -version: "0.2.2" +last_updated: "2025-12-15T00:00:00Z" +version: "0.3.0" engine_workspace_version: "2023.1.30" wgpu_version: "26.0.1" shader_backend_default: "naga" winit_version: "0.29.10" -repo_commit: "1f91ff4ec776ec5435fce8a53441010d9e0c86e6" +repo_commit: "71256389b9efe247a59aabffe9de58147b30669d" owners: ["lambda-sh"] reviewers: ["engine", "rendering"] tags: ["tutorial", "graphics", "stencil", "depth", "msaa", "mirror", "3d", "push-constants", "wgpu", "rust"] @@ -200,14 +200,22 @@ let pass_mask = RenderPassBuilder::new() .with_stencil_clear(0) .with_multi_sample(msaa_samples) .without_color() // no color target - .build(ctx); + .build( + ctx.gpu(), + ctx.surface_format(), + ctx.depth_format(), + ); let pass_color = RenderPassBuilder::new() .with_label("reflective-room-pass-color") .with_multi_sample(msaa_samples) .with_depth_clear(1.0) // or .with_depth_load() when depth test is off .with_stencil_load() // preserve mask from pass 1 - .build(ctx); + .build( + ctx.gpu(), + ctx.surface_format(), + ctx.depth_format(), + ); ``` Rationale: pipelines that use stencil require a depth‑stencil attachment, even if depth testing is disabled. @@ -231,7 +239,14 @@ let pipe_floor_mask = RenderPipelineBuilder::new() read_mask: 0xFF, write_mask: 0xFF, }) .with_multi_sample(msaa_samples) - .build(ctx, &pass_mask, &shader_vs, None); + .build( + ctx.gpu(), + ctx.surface_format(), + ctx.depth_format(), + &pass_mask, + &shader_vs, + None, + ); ``` ### Step 6 — Pipeline: Reflected Cube (Stencil Test) @@ -253,7 +268,14 @@ let mut builder = RenderPipelineBuilder::new() .with_depth_write(false) .with_depth_compare(CompareFunction::Always); -let pipe_reflected = builder.build(ctx, &pass_color, &shader_vs, Some(&shader_fs_lit)); +let pipe_reflected = builder.build( + ctx.gpu(), + ctx.surface_format(), + ctx.depth_format(), + &pass_color, + &shader_vs, + Some(&shader_fs_lit), +); ``` ### Step 7 — Pipeline: Floor Visual (Tinted) @@ -273,7 +295,14 @@ if depth_test_enabled || stencil_enabled { .with_depth_compare(if depth_test_enabled { CompareFunction::LessEqual } else { CompareFunction::Always }); } -let pipe_floor_visual = floor_vis.build(ctx, &pass_color, &shader_vs, Some(&shader_fs_floor)); +let pipe_floor_visual = floor_vis.build( + ctx.gpu(), + ctx.surface_format(), + ctx.depth_format(), + &pass_color, + &shader_vs, + Some(&shader_fs_floor), +); ``` ### Step 8 — Pipeline: Normal Cube @@ -293,7 +322,14 @@ if depth_test_enabled || stencil_enabled { .with_depth_compare(if depth_test_enabled { CompareFunction::Less } else { CompareFunction::Always }); } -let pipe_normal = normal.build(ctx, &pass_color, &shader_vs, Some(&shader_fs_lit)); +let pipe_normal = normal.build( + ctx.gpu(), + ctx.surface_format(), + ctx.depth_format(), + &pass_color, + &shader_vs, + Some(&shader_fs_lit), +); ``` ### Step 9 — Per‑Frame Transforms and Reflection @@ -422,6 +458,7 @@ The reflective floor combines a simple stencil mask with an optional depth test ## Changelog +- 2025-12-15, 0.3.0: Update builder API calls to use `ctx.gpu()` and add `surface_format`/`depth_format` parameters to `RenderPassBuilder` and `RenderPipelineBuilder`. - 2025-11-21, 0.2.2: Align tutorial with removal of the unmasked reflection debug toggle in the example and update metadata to the current engine workspace commit. - 0.2.0 (2025‑11‑19): Updated for camera pitch, front‑face culling on reflection, lit translucent floor, unmasked reflection debug toggle, floor overlay toggle, and Metal portability note. - 0.1.0 (2025‑11‑17): Initial draft aligned with `crates/lambda-rs/examples/reflective_room.rs`, including stencil mask pass, reflected pipeline, and MSAA/depth toggles. diff --git a/docs/tutorials/textured-cube.md b/docs/tutorials/textured-cube.md index 6506ac3b..319d81e0 100644 --- a/docs/tutorials/textured-cube.md +++ b/docs/tutorials/textured-cube.md @@ -3,13 +3,13 @@ title: "Textured Cube: 3D Push Constants + 2D Sampling" document_id: "textured-cube-tutorial-2025-11-10" status: "draft" created: "2025-11-10T00:00:00Z" -last_updated: "2025-11-10T03:00:00Z" -version: "0.1.1" +last_updated: "2025-12-15T00:00:00Z" +version: "0.2.0" engine_workspace_version: "2023.1.30" wgpu_version: "26.0.1" shader_backend_default: "naga" winit_version: "0.29.10" -repo_commit: "fe79756541e33270eca76638400bb64c6ec9f732" +repo_commit: "71256389b9efe247a59aabffe9de58147b30669d" owners: ["lambda-sh"] reviewers: ["engine", "rendering"] tags: ["tutorial", "graphics", "3d", "push-constants", "textures", "samplers", "rust", "wgpu"] @@ -304,7 +304,7 @@ let texture2d = TextureBuilder::new_2d(TextureFormat::Rgba8UnormSrgb) .with_size(tex_w, tex_h) .with_data(&pixels) .with_label("checkerboard") - .build(render_context) + .build(render_context.gpu()) .expect("Failed to create 2D texture"); ``` @@ -318,7 +318,7 @@ use lambda::render::texture::SamplerBuilder; let sampler = SamplerBuilder::new() .linear_clamp() - .build(render_context); + .build(render_context.gpu()); ``` ### Step 6 — Bind Group Layout and Bind Group @@ -330,13 +330,13 @@ use lambda::render::bind::{BindGroupBuilder, BindGroupLayoutBuilder}; let layout = BindGroupLayoutBuilder::new() .with_sampled_texture(1) .with_sampler(2) - .build(render_context); + .build(render_context.gpu()); let bind_group = BindGroupBuilder::new() .with_layout(&layout) .with_texture(1, &texture2d) .with_sampler(2, &sampler) - .build(render_context); + .build(render_context.gpu()); ``` ### Step 7 — Render Pipeline with Depth and Culling @@ -352,7 +352,11 @@ use lambda::render::{ let render_pass = RenderPassBuilder::new() .with_label("textured-cube-pass") .with_depth() - .build(render_context); + .build( + render_context.gpu(), + render_context.surface_format(), + render_context.depth_format(), + ); let push_constants_size = std::mem::size_of::() as u32; @@ -361,12 +365,19 @@ let pipeline = RenderPipelineBuilder::new() .with_depth() .with_push_constant(PipelineStage::VERTEX, push_constants_size) .with_buffer( - BufferBuilder::build_from_mesh(&mesh, render_context) + BufferBuilder::build_from_mesh(&mesh, render_context.gpu()) .expect("Failed to create vertex buffer"), mesh.attributes().to_vec(), ) .with_layouts(&[&layout]) - .build(render_context, &render_pass, &self.shader_vs, Some(&self.shader_fs)); + .build( + render_context.gpu(), + render_context.surface_format(), + render_context.depth_format(), + &render_pass, + &self.shader_vs, + Some(&self.shader_fs), + ); // Attach to obtain ResourceId handles self.render_pass = Some(render_context.attach_render_pass(render_pass)); @@ -520,5 +531,6 @@ constants for per‑draw transforms alongside 2D sampling in a 3D render path. - Bind two textures and blend per face based on projected UVs. ## Changelog +- 0.2.0 (2025-12-15): Update builder API calls to use `render_context.gpu()` and add `surface_format`/`depth_format` parameters to `RenderPassBuilder` and `RenderPipelineBuilder`. - 0.1.1 (2025-11-10): Add Conclusion section summarizing outcomes; update metadata and commit. - 0.1.0 (2025-11-10): Initial draft aligned with `crates/lambda-rs/examples/textured_cube.rs` including push constants, depth, culling, and projected UV sampling. diff --git a/docs/tutorials/textured-quad.md b/docs/tutorials/textured-quad.md index cc828dcc..24c7b1d0 100644 --- a/docs/tutorials/textured-quad.md +++ b/docs/tutorials/textured-quad.md @@ -3,13 +3,13 @@ title: "Textured Quad: Sample a 2D Texture" document_id: "textured-quad-tutorial-2025-11-01" status: "draft" created: "2025-11-01T00:00:00Z" -last_updated: "2025-11-10T03:00:00Z" -version: "0.3.3" +last_updated: "2025-12-15T00:00:00Z" +version: "0.4.0" engine_workspace_version: "2023.1.30" wgpu_version: "26.0.1" shader_backend_default: "naga" winit_version: "0.29.10" -repo_commit: "fe79756541e33270eca76638400bb64c6ec9f732" +repo_commit: "71256389b9efe247a59aabffe9de58147b30669d" owners: ["lambda-sh"] reviewers: ["engine", "rendering"] tags: ["tutorial", "graphics", "textures", "samplers", "rust", "wgpu"] @@ -306,7 +306,7 @@ let texture = TextureBuilder::new_2d(TextureFormat::Rgba8UnormSrgb) .with_size(texture_width, texture_height) .with_data(&pixels) .with_label("checkerboard") - .build(render_context) + .build(render_context.gpu()) .expect("Failed to create texture"); ``` @@ -321,7 +321,7 @@ use lambda::render::texture::SamplerBuilder; let sampler = SamplerBuilder::new() .linear_clamp() .with_label("linear-clamp") - .build(render_context); + .build(render_context.gpu()); ``` This sampler selects linear minification and magnification with clamp‑to‑edge addressing. Linear filtering smooths the checkerboard when scaled, while clamping prevents wrapping at the texture borders. @@ -335,13 +335,13 @@ use lambda::render::bind::{BindGroupLayoutBuilder, BindGroupBuilder}; let layout = BindGroupLayoutBuilder::new() .with_sampled_texture(1) // texture2D at binding 1 .with_sampler(2) // sampler at binding 2 - .build(render_context); + .build(render_context.gpu()); let bind_group = BindGroupBuilder::new() .with_layout(&layout) .with_texture(1, &texture) .with_sampler(2, &sampler) - .build(render_context); + .build(render_context.gpu()); ``` The bind group layout declares the shader‑visible interface for set 0: a sampled `texture2D` at binding 1 and a `sampler` at binding 2. The bind group then binds the concrete texture and sampler objects to those indices so the fragment shader can sample them during rendering. @@ -358,7 +358,11 @@ use lambda::render::{ let render_pass = RenderPassBuilder::new() .with_label("textured-quad-pass") - .build(render_context); + .build( + render_context.gpu(), + render_context.surface_format(), + render_context.depth_format(), + ); let mesh = self.mesh.as_ref().expect("mesh must be created"); @@ -366,11 +370,18 @@ let pipeline = RenderPipelineBuilder::new() .with_culling(CullingMode::None) .with_layouts(&[&layout]) .with_buffer( - BufferBuilder::build_from_mesh(mesh, render_context) + BufferBuilder::build_from_mesh(mesh, render_context.gpu()) .expect("Failed to create vertex buffer"), mesh.attributes().to_vec(), ) - .build(render_context, &render_pass, &self.shader_vs, Some(&self.shader_fs)); + .build( + render_context.gpu(), + render_context.surface_format(), + render_context.depth_format(), + &render_pass, + &self.shader_vs, + Some(&self.shader_fs), + ); // Attach resources to obtain `ResourceId`s for rendering self.render_pass = Some(render_context.attach_render_pass(render_pass)); @@ -474,6 +485,7 @@ with correct color space handling and filtering. - Discuss artifacts without mipmaps and how multiple levels would improve minification. ## Changelog +- 0.4.0 (2025-12-15): Update builder API calls to use `render_context.gpu()` and add `surface_format`/`depth_format` parameters to `RenderPassBuilder` and `RenderPipelineBuilder`. - 0.3.3 (2025-11-10): Add Conclusion section summarizing outcomes; update metadata and commit. - 0.3.2 (2025-11-10): Add narrative explanations after each code block; clarify lifecycle and binding flow. - 0.3.1 (2025-11-10): Align with example; add shader constants; attach resources; fix variable names; add missing section. diff --git a/docs/tutorials/uniform-buffers.md b/docs/tutorials/uniform-buffers.md index e779b397..b04034f9 100644 --- a/docs/tutorials/uniform-buffers.md +++ b/docs/tutorials/uniform-buffers.md @@ -3,24 +3,26 @@ title: "Uniform Buffers: Build a Spinning Triangle" document_id: "uniform-buffers-tutorial-2025-10-17" status: "draft" created: "2025-10-17T00:00:00Z" -last_updated: "2025-11-10T03:00:00Z" -version: "0.4.1" +last_updated: "2025-12-15T00:00:00Z" +version: "0.5.0" engine_workspace_version: "2023.1.30" wgpu_version: "26.0.1" shader_backend_default: "naga" winit_version: "0.29.10" -repo_commit: "fe79756541e33270eca76638400bb64c6ec9f732" +repo_commit: "71256389b9efe247a59aabffe9de58147b30669d" owners: ["lambda-sh"] reviewers: ["engine", "rendering"] tags: ["tutorial", "graphics", "uniform-buffers", "rust", "wgpu"] --- ## Overview + Uniform buffer objects (UBOs) are a standard mechanism to pass per‑frame or per‑draw constants to shaders. This document demonstrates a minimal 3D spinning triangle that uses a UBO to provide a model‑view‑projection matrix to the vertex shader. Reference implementation: `crates/lambda-rs/examples/uniform_buffer_triangle.rs`. ## Table of Contents + - [Overview](#overview) - [Goals](#goals) - [Prerequisites](#prerequisites) @@ -51,17 +53,20 @@ Reference implementation: `crates/lambda-rs/examples/uniform_buffer_triangle.rs` - Learn how to construct a render pipeline and issue draw commands using Lambda’s builders. ## Prerequisites + - Rust toolchain installed and the workspace builds: `cargo build --workspace`. - Familiarity with basic Rust and the repository’s example layout. - Ability to run examples: `cargo run --example minimal` verifies setup. ## Requirements and Constraints + - The uniform block layout in the shader and the Rust structure MUST match in size, alignment, and field order. - The bind group layout in Rust MUST match the shader `set` and `binding` indices. - Matrices MUST be provided in the order expected by the shader (column‑major in this example). Rationale: prevents implicit driver conversions and avoids incorrect transforms. - Acronyms MUST be defined on first use (e.g., uniform buffer object (UBO)). ## Data Flow + - CPU writes → UBO → bind group (set 0) → pipeline layout → vertex shader. - A single UBO MAY be reused across multiple draws and pipelines. @@ -80,6 +85,7 @@ Bind Group ──▶ Pipeline Layout ──▶ Render Pipeline ──▶ Vertex ## Implementation Steps ### Step 1 — Runtime and Component Skeleton + Before rendering, create a minimal application entry point and a `Component` that receives lifecycle callbacks. The engine routes initialization, input, updates, and rendering through the component interface, which provides the context needed to create GPU resources and submit commands. ```rust @@ -121,6 +127,7 @@ fn main() { ``` ### Step 2 — Vertex and Fragment Shaders + Define shader stages next. The vertex shader declares three vertex attributes and a uniform block at set 0, binding 0. It multiplies the incoming position by the matrix stored in the UBO. The fragment shader returns the interpolated color. Declaring the uniform block now establishes the contract that the Rust side will satisfy via a matching bind group layout and buffer. ```glsl @@ -176,6 +183,7 @@ let fragment_shader: Shader = shader_builder.build(fragment_virtual); ``` ### Step 3 — Mesh Data and Vertex Layout + Provide vertex data for a single triangle and describe how the pipeline reads it. Each vertex stores position, normal, and color as three `f32` values. The attribute descriptors specify locations and byte offsets so the pipeline can interpret the packed buffer consistently across platforms. ```rust @@ -213,6 +221,7 @@ let mesh: Mesh = mesh_builder ``` ### Step 4 — Uniform Data Layout in Rust + Mirror the shader’s uniform block with a Rust structure. Use `#[repr(C)]` so the memory layout is predictable. A `mat4` in the shader corresponds to a 4×4 `f32` array here. Many GPU interfaces expect column‑major matrices; transpose before upload if the local math library is row‑major. This avoids implicit driver conversions and prevents incorrect transforms. ```rust @@ -224,6 +233,7 @@ pub struct GlobalsUniform { ``` ### Step 5 — Bind Group Layout at Set 0 + Create a bind group layout that matches the shader declaration. This layout says: at set 0, binding 0 there is a uniform buffer visible to the vertex stage. The pipeline layout will incorporate this, ensuring the shader and the bound resources agree at draw time. ```rust @@ -231,10 +241,11 @@ use lambda::render::bind::{BindGroupLayoutBuilder, BindingVisibility}; let layout = BindGroupLayoutBuilder::new() .with_uniform(0, BindingVisibility::Vertex) // binding 0 - .build(render_context); + .build(render_context.gpu()); ``` ### Step 6 — Create the Uniform Buffer and Bind Group + Allocate the uniform buffer, seed it with an initial matrix, and create a bind group using the layout. Mark the buffer usage as `UNIFORM` and properties as `CPU_VISIBLE` to permit direct per‑frame writes from the CPU. This is the simplest path for frequently updated data. ```rust @@ -247,7 +258,7 @@ let uniform_buffer = BufferBuilder::new() .with_usage(Usage::UNIFORM) .with_properties(Properties::CPU_VISIBLE) .with_label("globals-uniform") - .build(render_context, vec![initial_uniform]) + .build(render_context.gpu(), vec![initial_uniform]) .expect("Failed to create uniform buffer"); use lambda::render::bind::BindGroupBuilder; @@ -255,10 +266,11 @@ use lambda::render::bind::BindGroupBuilder; let bind_group = BindGroupBuilder::new() .with_layout(&layout) .with_uniform(0, &uniform_buffer, 0, None) // binding 0 - .build(render_context); + .build(render_context.gpu()); ``` ### Step 7 — Build the Render Pipeline + Construct the render pipeline, supplying the bind group layouts, vertex buffer, and the shader pair. Disable face culling for simplicity so both sides of the triangle remain visible regardless of winding during early experimentation. ```rust @@ -267,19 +279,31 @@ use lambda::render::{ render_pass::RenderPassBuilder, }; -let render_pass = RenderPassBuilder::new().build(render_context); +let render_pass = RenderPassBuilder::new().build( + render_context.gpu(), + render_context.surface_format(), + render_context.depth_format(), +); let pipeline = RenderPipelineBuilder::new() .with_culling(lambda::render::pipeline::CullingMode::None) .with_layouts(&[&layout]) .with_buffer( - BufferBuilder::build_from_mesh(&mesh, render_context).expect("Failed to create buffer"), + BufferBuilder::build_from_mesh(&mesh, render_context.gpu()).expect("Failed to create buffer"), mesh.attributes().to_vec(), ) - .build(render_context, &render_pass, &vertex_shader, Some(&fragment_shader)); + .build( + render_context.gpu(), + render_context.surface_format(), + render_context.depth_format(), + &render_pass, + &vertex_shader, + Some(&fragment_shader), + ); ``` ### Step 8 — Per‑Frame Update and Write + Animate by recomputing the model‑view‑projection matrix each frame and writing it into the uniform buffer. The helper `compute_model_view_projection_matrix_about_pivot` maintains a correct aspect ratio using the current window dimensions and rotates the model around a chosen pivot. ```rust @@ -314,11 +338,12 @@ fn update_uniform_each_frame( ); let value = GlobalsUniform { render_matrix: model_view_projection_matrix.transpose() }; - uniform_buffer.write_value(render_context, 0, &value); + uniform_buffer.write_value(render_context.gpu(), 0, &value); } ``` ### Step 9 — Issue Draw Commands + Record commands in the order the GPU expects: begin the render pass, set the pipeline, configure viewport and scissors, bind the vertex buffer and the uniform bind group, draw the vertices, then end the pass. This sequence describes the full state required for a single draw. ```rust @@ -342,6 +367,7 @@ let commands = vec![ ``` ### Step 10 — Handle Window Resize + Track window dimensions and update the per‑frame matrix using the new aspect ratio. Forwarding resize events into stored `width` and `height` maintains consistent camera projection across resizes. ```rust @@ -359,10 +385,12 @@ fn on_event(&mut self, event: Events) -> Result { ``` ## Validation + - Build the workspace: `cargo build --workspace` - Run the example: `cargo run --example uniform_buffer_triangle` ## Notes + - Layout matching: The Rust `GlobalsUniform` MUST match the shader block layout. Keep `#[repr(C)]` and follow alignment rules. - Matrix order: The shader expects column‑major matrices, so the uploaded matrix MUST be transposed if the local math library uses row‑major. - Binding indices: The Rust bind group layout and `.with_uniform(0, ...)`, plus the shader `set = 0, binding = 0`, MUST be consistent. @@ -370,6 +398,7 @@ fn on_event(&mut self, event: Events) -> Result { - Pipeline layout: All bind group layouts used by the pipeline MUST be included via `.with_layouts(...)`. ## Conclusion + This tutorial produced a spinning triangle that reads a model‑view‑projection matrix from a uniform buffer. The implementation aligned the shader and Rust layouts, created shaders and a mesh, defined a bind group layout and uniform @@ -412,9 +441,9 @@ multiple objects and passes. ## Changelog +- 0.5.0 (2025-12-15): Update builder API calls to use `render_context.gpu()` and add `surface_format`/`depth_format` parameters to `RenderPassBuilder` and `RenderPipelineBuilder`. - 0.4.1 (2025‑11‑10): Add Conclusion section summarizing accomplishments; update metadata and commit. - - 0.4.0 (2025‑10‑30): Added table of contents with links; converted sections to anchored headings; added ASCII data flow diagram; metadata updated. - 0.2.0 (2025‑10‑17): Added goals and book‑style step explanations; expanded rationale before code blocks; refined validation and notes. From c60c667a71d06604439bd0e95699651446edb82e Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 15 Dec 2025 13:43:56 -0800 Subject: [PATCH 23/28] [update] spacing. --- docs/game_roadmap_and_prototype.md | 14 +++++++++++++ docs/specs/depth-stencil-msaa.md | 5 +++++ ...dexed-draws-and-multiple-vertex-buffers.md | 1 + docs/specs/textures-and-samplers.md | 5 +++++ docs/specs/uniform-buffers-and-bind-groups.md | 9 +++++++-- ...dexed-draws-and-multiple-vertex-buffers.md | 7 +++++++ docs/tutorials/instanced-quads.md | 6 ++++++ docs/tutorials/reflective-room.md | 15 ++++++++++++++ docs/tutorials/textured-cube.md | 20 +++++++++++++++++++ docs/tutorials/textured-quad.md | 19 ++++++++++++++++++ 10 files changed, 99 insertions(+), 2 deletions(-) diff --git a/docs/game_roadmap_and_prototype.md b/docs/game_roadmap_and_prototype.md index 0bdcf6f3..abb3cd21 100644 --- a/docs/game_roadmap_and_prototype.md +++ b/docs/game_roadmap_and_prototype.md @@ -24,6 +24,7 @@ This document outlines current engine capabilities, the gaps to address for 2D/3 Key modules: windowing/events (winit), GPU (wgpu), render context, runtime loop, and GLSL→SPIR‑V shader compilation (naga). Frame flow: + ``` App Components --> ApplicationRuntime --> RenderContext --> wgpu (Device/Queue/Surface) | | | | @@ -49,6 +50,7 @@ Currently supported commands: Begin/EndRenderPass, SetPipeline, SetViewports, Se ## Targeted API Additions (sketches) Bind groups and uniforms (value: larger, structured GPU data; portable across adapters; enables cameras/materials): + ```rust // Layout with one uniform buffer at set(0) binding(0) let layout = BindGroupLayoutBuilder::new() @@ -84,10 +86,12 @@ RC::Draw { vertices: 0..3 }; ``` Notes + - UBO vs push constants: UBOs scale to KBs and are supported widely; use for view/projection and per‑frame data. - Dynamic offsets (optional later) let you pack many small structs into one UBO. Textures and samplers (value: sprites, materials, UI images; sRGB correctness): + ```rust let tex = TextureBuilder::new_2d(TextureFormat::Rgba8UnormSrgb) .with_size(w, h) @@ -109,6 +113,7 @@ let tex_group = BindGroupBuilder::new(&tex_layout) ``` Index draw and instancing (value: reduce vertex duplication; batch many objects in one draw): + ```rust RC::BindVertexBuffer { pipeline: pipe_id, buffer: 0 }; RC::BindVertexBuffer { pipeline: pipe_id, buffer: 1 }; // instances @@ -117,6 +122,7 @@ RC::DrawIndexed { indices: 0..index_count, base_vertex: 0, instances: 0..instanc ``` Instance buffer attributes example + ```rust // slot 1: per-instance mat3x2 (2D) packed as 3x vec2, plus tint color let instance_attrs = vec![ @@ -130,6 +136,7 @@ let instance_attrs = vec![ ``` Depth/MSAA (value: correct 3D visibility and improved edge quality): + ```rust let pass = RenderPassBuilder::new() .with_clear_color(wgpu::Color::BLACK) @@ -154,10 +161,12 @@ let pipe = RenderPipelineBuilder::new() ``` Notes + - Use reversed‑Z (Greater) later for precision, but start with Less. - MSAA sample count must match between pass and pipeline. Offscreen render targets (value: post‑processing, shadow maps, UI composition, picking): + ```rust let offscreen = RenderTargetBuilder::new() .with_color(TextureFormat::Rgba8UnormSrgb, width, height) @@ -189,6 +198,7 @@ RC::EndRenderPass; ``` WGSL support (value: first‑class wgpu shader language, fewer translation pitfalls): + ```rust let vs = VirtualShader::WgslSource { source: include_str!("shaders/quad.wgsl").into(), name: "quad".into(), entry_point: "vs_main".into() }; let fs = VirtualShader::WgslSource { source: include_str!("shaders/quad.wgsl").into(), name: "quad".into(), entry_point: "fs_main".into() }; @@ -201,6 +211,7 @@ Shader hot‑reload (value: faster iteration; no rebuild): watch file timestamps Goals: sprite batching via instancing; atlas textures; ortho camera; input mapping; text HUD. Target 60 FPS with 10k sprites (mid‑range GPU). Core draw: + ```rust RC::BeginRenderPass { render_pass: pass_id, viewport }; RC::SetPipeline { pipeline: pipe_id }; @@ -214,12 +225,14 @@ RC::EndRenderPass; ``` Building instance data each frame (value: dynamic transforms with minimal overhead): + ```rust // CPU side: update transforms and pack into a Vec queue.write_buffer(instance_vbo.raw(), 0, bytemuck::cast_slice(&instances)); ``` Text rendering options (value: legible UI/HUD): + - Bitmap font atlas: simplest path; pack glyphs into the sprite pipeline. - glyphon/glyph_brush integration: high‑quality layout; more deps; implement later. @@ -230,6 +243,7 @@ Goals: depth test/write, indexed mesh, textured material, simple lighting; orbit Core draw mirrors 2D but with depth enabled and mesh buffers. Camera helpers (value: reduce boilerplate and bugs): + ```rust let proj = matrix::perspective_matrix(60f32.to_radians(), width as f32 / height as f32, 0.1, 100.0); let view = matrix::translation_matrix([0.0, 0.0, -5.0]); // or look_at helper later diff --git a/docs/specs/depth-stencil-msaa.md b/docs/specs/depth-stencil-msaa.md index 169124ff..9b2486e3 100644 --- a/docs/specs/depth-stencil-msaa.md +++ b/docs/specs/depth-stencil-msaa.md @@ -18,6 +18,7 @@ tags: ["spec", "rendering", "depth", "stencil", "msaa"] # Depth/Stencil and Multi-Sample Rendering Summary + - Add configurable depth testing/writes and multi-sample anti-aliasing (MSAA) to the high-level rendering API via builders, without exposing `wgpu` types. - Provide validation and predictable defaults to enable 3D scenes and @@ -85,6 +86,7 @@ App Code - `RenderPipelineBuilder::with_stencil(StencilState) -> Self` - `RenderPipelineBuilder::with_multi_sample(u32) -> Self` - Example (engine types only) + ```rust use lambda::render::render_pass::RenderPassBuilder; use lambda::render::pipeline::{RenderPipelineBuilder, CompareFunction}; @@ -113,6 +115,7 @@ App Code Some(&fragment_shader), ); ``` + - Behavior - Defaults - If neither depth nor stencil is requested on the pass, the pass MUST NOT @@ -178,6 +181,7 @@ App Code - `render-validation-device`: device/format capability advisories (MSAA sample support). Always-on safeguards (release and debug) + - Clamp depth clear to `[0.0, 1.0]`. - Align pipeline `sample_count` to the pass `sample_count`. - Clamp invalid MSAA sample counts to `1`. @@ -254,6 +258,7 @@ Always-on safeguards (release and debug) defaults (no depth, no multi-sampling) unless explicitly configured. ## Changelog + - 2025-12-15 (v0.5.0) — Update example code to use `render_context.gpu()` and add `surface_format`/`depth_format` parameters to `RenderPassBuilder` and `RenderPipelineBuilder`. - 2025-11-21 (v0.4.1) — Clarify depth attachment and clear behavior for stencil-only passes; align specification with engine behavior that preserves diff --git a/docs/specs/indexed-draws-and-multiple-vertex-buffers.md b/docs/specs/indexed-draws-and-multiple-vertex-buffers.md index c880cc11..00963fed 100644 --- a/docs/specs/indexed-draws-and-multiple-vertex-buffers.md +++ b/docs/specs/indexed-draws-and-multiple-vertex-buffers.md @@ -18,6 +18,7 @@ tags: ["spec", "rendering", "vertex-input", "indexed-draws"] # Indexed Draws and Multiple Vertex Buffers ## Table of Contents + - [Summary](#summary) - [Scope](#scope) - [Terminology](#terminology) diff --git a/docs/specs/textures-and-samplers.md b/docs/specs/textures-and-samplers.md index 93f5e979..2a42f1e6 100644 --- a/docs/specs/textures-and-samplers.md +++ b/docs/specs/textures-and-samplers.md @@ -18,6 +18,7 @@ tags: ["spec", "rendering", "textures", "samplers", "wgpu"] # Textures and Samplers Summary + - Introduces first-class 2D and 3D sampled textures and samplers with a builder-based application programming interface and platform abstraction. - Rationale: Texture sampling is foundational for images, sprites, materials, @@ -305,6 +306,7 @@ Render pass: SetPipeline -> SetBindGroup -> Draw ## Example Usage Rust (2D high level) + ```rust use lambda::render::texture::{TextureBuilder, SamplerBuilder, TextureFormat}; use lambda::render::bind::{BindGroupLayoutBuilder, BindGroupBuilder, BindingVisibility}; @@ -338,6 +340,7 @@ RC::SetBindGroup { set: 0, group: group_id, dynamic_offsets: vec![] }; ``` WGSL snippet (2D) + ```wgsl @group(0) @binding(1) var texture_color: texture_2d; @group(0) @binding(2) var sampler_color: sampler; @@ -350,6 +353,7 @@ fn fs_main(in_uv: vec2) -> @location(0) vec4 { ``` Rust (3D high level) + ```rust use lambda::render::texture::{TextureBuilder, TextureFormat}; use lambda::render::bind::{BindGroupLayoutBuilder, BindGroupBuilder}; @@ -375,6 +379,7 @@ let group3d = BindGroupBuilder::new() ``` WGSL snippet (3D) + ```wgsl @group(0) @binding(1) var volume_tex: texture_3d; @group(0) @binding(2) var volume_samp: sampler; diff --git a/docs/specs/uniform-buffers-and-bind-groups.md b/docs/specs/uniform-buffers-and-bind-groups.md index c4c4ca5c..a701c89c 100644 --- a/docs/specs/uniform-buffers-and-bind-groups.md +++ b/docs/specs/uniform-buffers-and-bind-groups.md @@ -18,6 +18,7 @@ tags: ["spec", "rendering", "uniforms", "bind-groups", "wgpu"] # Uniform Buffers and Bind Groups Summary + - Specifies uniform buffer objects (UBOs) and bind groups for the wgpu‑backed renderer, preserving builder/command patterns and the separation between platform and high‑level layers. @@ -72,6 +73,7 @@ Summary platform layer. Data flow (one-time setup → per-frame): + ``` BindGroupLayoutBuilder --> BindGroupLayout --+--> RenderPipelineBuilder (layouts) | @@ -142,6 +144,7 @@ Per-frame commands: BeginRenderPass -> SetPipeline -> SetBindGroup -> Draw -> En ## Example Usage Rust (high level) + ```rust use lambda::render::{ bind::{BindGroupLayoutBuilder, BindGroupBuilder, BindingVisibility}, @@ -198,6 +201,7 @@ rc.render(cmds); ``` WGSL snippet + ```wgsl struct Globals { view_proj: mat4x4; }; @group(0) @binding(0) var globals: Globals; @@ -209,12 +213,14 @@ fn vs_main(in_pos: vec3) -> @builtin(position) vec4 { ``` GLSL snippet (via naga -> SPIR-V) + ```glsl layout(set = 0, binding = 0) uniform Globals { mat4 view_proj; } globals; void main() { gl_Position = globals.view_proj * vec4(in_pos, 1.0); } ``` Dynamic offsets + ```rust let dyn_layout = BindGroupLayoutBuilder::new() .with_uniform_dynamic(0, BindingVisibility::Vertex) @@ -226,6 +232,7 @@ let stride = lambda::render::validation::align_up(size, align); let offsets = vec![0u32, stride as u32, (2*stride) as u32]; RC::SetBindGroup { set: 0, group: dyn_group_id, dynamic_offsets: offsets }; ``` + ## Performance Considerations - Prefer `Properties::DEVICE_LOCAL` for long‑lived uniform buffers that are @@ -293,8 +300,6 @@ RC::SetBindGroup { set: 0, group: dyn_group_id, dynamic_offsets: offsets }; groups continue to function. New pipelines MAY specify layouts via `with_layouts` without impacting prior behavior. - - ## Changelog - 2025-12-15 (v0.5.0) — Update example code to use `rc.gpu()` and add `surface_format`/`depth_format` parameters to `RenderPipelineBuilder`. diff --git a/docs/tutorials/indexed-draws-and-multiple-vertex-buffers.md b/docs/tutorials/indexed-draws-and-multiple-vertex-buffers.md index b372838e..d21a9721 100644 --- a/docs/tutorials/indexed-draws-and-multiple-vertex-buffers.md +++ b/docs/tutorials/indexed-draws-and-multiple-vertex-buffers.md @@ -16,11 +16,13 @@ tags: ["tutorial", "graphics", "indexed-draws", "vertex-buffers", "rust", "wgpu" --- ## Overview + This tutorial constructs a small scene rendered with indexed geometry and multiple vertex buffers. The example separates per-vertex positions from per-vertex colors and draws the result using the engine’s high-level buffer and command builders. Reference implementation: `crates/lambda-rs/examples/indexed_multi_vertex_buffers.rs`. ## Table of Contents + - [Overview](#overview) - [Goals](#goals) - [Prerequisites](#prerequisites) @@ -86,6 +88,7 @@ Render Pass → wgpu::RenderPass::{set_vertex_buffer, set_index_buffer, draw_ind ## Implementation Steps ### Step 1 — Shaders and Vertex Types + Step 1 defines the shader interface and vertex structures used by the example. The shaders consume positions and colors at locations `0` and `1`, and the vertex types store those attributes as three-component floating-point arrays. ```glsl @@ -130,6 +133,7 @@ struct ColorVertex { The shader `location` qualifiers match the vertex buffer layouts declared on the pipeline, and the `PositionVertex` and `ColorVertex` types mirror the `vec3` inputs as `[f32; 3]` arrays in Rust. ### Step 2 — Component State and Shader Construction + Step 2 introduces the `IndexedMultiBufferExample` component and its `Default` implementation, which builds shader objects from the GLSL source and initializes render-resource fields and window dimensions. ```rust @@ -192,6 +196,7 @@ impl Default for IndexedMultiBufferExample { This `Default` implementation ensures that the component has valid shaders and initial dimensions before it attaches to the render context. ### Step 3 — Render Pass, Vertex Data, Buffers, and Pipeline + Step 3 implements `on_attach` to create the render pass, vertex and index data, GPU buffers, and the render pipeline, then attaches them to the `RenderContext`. ```rust @@ -328,6 +333,7 @@ fn on_attach( The pipeline uses the order of `with_buffer` calls to assign vertex buffer slots. The first buffer occupies slot `0` and provides attributes at location `0`, while the second buffer occupies slot `1` and provides attributes at location `1`. The component stores attached resource identifiers and the index count for use during rendering. ### Step 4 — Resize Handling and Updates + Step 4 wires window resize events into the component and implements detach and update hooks. The resize handler keeps `width` and `height` in sync with the window so that the viewport matches the surface size. ```rust @@ -368,6 +374,7 @@ fn on_update( The resize path is the only dynamic input in this example. The update hook is a no-op that keeps the component interface aligned with other examples. ### Step 5 — Render Commands and Runtime Entry Point + Step 5 records the render commands that bind the pipeline, vertex buffers, and index buffer, and then wires the component into the runtime as a windowed application. ```rust diff --git a/docs/tutorials/instanced-quads.md b/docs/tutorials/instanced-quads.md index 936daefb..ff0a4ca3 100644 --- a/docs/tutorials/instanced-quads.md +++ b/docs/tutorials/instanced-quads.md @@ -16,6 +16,7 @@ tags: ["tutorial", "graphics", "instancing", "vertex-buffers", "rust", "wgpu"] --- ## Overview + This tutorial builds an instanced rendering example using the `lambda-rs` crate. The final application renders a grid of 2D quads that all share the same geometry but read per-instance offsets and colors from a second vertex buffer. The example demonstrates how to configure per-vertex and per-instance buffers, construct an instanced render pipeline, and issue draw commands with a multi-instance range. Reference implementation: `crates/lambda-rs/examples/instanced_quads.rs`. @@ -69,6 +70,7 @@ Render Pass ## Implementation Steps ### Step 1 — Shaders and Attribute Layout + Step 1 defines the vertex and fragment shaders for instanced quads. The vertex shader consumes per-vertex positions and per-instance offsets and colors, and the fragment shader writes the interpolated color. ```glsl @@ -101,6 +103,7 @@ void main() { Attribute locations `0`, `1`, and `2` correspond to pipeline vertex attribute definitions for the per-vertex position and the per-instance offset and color. These locations will be matched by `VertexAttribute` entries when the render pipeline is constructed. ### Step 2 — Vertex and Instance Types and Component State + Step 2 introduces the Rust vertex and instance structures and prepares the component state. The component stores compiled shaders and identifiers for the render pass, pipeline, and buffers. ```rust @@ -210,6 +213,7 @@ impl Default for InstancedQuadsExample { The `QuadVertex` and `InstanceData` structures mirror the GLSL inputs as arrays of `f32`, and the component tracks resource identifiers and counts that are populated during attachment. The `Default` implementation constructs shader objects from the GLSL source so that the component is ready to build a pipeline when it receives a `RenderContext`. ### Step 3 — Render Pass, Geometry, Instances, and Buffers + Step 3 implements the `on_attach` method for the component. This method creates the render pass, quad geometry, instance data, GPU buffers, and the render pipeline. It also records the number of indices and instances for use during rendering. ```rust @@ -352,6 +356,7 @@ fn on_attach( The first buffer created by `with_buffer` is treated as a per-vertex buffer in slot `0`, while `with_instance_buffer` registers the instance buffer in slot `1` with per-instance step mode. The `vertex_attributes` and `instance_attributes` vectors connect shader locations `0`, `1`, and `2` to their corresponding buffer slots and formats, and the component records index and instance counts for later draws. The effective byte offset of each attribute is computed as `attribute.offset + attribute.element.offset`. In this example `attribute.offset` is kept at `0` for all attributes, and the struct layout is expressed entirely through `VertexElement::offset` (for example, the `color` field in `InstanceData` starts 12 bytes after the `offset` field). More complex layouts MAY use a non-zero `attribute.offset` to reuse the same attribute description at different base positions within a vertex or instance element. ### Step 4 — Resize Handling and Updates + Step 4 wires window resize events into the component and implements detach and update hooks. The resize handler keeps `width` and `height` in sync with the window so that the viewport matches the surface size. ```rust @@ -393,6 +398,7 @@ fn on_update( The component does not modify instance data over time, so `on_update` is a no-op. The resize path is the only dynamic input and ensures that the viewport used during rendering matches the current window size. ### Step 5 — Render Commands and Runtime Entry Point + Step 5 records the render commands that bind the pipeline, vertex buffers, and index buffer, then wires the component into the `lambda-rs` runtime as a windowed application. ```rust diff --git a/docs/tutorials/reflective-room.md b/docs/tutorials/reflective-room.md index 847a1754..8e51f6ae 100644 --- a/docs/tutorials/reflective-room.md +++ b/docs/tutorials/reflective-room.md @@ -16,11 +16,13 @@ tags: ["tutorial", "graphics", "stencil", "depth", "msaa", "mirror", "3d", "push --- ## Overview + This tutorial builds a reflective floor using the stencil buffer with an optional depth test and 4× multi‑sample anti‑aliasing (MSAA). The scene renders in four phases: a floor mask into stencil, a mirrored cube clipped by the mask, a translucent lit floor surface, and a normal cube above the plane. The camera looks down at a moderate angle so the reflection is clearly visible. Reference implementation: `crates/lambda-rs/examples/reflective_room.rs`. ## Table of Contents + - [Overview](#overview) - [Goals](#goals) - [Prerequisites](#prerequisites) @@ -53,10 +55,12 @@ Reference implementation: `crates/lambda-rs/examples/reflective_room.rs`. - Provide runtime toggles for MSAA, stencil, and depth testing, plus camera pitch and visibility helpers. ## Prerequisites + - Build the workspace: `cargo build --workspace`. - Run an example to verify setup: `cargo run --example minimal`. ## Requirements and Constraints + - A pipeline that uses stencil state MUST render into a pass with a depth‑stencil attachment. Use `DepthFormat::Depth24PlusStencil8`. - The mask pass MUST disable depth writes and write stencil with `Replace` so the floor area becomes `1`. - The reflected cube pipeline MUST test stencil `Equal` against reference `1` and SHOULD set stencil write mask to `0x00`. @@ -88,6 +92,7 @@ Pass 4: Color — draw normal cube above the floor ## Implementation Steps ### Step 1 — Runtime and Component Skeleton + Define a `Component` that owns shaders, meshes, render passes, pipelines, window size, elapsed time, and user‑toggleable settings for MSAA, stencil, and depth testing. ```rust @@ -124,6 +129,7 @@ impl Component for ReflectiveRoomExample { /* lifecycle Narrative: The component stores GPU handles and toggles. When settings change, mark `needs_rebuild = true` and rebuild pipelines/passes on the next frame. ### Step 2 — Shaders and Push Constants + Use one vertex shader and two fragment shaders. The vertex shader expects push constants with two `mat4` values: the MVP and the model matrix, used to transform positions and rotate normals to world space. The floor fragment shader is lit and translucent so the reflection reads beneath it. ```glsl @@ -184,11 +190,13 @@ pub fn push_constants_to_words(pc: &PushConstant) -> &[u32] { ``` ### Step 3 — Meshes: Cube and Floor + Build a unit cube (36 vertices) with per‑face normals and a large XZ floor quad at `y = 0`. Provide matching vertex attributes for position and normal at locations 0 and 1. Reference: `crates/lambda-rs/examples/reflective_room.rs:740` and `crates/lambda-rs/examples/reflective_room.rs:807`. ### Step 4 — Render Passes: Mask and Color + Create a depth/stencil‑only pass for the floor mask and a color pass for the scene. Use the same sample count on both. ```rust @@ -221,6 +229,7 @@ let pass_color = RenderPassBuilder::new() Rationale: pipelines that use stencil require a depth‑stencil attachment, even if depth testing is disabled. ### Step 5 — Pipeline: Floor Mask (Stencil Write) + Draw the floor geometry to write `stencil = 1` where the floor covers. Do not write to color. Disable depth writes and set depth compare to `Always`. ```rust @@ -250,6 +259,7 @@ let pipe_floor_mask = RenderPipelineBuilder::new() ``` ### Step 6 — Pipeline: Reflected Cube (Stencil Test) + Render the mirrored cube only where the floor mask is present. Mirroring flips the winding, so cull front faces for the reflected draw. Use `depth_compare = Always` and disable depth writes so the reflection remains visible; the stencil confines it to the floor. ```rust @@ -279,6 +289,7 @@ let pipe_reflected = builder.build( ``` ### Step 7 — Pipeline: Floor Visual (Tinted) + Draw the floor surface with a translucent tint so the reflection remains visible beneath. ```rust @@ -306,6 +317,7 @@ let pipe_floor_visual = floor_vis.build( ``` ### Step 8 — Pipeline: Normal Cube + Draw the unreflected cube above the floor using the lit fragment shader. Enable back‑face culling and depth testing when requested. ```rust @@ -333,6 +345,7 @@ let pipe_normal = normal.build( ``` ### Step 9 — Per‑Frame Transforms and Reflection + Compute camera, model rotation, and the mirror transform across the floor plane. The camera pitches downward and translates to a higher vantage point. Build the mirror using the plane‑reflection matrix `R = I − 2 n n^T` for a plane through the origin with unit normal `n` (for a flat floor, `n = (0,1,0)`). ```rust @@ -364,6 +377,7 @@ let mvp_reflect = projection.multiply(&view).multiply(&model_reflect); ``` ### Step 10 — Record Commands and Draw Order + Record commands in the following order. Set `viewport` and `scissor` to the window dimensions. ```rust @@ -404,6 +418,7 @@ cmds.push(RenderCommand::EndRenderPass); ``` ### Step 11 — Input, MSAA/Depth/Stencil Toggles, and Resize + Support runtime toggles to observe the impact of each setting: - `M` toggles MSAA between `1×` and `4×`. Rebuild passes and pipelines when it changes. diff --git a/docs/tutorials/textured-cube.md b/docs/tutorials/textured-cube.md index 319d81e0..37520655 100644 --- a/docs/tutorials/textured-cube.md +++ b/docs/tutorials/textured-cube.md @@ -16,11 +16,13 @@ tags: ["tutorial", "graphics", "3d", "push-constants", "textures", "samplers", " --- ## Overview + This tutorial builds a spinning 3D cube that uses push constants to provide model‑view‑projection (MVP) and model matrices to the vertex shader, and samples a 2D checkerboard texture in the fragment shader. Depth testing and back‑face culling are enabled so hidden faces do not render. Reference implementation: `crates/lambda-rs/examples/textured_cube.rs`. ## Table of Contents + - [Overview](#overview) - [Goals](#goals) - [Prerequisites](#prerequisites) @@ -51,10 +53,12 @@ Reference implementation: `crates/lambda-rs/examples/textured_cube.rs`. - Sample a 2D texture in the fragment stage using a separate sampler, and apply simple Lambert lighting to emphasize shape. ## Prerequisites + - Workspace builds: `cargo build --workspace`. - Run a quick example: `cargo run --example minimal`. ## Requirements and Constraints + - Push constant size and stage visibility MUST match the shader declaration. This example sends 128 bytes (two `mat4`), at vertex stage only. - The push constant byte order MUST match the shader’s expected matrix layout. This example transposes matrices before upload to match column‑major multiplication in GLSL. - Face winding MUST be counter‑clockwise (CCW) for back‑face culling to work with `CullingMode::Back`. @@ -83,6 +87,7 @@ Render Pass (depth enabled, back‑face culling) → Draw 36 vertices ## Implementation Steps ### Step 1 — Runtime and Component Skeleton + Create the application runtime and a `Component` that stores shader handles, GPU resource identifiers, window size, and elapsed time for animation. ```rust @@ -151,6 +156,7 @@ fn main() { This scaffold establishes the runtime and stores component state required to create resources and animate the cube. ### Step 2 — Shaders with Push Constants + Define GLSL 450 shaders. The vertex shader declares a push constant block with two `mat4` values: `mvp` and `model`. The fragment shader samples a 2D texture using a separate sampler and applies simple Lambert lighting for shape definition. ```glsl @@ -221,6 +227,7 @@ void main() { Compile these as `VirtualShader::Source` instances using `ShaderBuilder` during `on_attach` or `Default`. Keep the binding indices in the shader consistent with the Rust side. ### Step 3 — Cube Mesh and Vertex Layout + Build a unit cube centered at the origin. The following snippet uses a helper to add a face as two triangles with a shared normal. Attribute layout matches the shaders: location 0 = position, 1 = normal, 2 = color (unused). ```rust @@ -280,6 +287,7 @@ let mesh: Mesh = mesh_builder This produces 36 vertices (6 faces × 2 triangles × 3 vertices) with CCW winding and per‑face normals. ### Step 4 — Build a 2D Checkerboard Texture + Generate a simple grayscale checkerboard and upload it as an sRGB 2D texture. ```rust @@ -311,6 +319,7 @@ let texture2d = TextureBuilder::new_2d(TextureFormat::Rgba8UnormSrgb) Using `Rgba8UnormSrgb` ensures sampling converts from sRGB to linear space before shading. ### Step 5 — Create a Sampler + Create a linear filtering sampler with clamp‑to‑edge addressing. ```rust @@ -322,6 +331,7 @@ let sampler = SamplerBuilder::new() ``` ### Step 6 — Bind Group Layout and Bind Group + Declare the layout and bind the texture and sampler at set 0, bindings 1 and 2. ```rust @@ -340,6 +350,7 @@ let bind_group = BindGroupBuilder::new() ``` ### Step 7 — Render Pipeline with Depth and Culling + Enable depth, back‑face culling, and declare a vertex buffer built from the mesh. Add a push constant range for the vertex stage. ```rust @@ -386,6 +397,7 @@ self.bind_group = Some(render_context.attach_bind_group(bind_group)); ``` ### Step 8 — Per‑Frame Camera and Transforms + Compute yaw and pitch from elapsed time, build `model`, `view`, and perspective `projection`, then combine to an MVP matrix. Update `elapsed` in `on_update`. ```rust @@ -423,6 +435,7 @@ let mvp = projection.multiply(&view).multiply(&model); This multiplication order produces clip‑space positions as `mvp * vec4(position, 1)`. The final upload transposes matrices to match GLSL column‑major layout. ### Step 9 — Record Draw Commands with Push Constants + Define a push constant struct and a helper to reinterpret it as `[u32]`. Record commands to begin the pass, set pipeline state, bind the texture and sampler, push constants, and draw 36 vertices. ```rust @@ -474,6 +487,7 @@ let commands = vec![ ``` ### Step 10 — Handle Window Resize + Track window size from events so the projection and viewport use current dimensions. ```rust @@ -489,11 +503,13 @@ fn on_event(&mut self, event: Events) -> Result { ``` ## Validation + - Build the workspace: `cargo build --workspace` - Run the example: `cargo run -p lambda-rs --example textured_cube` - Expected behavior: a spinning cube shows a gray checkerboard on all faces, shaded by a directional light. Hidden faces do not render due to back‑face culling and depth testing. ## Notes + - Push constant limits: total size MUST be within the device’s push constant limit. This example uses 128 bytes, which fits common defaults. - Matrix layout: GLSL multiplies column‑major by default; transposing on upload aligns memory layout and multiplication order. - Normal transform: `mat3(model)` is correct when the model matrix contains only rotations and uniform scale. For non‑uniform scale, compute the normal matrix as the inverse‑transpose of the upper‑left 3×3. @@ -502,6 +518,7 @@ fn on_event(&mut self, event: Events) -> Result { - Indices: the cube uses non‑indexed vertices for clarity. An index buffer SHOULD be used for efficiency in production code. ## Conclusion + This tutorial delivered a rotating, textured cube with depth testing and back‑face culling. It compiled shaders that use a vertex push‑constant block for model‑view‑projection and model matrices, built a cube mesh and vertex @@ -511,10 +528,12 @@ constants, and draw commands were recorded. The result demonstrates push constants for per‑draw transforms alongside 2D sampling in a 3D render path. ## Putting It Together + - Full reference: `crates/lambda-rs/examples/textured_cube.rs` - The example includes logging in `on_attach` and uses the same builders and commands shown here. ## Exercises + - Exercise 1: Add roll - Add a Z‑axis rotation to the model matrix and verify culling remains correct. - Exercise 2: Nearest filtering @@ -531,6 +550,7 @@ constants for per‑draw transforms alongside 2D sampling in a 3D render path. - Bind two textures and blend per face based on projected UVs. ## Changelog + - 0.2.0 (2025-12-15): Update builder API calls to use `render_context.gpu()` and add `surface_format`/`depth_format` parameters to `RenderPassBuilder` and `RenderPipelineBuilder`. - 0.1.1 (2025-11-10): Add Conclusion section summarizing outcomes; update metadata and commit. - 0.1.0 (2025-11-10): Initial draft aligned with `crates/lambda-rs/examples/textured_cube.rs` including push constants, depth, culling, and projected UV sampling. diff --git a/docs/tutorials/textured-quad.md b/docs/tutorials/textured-quad.md index 24c7b1d0..9c4eee5c 100644 --- a/docs/tutorials/textured-quad.md +++ b/docs/tutorials/textured-quad.md @@ -16,11 +16,13 @@ tags: ["tutorial", "graphics", "textures", "samplers", "rust", "wgpu"] --- ## Overview + This tutorial builds a textured quad using a sampled 2D texture and sampler. It covers creating pixel data on the central processing unit (CPU), uploading to a graphics processing unit (GPU) texture, defining a sampler, wiring a bind group layout, and sampling the texture in the fragment shader. Reference implementation: `crates/lambda-rs/examples/textured_quad.rs`. ## Table of Contents + - [Overview](#overview) - [Goals](#goals) - [Prerequisites](#prerequisites) @@ -50,10 +52,12 @@ Reference implementation: `crates/lambda-rs/examples/textured_quad.rs`. - Create a `Texture` and `Sampler`, bind them in a layout, and draw using Lambda’s builders. ## Prerequisites + - Workspace builds: `cargo build --workspace`. - Run any example to verify setup: `cargo run --example minimal`. ## Requirements and Constraints + - Binding indices MUST match between Rust and shaders: set 0, binding 1 is the 2D texture; set 0, binding 2 is the sampler. - The example uses `TextureFormat::Rgba8UnormSrgb` so sampling converts from sRGB to linear space before shading. Rationale: produces correct color and filtering behavior for color images. - The CPU pixel buffer length MUST equal `width * height * 4` bytes for `Rgba8*` formats. @@ -75,6 +79,7 @@ Render pass -> SetPipeline -> SetBindGroup -> Draw (fragment samples) ## Implementation Steps ### Step 1 — Runtime and Component Skeleton + Create the application runtime and a `Component` that receives lifecycle callbacks and a render context for resource creation and command submission. @@ -171,6 +176,7 @@ fn main() { This scaffold establishes the runtime entry point and a component that participates in the engine lifecycle. The struct stores shader handles and placeholders for GPU resources that will be created during attachment. The `Default` implementation compiles inline GLSL into `Shader` objects up front so pipeline creation can proceed deterministically. At this stage the window is created and ready; no rendering occurs yet. ### Step 2 — Vertex and Fragment Shaders + Define GLSL 450 shaders. The vertex shader forwards UV to the fragment shader; the fragment samples `sampler2D(tex, samp)`. Place these constants near the top of `textured_quad.rs`: @@ -233,6 +239,7 @@ self.shader_fs = shader_fs; This compiles the virtual shaders to SPIR‑V using the engine’s shader builder and stores the resulting `Shader` objects on the component. The shaders are now ready for pipeline creation; drawing will begin only after a pipeline and render pass are created and attached. ### Step 3 — Mesh Data and Vertex Layout + Placement: on_attach. Define two triangles forming a quad. Pack UV into the vertex attribute at @@ -280,6 +287,7 @@ self.mesh = Some(mesh); This builds a quad from two triangles and declares the vertex attribute layout that the shaders consume. Positions map to location 0, normals to location 1, and UVs are encoded in the color field at location 2. The mesh currently resides on the CPU; a vertex buffer is created when building the pipeline. ### Step 4 — Build a 2D Texture (Checkerboard) + Placement: on_attach. Generate a simple checkerboard and upload it as an sRGB 2D texture. @@ -313,6 +321,7 @@ let texture = TextureBuilder::new_2d(TextureFormat::Rgba8UnormSrgb) This produces a GPU texture in `Rgba8UnormSrgb` format containing a checkerboard pattern. The builder uploads the CPU byte buffer and returns a handle suitable for binding. Using an sRGB color format ensures correct linearization during sampling in the fragment shader. ### Step 5 — Create a Sampler + Create a linear filtering sampler with clamp‑to‑edge addressing. ```rust @@ -327,6 +336,7 @@ let sampler = SamplerBuilder::new() This sampler selects linear minification and magnification with clamp‑to‑edge addressing. Linear filtering smooths the checkerboard when scaled, while clamping prevents wrapping at the texture borders. ### Step 6 — Bind Group Layout and Bind Group + Declare the layout and bind the texture and sampler at set 0, bindings 1 and 2. ```rust @@ -347,6 +357,7 @@ let bind_group = BindGroupBuilder::new() The bind group layout declares the shader‑visible interface for set 0: a sampled `texture2D` at binding 1 and a `sampler` at binding 2. The bind group then binds the concrete texture and sampler objects to those indices so the fragment shader can sample them during rendering. ### Step 7 — Create the Render Pipeline + Build a pipeline that consumes the mesh vertex buffer and the layout. Disable face culling for simplicity. ```rust @@ -392,6 +403,7 @@ self.bind_group = Some(render_context.attach_bind_group(bind_group)); The render pass targets the surface’s color attachment. The pipeline uses the compiled shaders, disables face culling for clarity, and declares a vertex buffer built from the mesh with attribute descriptors that match the shader locations. Attaching the pass, pipeline, and bind group to the render context yields stable `ResourceId`s that render commands will reference. ### Step 8 — Record Draw Commands + Center a square viewport inside the window, bind pipeline, bind group, and draw six vertices. ```rust @@ -429,6 +441,7 @@ let commands = vec![ These commands open a render pass with a centered square viewport, select the pipeline, bind the texture and sampler group at set 0, bind the vertex buffer at slot 0, draw six vertices, and end the pass. When submitted, they render a textured quad while preserving aspect ratio via the viewport. ### Step 9 — Handle Window Resize + Track window size from events and recompute the centered square viewport. ```rust @@ -446,12 +459,14 @@ fn on_event(&mut self, event: Events) -> Result { This event handler updates the stored window dimensions when a resize occurs. The render path uses these values to recompute the centered square viewport so the quad remains square and centered as the window changes size. ## Validation + - Build the workspace: `cargo build --workspace` - Run the example (workspace root): `cargo run --example textured_quad` - If needed, specify the package: `cargo run -p lambda-rs --example textured_quad` - Expected behavior: a centered square quad shows a gray checkerboard. Resizing the window preserves square aspect ratio by letterboxing with the viewport. With linear filtering, downscaling appears smooth. ## Notes + - sRGB vs linear formats: `Rgba8UnormSrgb` SHOULD be used for color images so sampling converts to linear space automatically. Use non‑sRGB formats (for example, `Rgba8Unorm`) for data textures like normal maps. - Binding indices: The `BindGroupLayout` and `BindGroup` indices MUST match shader `set` and `binding` qualifiers. Mismatches surface as validation errors. - Vertex attributes: Packing UV into the color slot is a simplification for the example. Defining a dedicated UV attribute at its own location is RECOMMENDED for production code. @@ -459,6 +474,7 @@ This event handler updates the stored window dimensions when a resize occurs. Th - Pipeline layout: Include all used layouts via `.with_layouts(...)` when creating the pipeline; otherwise binding state is incomplete at draw time. ## Conclusion + This tutorial implemented a complete 2D sampling path. It generated a checkerboard on the CPU, uploaded it as an sRGB texture, created a linear‑clamp sampler, and defined matching binding layouts. Shaders forwarded @@ -467,10 +483,12 @@ were recorded using a centered viewport. The result renders a textured quad with correct color space handling and filtering. ## Putting It Together + - Full reference: `crates/lambda-rs/examples/textured_quad.rs` - Minimal differences: the example includes empty `on_detach` and `on_update` hooks and a log line in `on_attach`. ## Exercises + - Exercise 1: Nearest filtering - Replace `linear_clamp()` with `nearest_clamp()` and observe sharper scaling. - Exercise 2: Repeat addressing @@ -485,6 +503,7 @@ with correct color space handling and filtering. - Discuss artifacts without mipmaps and how multiple levels would improve minification. ## Changelog + - 0.4.0 (2025-12-15): Update builder API calls to use `render_context.gpu()` and add `surface_format`/`depth_format` parameters to `RenderPassBuilder` and `RenderPipelineBuilder`. - 0.3.3 (2025-11-10): Add Conclusion section summarizing outcomes; update metadata and commit. - 0.3.2 (2025-11-10): Add narrative explanations after each code block; clarify lifecycle and binding flow. From 302f28c105941efefa36147694dd2b44becbb4bc Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 15 Dec 2025 13:44:34 -0800 Subject: [PATCH 24/28] [add] high level instance type to use for configuring render targets and building the GPU. --- crates/lambda-rs/src/render/gpu.rs | 13 +- crates/lambda-rs/src/render/instance.rs | 361 +++++++++++++++++++ crates/lambda-rs/src/render/mod.rs | 7 +- crates/lambda-rs/src/render/render_target.rs | 5 +- 4 files changed, 376 insertions(+), 10 deletions(-) create mode 100644 crates/lambda-rs/src/render/instance.rs diff --git a/crates/lambda-rs/src/render/gpu.rs b/crates/lambda-rs/src/render/gpu.rs index 82d0023f..7fad7a2e 100644 --- a/crates/lambda-rs/src/render/gpu.rs +++ b/crates/lambda-rs/src/render/gpu.rs @@ -23,9 +23,12 @@ use lambda_platform::wgpu as platform; -use super::texture::{ - DepthFormat, - TextureFormat, +use super::{ + instance::Instance, + texture::{ + DepthFormat, + TextureFormat, + }, }; // --------------------------------------------------------------------------- @@ -222,12 +225,12 @@ impl GpuBuilder { /// presentation. Pass `None` for headless/compute-only contexts. pub fn build( self, - instance: &platform::instance::Instance, + instance: &Instance, surface: Option<&platform::surface::Surface<'_>>, ) -> Result { let platform_gpu = self .inner - .build(instance, surface) + .build(instance.platform(), surface) .map_err(GpuBuildError::from_platform)?; return Ok(Gpu::from_platform(platform_gpu)); } diff --git a/crates/lambda-rs/src/render/instance.rs b/crates/lambda-rs/src/render/instance.rs new file mode 100644 index 00000000..87e423df --- /dev/null +++ b/crates/lambda-rs/src/render/instance.rs @@ -0,0 +1,361 @@ +//! High-level graphics instance abstraction. +//! +//! The `Instance` type wraps the platform instance, providing a stable +//! engine-facing API for instance creation and configuration. +//! +//! # Usage +//! +//! Create an instance using the builder pattern: +//! +//! ```ignore +//! let instance = InstanceBuilder::new() +//! .with_label("My Application") +//! .with_backends(Backends::PRIMARY) +//! .build(); +//! ``` +//! +//! The instance is then used to create surfaces and GPUs: +//! +//! ```ignore +//! let surface = WindowSurface::new(&instance, &window)?; +//! let gpu = GpuBuilder::new().build(&instance, Some(&surface))?; +//! ``` + +use lambda_platform::wgpu as platform; + +// --------------------------------------------------------------------------- +// Backends +// --------------------------------------------------------------------------- + +/// Graphics API backends available for rendering. +/// +/// This type mirrors the platform `Backends` bitset, exposing the same +/// options without leaking `wgpu` types to the engine layer. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Backends(platform::instance::Backends); + +impl Backends { + /// Primary desktop backends (Vulkan/Metal/DX12). + /// + /// This is the recommended default for cross-platform desktop applications. + pub const PRIMARY: Backends = Backends(platform::instance::Backends::PRIMARY); + + /// Vulkan backend (Linux, Windows, Android). + pub const VULKAN: Backends = Backends(platform::instance::Backends::VULKAN); + + /// Metal backend (macOS, iOS). + pub const METAL: Backends = Backends(platform::instance::Backends::METAL); + + /// DirectX 12 backend (Windows). + pub const DX12: Backends = Backends(platform::instance::Backends::DX12); + + /// OpenGL / WebGL backend. + pub const GL: Backends = Backends(platform::instance::Backends::GL); + + /// Browser WebGPU backend. + pub const BROWSER_WEBGPU: Backends = + Backends(platform::instance::Backends::BROWSER_WEBGPU); + + /// Convert to the platform representation for internal use. + #[inline] + pub(crate) fn to_platform(self) -> platform::instance::Backends { + return 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); + } +} + +// --------------------------------------------------------------------------- +// InstanceFlags +// --------------------------------------------------------------------------- + +/// Configuration flags for instance creation. +/// +/// These flags control debugging and validation behavior at the instance +/// level. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct InstanceFlags(platform::instance::InstanceFlags); + +impl InstanceFlags { + /// Enable validation layers for debugging. + /// + /// This enables GPU validation which can help catch errors but has + /// performance overhead. Recommended for development builds. + pub const VALIDATION: InstanceFlags = + InstanceFlags(platform::instance::InstanceFlags::VALIDATION); + + /// Enable additional debugging features. + /// + /// This enables extra debugging information where supported by the + /// graphics backend. + pub const DEBUG: InstanceFlags = + InstanceFlags(platform::instance::InstanceFlags::DEBUG); + + /// Convert to the platform representation for internal use. + #[inline] + pub(crate) fn to_platform(self) -> platform::instance::InstanceFlags { + return self.0; + } +} + +impl Default for InstanceFlags { + fn default() -> Self { + return InstanceFlags(platform::instance::InstanceFlags::default()); + } +} + +impl std::ops::BitOr for InstanceFlags { + type Output = InstanceFlags; + + fn bitor(self, rhs: InstanceFlags) -> InstanceFlags { + return InstanceFlags(self.0 | rhs.0); + } +} + +// --------------------------------------------------------------------------- +// Dx12Compiler +// --------------------------------------------------------------------------- + +/// DirectX 12 shader compiler selection (Windows only). +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum Dx12Compiler { + /// Use the FXC compiler (legacy, broadly compatible). + #[default] + Fxc, +} + +impl Dx12Compiler { + /// Convert to the platform representation for internal use. + #[inline] + pub(crate) fn to_platform(self) -> platform::instance::Dx12Compiler { + return match self { + Dx12Compiler::Fxc => platform::instance::Dx12Compiler::Fxc, + }; + } +} + +// --------------------------------------------------------------------------- +// Gles3MinorVersion +// --------------------------------------------------------------------------- + +/// OpenGL ES 3.x minor version selection. +/// +/// Used for WebGL and OpenGL ES targets to specify the required minor +/// version of the OpenGL ES 3.x API. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum Gles3MinorVersion { + /// Let the platform select an appropriate version. + #[default] + Automatic, + /// OpenGL ES 3.0 + Version0, + /// OpenGL ES 3.1 + Version1, + /// OpenGL ES 3.2 + Version2, +} + +impl Gles3MinorVersion { + /// Convert to the platform representation for internal use. + #[inline] + pub(crate) fn to_platform(self) -> platform::instance::Gles3MinorVersion { + return match self { + Gles3MinorVersion::Automatic => { + platform::instance::Gles3MinorVersion::Automatic + } + Gles3MinorVersion::Version0 => { + platform::instance::Gles3MinorVersion::Version0 + } + Gles3MinorVersion::Version1 => { + platform::instance::Gles3MinorVersion::Version1 + } + Gles3MinorVersion::Version2 => { + platform::instance::Gles3MinorVersion::Version2 + } + }; + } +} + +// --------------------------------------------------------------------------- +// Instance +// --------------------------------------------------------------------------- + +/// High-level graphics instance. +/// +/// The instance is the root object for the graphics subsystem. It manages +/// the connection to the graphics backend and is used to create surfaces +/// and enumerate adapters. +/// +/// Create an instance using `InstanceBuilder`: +/// +/// ```ignore +/// let instance = InstanceBuilder::new() +/// .with_label("My Application") +/// .build(); +/// ``` +pub struct Instance { + inner: platform::instance::Instance, +} + +impl Instance { + /// Return the optional label attached at construction time. + #[inline] + pub fn label(&self) -> Option<&str> { + return self.inner.label(); + } + + /// Borrow the underlying platform instance for internal use. + /// + /// This is crate-visible to allow surfaces and GPUs to access the + /// platform instance without exposing it publicly. + #[inline] + pub(crate) fn platform(&self) -> &platform::instance::Instance { + return &self.inner; + } +} + +impl std::fmt::Debug for Instance { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return f + .debug_struct("Instance") + .field("label", &self.inner.label()) + .finish_non_exhaustive(); + } +} + +// --------------------------------------------------------------------------- +// InstanceBuilder +// --------------------------------------------------------------------------- + +/// Builder for creating a graphics `Instance` with configurable options. +/// +/// The builder provides a fluent interface for configuring instance +/// creation parameters. All options have sensible defaults for desktop +/// applications. +/// +/// # Defaults +/// +/// - **Backends**: `Backends::PRIMARY` (Vulkan/Metal/DX12) +/// - **Flags**: Platform defaults (no validation) +/// +/// # Example +/// +/// ```ignore +/// let instance = InstanceBuilder::new() +/// .with_label("My Application Instance") +/// .with_backends(Backends::PRIMARY) +/// .with_flags(InstanceFlags::VALIDATION) +/// .build(); +/// ``` +pub struct InstanceBuilder { + inner: platform::instance::InstanceBuilder, +} + +impl InstanceBuilder { + /// Create a new builder with default settings. + pub fn new() -> Self { + return InstanceBuilder { + inner: platform::instance::InstanceBuilder::new(), + }; + } + + /// Attach a debug label to the instance. + /// + /// Labels appear in debug output and profiling tools, making it easier + /// to identify resources during development. + pub fn with_label(mut self, label: &str) -> Self { + self.inner = self.inner.with_label(label); + return self; + } + + /// Select which graphics backends to enable. + /// + /// Multiple backends can be combined using the `|` operator. The runtime + /// will select the best available backend from the enabled set. + /// + /// # Example + /// + /// ```ignore + /// // Enable Vulkan and Metal + /// builder.with_backends(Backends::VULKAN | Backends::METAL) + /// ``` + pub fn with_backends(mut self, backends: Backends) -> Self { + self.inner = self.inner.with_backends(backends.to_platform()); + return self; + } + + /// Set instance flags for debugging and validation. + /// + /// Validation is recommended during development but has performance + /// overhead and should be disabled in release builds. + pub fn with_flags(mut self, flags: InstanceFlags) -> Self { + self.inner = self.inner.with_flags(flags.to_platform()); + return self; + } + + /// Choose a DX12 shader compiler variant (Windows only). + /// + /// This option only affects DirectX 12 backends on Windows. + pub fn with_dx12_shader_compiler(mut self, compiler: Dx12Compiler) -> Self { + self.inner = self.inner.with_dx12_shader_compiler(compiler.to_platform()); + return self; + } + + /// Configure the GLES minor version for WebGL/OpenGL ES targets. + /// + /// This option only affects OpenGL and WebGL backends. + pub fn with_gles_minor_version(mut self, version: Gles3MinorVersion) -> Self { + self.inner = self.inner.with_gles_minor_version(version.to_platform()); + return self; + } + + /// Build the `Instance` from the accumulated options. + pub fn build(self) -> Instance { + return Instance { + inner: self.inner.build(), + }; + } +} + +impl Default for InstanceBuilder { + fn default() -> Self { + return Self::new(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn instance_builder_sets_label() { + let instance = InstanceBuilder::new().with_label("Test Instance").build(); + assert_eq!(instance.label(), Some("Test Instance")); + } + + #[test] + fn instance_builder_default_backends() { + // Just ensure we can build with defaults without panicking + let _instance = InstanceBuilder::new().build(); + } + + #[test] + fn backends_bitor() { + let combined = Backends::VULKAN | Backends::METAL; + // Verify the operation doesn't panic and produces a valid result + assert_ne!(combined, Backends::VULKAN); + assert_ne!(combined, Backends::METAL); + } +} diff --git a/crates/lambda-rs/src/render/mod.rs b/crates/lambda-rs/src/render/mod.rs index 625c2538..247d351b 100644 --- a/crates/lambda-rs/src/render/mod.rs +++ b/crates/lambda-rs/src/render/mod.rs @@ -34,6 +34,7 @@ pub mod buffer; pub mod command; pub mod encoder; pub mod gpu; +pub mod instance; pub mod mesh; pub mod pipeline; pub mod render_pass; @@ -117,13 +118,13 @@ impl RenderContextBuilder { ) -> Result { let RenderContextBuilder { name, .. } = self; - let instance = platform::instance::InstanceBuilder::new() + let instance = instance::InstanceBuilder::new() .with_label(&format!("{} Instance", name)) .build(); let mut surface = platform::surface::SurfaceBuilder::new() .with_label(&format!("{} Surface", name)) - .build(&instance, window.window_handle()) + .build(instance.platform(), window.window_handle()) .map_err(|e| { RenderContextError::SurfaceCreate(format!( "Failed to create rendering surface: {:?}", @@ -208,7 +209,7 @@ impl RenderContextBuilder { /// reconfiguration with preserved present mode and usage. pub struct RenderContext { label: String, - instance: platform::instance::Instance, + instance: instance::Instance, surface: platform::surface::Surface<'static>, gpu: gpu::Gpu, config: surface::SurfaceConfig, diff --git a/crates/lambda-rs/src/render/render_target.rs b/crates/lambda-rs/src/render/render_target.rs index a53be7a3..d5f2a55f 100644 --- a/crates/lambda-rs/src/render/render_target.rs +++ b/crates/lambda-rs/src/render/render_target.rs @@ -20,6 +20,7 @@ use lambda_platform::wgpu as platform; use super::{ gpu::Gpu, + instance::Instance, surface::{ Frame, PresentMode, @@ -86,12 +87,12 @@ impl WindowSurface { /// The surface must be configured before use by calling /// `configure_with_defaults` or `resize`. pub fn new( - instance: &platform::instance::Instance, + instance: &Instance, window: &Window, ) -> Result { let surface = platform::surface::SurfaceBuilder::new() .with_label("Lambda Window Surface") - .build(instance, window.window_handle()) + .build(instance.platform(), window.window_handle()) .map_err(|_| { WindowSurfaceError::CreationFailed( "Failed to create window surface".to_string(), From af2f7f3d5bd58b79ddb9d33faa38b8382fca26f3 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 15 Dec 2025 13:59:47 -0800 Subject: [PATCH 25/28] [update] gpu and render context to use the window surface. --- crates/lambda-rs/src/render/gpu.rs | 6 ++-- crates/lambda-rs/src/render/mod.rs | 48 +++++++++++++----------------- 2 files changed, 25 insertions(+), 29 deletions(-) diff --git a/crates/lambda-rs/src/render/gpu.rs b/crates/lambda-rs/src/render/gpu.rs index 7fad7a2e..7cbb22a2 100644 --- a/crates/lambda-rs/src/render/gpu.rs +++ b/crates/lambda-rs/src/render/gpu.rs @@ -25,6 +25,7 @@ use lambda_platform::wgpu as platform; use super::{ instance::Instance, + render_target::WindowSurface, texture::{ DepthFormat, TextureFormat, @@ -226,11 +227,12 @@ impl GpuBuilder { pub fn build( self, instance: &Instance, - surface: Option<&platform::surface::Surface<'_>>, + surface: Option<&WindowSurface>, ) -> Result { + let platform_surface = surface.map(|s| s.platform()); let platform_gpu = self .inner - .build(instance.platform(), surface) + .build(instance.platform(), platform_surface) .map_err(GpuBuildError::from_platform)?; return Ok(Gpu::from_platform(platform_gpu)); } diff --git a/crates/lambda-rs/src/render/mod.rs b/crates/lambda-rs/src/render/mod.rs index 247d351b..5b3ae56e 100644 --- a/crates/lambda-rs/src/render/mod.rs +++ b/crates/lambda-rs/src/render/mod.rs @@ -67,6 +67,7 @@ use self::{ }, pipeline::RenderPipeline, render_pass::RenderPass as RenderPassDesc, + render_target::RenderTarget, }; /// Builder for configuring a `RenderContext` tied to one window. @@ -122,9 +123,7 @@ impl RenderContextBuilder { .with_label(&format!("{} Instance", name)) .build(); - let mut surface = platform::surface::SurfaceBuilder::new() - .with_label(&format!("{} Surface", name)) - .build(instance.platform(), window.window_handle()) + let mut surface = render_target::WindowSurface::new(&instance, window) .map_err(|e| { RenderContextError::SurfaceCreate(format!( "Failed to create rendering surface: {:?}", @@ -145,10 +144,10 @@ impl RenderContextBuilder { let size = window.dimensions(); surface .configure_with_defaults( - gpu.platform(), + &gpu, size, - surface::PresentMode::default().to_platform(), - texture::TextureUsages::RENDER_ATTACHMENT.to_platform(), + surface::PresentMode::default(), + texture::TextureUsages::RENDER_ATTACHMENT, ) .map_err(|e| { RenderContextError::SurfaceConfig(format!( @@ -162,8 +161,8 @@ impl RenderContextBuilder { "Surface was not configured".to_string(), ) })?; - let config = surface::SurfaceConfig::from_platform(config); let texture_usage = config.usage; + let config = config.clone(); // Initialize a depth texture matching the surface size. let depth_format = texture::DepthFormat::Depth32Float; @@ -210,7 +209,7 @@ impl RenderContextBuilder { pub struct RenderContext { label: String, instance: instance::Instance, - surface: platform::surface::Surface<'static>, + surface: render_target::WindowSurface, gpu: gpu::Gpu, config: surface::SurfaceConfig, texture_usage: texture::TextureUsages, @@ -443,22 +442,18 @@ impl RenderContext { return Ok(()); } - let frame = match self.surface.acquire_next_frame() { - Ok(frame) => surface::Frame::from_platform(frame), - Err(err) => { - let high_level_err = surface::SurfaceError::from(err); - match high_level_err { - surface::SurfaceError::Lost | surface::SurfaceError::Outdated => { - self.reconfigure_surface(self.size)?; - let platform_frame = - self.surface.acquire_next_frame().map_err(|e| { - RenderError::Surface(surface::SurfaceError::from(e)) - })?; - surface::Frame::from_platform(platform_frame) - } - _ => return Err(RenderError::Surface(high_level_err)), + let frame = match self.surface.acquire_frame() { + Ok(frame) => frame, + Err(err) => match err { + surface::SurfaceError::Lost | surface::SurfaceError::Outdated => { + self.reconfigure_surface(self.size)?; + self + .surface + .acquire_frame() + .map_err(|e| RenderError::Surface(e))? } - } + _ => return Err(RenderError::Surface(err)), + }, }; let view = frame.texture_view(); @@ -714,16 +709,15 @@ impl RenderContext { ) -> Result<(), RenderError> { self .surface - .resize(self.gpu.platform(), size) + .resize(&self.gpu, size) .map_err(RenderError::Configuration)?; - let platform_config = self.surface.configuration().ok_or_else(|| { + let config = self.surface.configuration().ok_or_else(|| { RenderError::Configuration("Surface was not configured".to_string()) })?; - let config = surface::SurfaceConfig::from_platform(platform_config); self.texture_usage = config.usage; - self.config = config; + self.config = config.clone(); return Ok(()); } From 68f96e8cc00d3d3b615dbf9ba7a2439702984378 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 15 Dec 2025 14:02:44 -0800 Subject: [PATCH 26/28] [update] from_platform to be private. --- crates/lambda-rs/src/render/gpu.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/lambda-rs/src/render/gpu.rs b/crates/lambda-rs/src/render/gpu.rs index 7cbb22a2..3e8cf053 100644 --- a/crates/lambda-rs/src/render/gpu.rs +++ b/crates/lambda-rs/src/render/gpu.rs @@ -89,7 +89,7 @@ pub struct Gpu { impl Gpu { /// Create a new high-level GPU from a platform GPU. - pub(crate) fn from_platform(gpu: platform::gpu::Gpu) -> Self { + fn from_platform(gpu: platform::gpu::Gpu) -> Self { let limits = GpuLimits::from_platform(gpu.limits()); return Gpu { inner: gpu, limits }; } From 7b6edbfec93ff3d4c5efcf10a5c0cab9ebbbfac8 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 15 Dec 2025 14:14:55 -0800 Subject: [PATCH 27/28] [fix] into_raw to only be visible to the crate. --- crates/lambda-rs-platform/src/wgpu/command.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/lambda-rs-platform/src/wgpu/command.rs b/crates/lambda-rs-platform/src/wgpu/command.rs index ae675dcd..d4957252 100644 --- a/crates/lambda-rs-platform/src/wgpu/command.rs +++ b/crates/lambda-rs-platform/src/wgpu/command.rs @@ -18,7 +18,7 @@ pub struct CommandBuffer { impl CommandBuffer { /// Convert to the raw wgpu command buffer. - pub fn into_raw(self) -> wgpu::CommandBuffer { + pub(crate) fn into_raw(self) -> wgpu::CommandBuffer { self.raw } } From 3de031214570c1a81564e22583382c204a1707b2 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 15 Dec 2025 14:22:10 -0800 Subject: [PATCH 28/28] [fix] obj-loader. --- tools/obj_loader/src/main.rs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/tools/obj_loader/src/main.rs b/tools/obj_loader/src/main.rs index 566810c2..8c154d9f 100644 --- a/tools/obj_loader/src/main.rs +++ b/tools/obj_loader/src/main.rs @@ -10,7 +10,6 @@ use args::{ use lambda::{ component::Component, events::{ - ComponentEvent, Events, WindowEvent, }, @@ -37,11 +36,6 @@ use lambda::{ ShaderKind, VirtualShader, }, - vertex::{ - Vertex, - VertexAttribute, - VertexElement, - }, viewport, ResourceId, }, @@ -197,7 +191,12 @@ impl Component for ObjLoader { &mut self, render_context: &mut lambda::render::RenderContext, ) -> Result { - let render_pass = RenderPassBuilder::new().build(render_context); + let gpu = render_context.gpu(); + let surface_format = render_context.surface_format(); + let depth_format = render_context.depth_format(); + + let render_pass = + RenderPassBuilder::new().build(gpu, surface_format, depth_format); let push_constant_size = std::mem::size_of::() as u32; let mesh = MeshBuilder::new().build_from_obj(&self.obj_path); @@ -211,12 +210,14 @@ impl Component for ObjLoader { let pipeline = RenderPipelineBuilder::new() .with_push_constant(PipelineStage::VERTEX, push_constant_size) .with_buffer( - BufferBuilder::build_from_mesh(&mesh, render_context) + BufferBuilder::build_from_mesh(&mesh, gpu) .expect("Failed to create buffer"), mesh.attributes().to_vec(), ) .build( - render_context, + gpu, + surface_format, + depth_format, &render_pass, &self.vertex_shader, Some(&self.fragment_shader),