From 4a1f2eb69b0cc7739ea102514c1f57272a8694de Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Mon, 16 Feb 2026 17:38:05 +0000 Subject: [PATCH 01/49] InoxRenderer should always be mutable in its self methods. --- inox2d-opengl/src/lib.rs | 14 ++++++------- inox2d/src/render.rs | 43 ++++++++++++++++++++++++++++------------ 2 files changed, 37 insertions(+), 20 deletions(-) diff --git a/inox2d-opengl/src/lib.rs b/inox2d-opengl/src/lib.rs index 6e12b42..3234991 100644 --- a/inox2d-opengl/src/lib.rs +++ b/inox2d-opengl/src/lib.rs @@ -410,7 +410,7 @@ impl OpenglRenderer { } impl InoxRenderer for OpenglRenderer { - fn on_begin_masks(&self, masks: &Masks) { + fn on_begin_masks(&mut self, masks: &Masks) { self.push_debug_group("inox2d - begin masks"); let gl = &self.gl; @@ -432,7 +432,7 @@ impl InoxRenderer for OpenglRenderer { self.pop_debug_group(); } - fn on_begin_mask(&self, mask: &Mask) { + fn on_begin_mask(&mut self, mask: &Mask) { self.push_debug_group("inox2d - begin mask"); let gl = &self.gl; @@ -441,7 +441,7 @@ impl InoxRenderer for OpenglRenderer { } } - fn on_begin_masked_content(&self) { + fn on_begin_masked_content(&mut self) { self.push_debug_group("inox2d - begin masked content"); let gl = &self.gl; @@ -455,7 +455,7 @@ impl InoxRenderer for OpenglRenderer { self.pop_debug_group(); } - fn on_end_mask(&self) { + fn on_end_mask(&mut self) { let gl = &self.gl; unsafe { gl.stencil_mask(0xff); @@ -467,7 +467,7 @@ impl InoxRenderer for OpenglRenderer { } fn draw_textured_mesh_content( - &self, + &mut self, as_mask: bool, components: &TexturedMeshComponents, render_ctx: &TexturedMeshRenderCtx, @@ -533,7 +533,7 @@ impl InoxRenderer for OpenglRenderer { } fn begin_composite_content( - &self, + &mut self, _as_mask: bool, _components: &CompositeComponents, _render_ctx: &CompositeRenderCtx, @@ -566,7 +566,7 @@ impl InoxRenderer for OpenglRenderer { } fn finish_composite_content( - &self, + &mut self, as_mask: bool, components: &CompositeComponents, _render_ctx: &CompositeRenderCtx, diff --git a/inox2d/src/render.rs b/inox2d/src/render.rs index c77fe74..ecfa453 100644 --- a/inox2d/src/render.rs +++ b/inox2d/src/render.rs @@ -3,6 +3,7 @@ mod vertex_buffers; use std::collections::HashSet; use std::mem::swap; +use std::error::Error; use crate::node::{ components::{DeformStack, Mask, Masks, ZSort}, @@ -215,25 +216,33 @@ impl RenderCtx { /// /// Either way, the point is Inox2D will implement a `draw()` method for any `impl InoxRenderer`, dispatching calls based on puppet structure according to Inochi2D standard. pub trait InoxRenderer { + /// Begin rendering a whole puppet. + /// + /// Calls to `begin_render()` and `end_render_and_flush()` must be + /// balanced. Failure to balance these calls will result in a panic. + fn begin_render(&mut self) -> Result<(), Box> { + Ok(()) + } + /// Begin masking. /// /// Ref impl: Clear and start writing to the stencil buffer, lock the color buffer. - fn on_begin_masks(&self, masks: &Masks); + fn on_begin_masks(&mut self, masks: &Masks); /// Get prepared for rendering a singular Mask. - fn on_begin_mask(&self, mask: &Mask); + fn on_begin_mask(&mut self, mask: &Mask); /// Get prepared for rendering masked content. /// /// Ref impl: Read only from the stencil buffer, unlock the color buffer. - fn on_begin_masked_content(&self); + fn on_begin_masked_content(&mut self); /// End masking. /// /// Ref impl: Disable the stencil buffer. - fn on_end_mask(&self); + fn on_end_mask(&mut self); /// Draw TexturedMesh content. // TODO: TexturedMesh without any texture (usually for mesh masks)? fn draw_textured_mesh_content( - &self, + &mut self, as_mask: bool, components: &TexturedMeshComponents, render_ctx: &TexturedMeshRenderCtx, @@ -244,7 +253,7 @@ pub trait InoxRenderer { /// /// Ref impl: Prepare composite buffers. fn begin_composite_content( - &self, + &mut self, as_mask: bool, components: &CompositeComponents, render_ctx: &CompositeRenderCtx, @@ -254,30 +263,34 @@ pub trait InoxRenderer { /// /// Ref impl: Transfer content from composite buffers to normal buffers. fn finish_composite_content( - &self, + &mut self, as_mask: bool, components: &CompositeComponents, render_ctx: &CompositeRenderCtx, id: InoxNodeUuid, ); + + /// Finish rendering, flush any pending operations, and present the puppet + /// to the given display. + fn end_render_and_flush(&mut self) {} } pub trait InoxRendererExt { /// Draw a Drawable, which is potentially masked. - fn draw_drawable(&self, as_mask: bool, comps: &World, id: InoxNodeUuid); + fn draw_drawable(&mut self, as_mask: bool, comps: &World, id: InoxNodeUuid); /// Draw one composite. `components` must be referencing `comps`. - fn draw_composite(&self, as_mask: bool, comps: &World, components: &CompositeComponents, id: InoxNodeUuid); + fn draw_composite(&mut self, as_mask: bool, comps: &World, components: &CompositeComponents, id: InoxNodeUuid); /// Iterate over top-level drawables (excluding masks) in zsort order, /// and make draw calls correspondingly. /// /// This effectively draws the complete puppet. - fn draw(&self, puppet: &Puppet); + fn draw(&mut self, puppet: &Puppet); } impl InoxRendererExt for T { - fn draw_drawable(&self, as_mask: bool, comps: &World, id: InoxNodeUuid) { + fn draw_drawable(&mut self, as_mask: bool, comps: &World, id: InoxNodeUuid) { let drawable_kind = DrawableKind::new(id, comps, false).expect("Node must be a Drawable."); let masks = match drawable_kind { DrawableKind::TexturedMesh(ref components) => &components.drawable.masks, @@ -308,7 +321,7 @@ impl InoxRendererExt for T { } } - fn draw_composite(&self, as_mask: bool, comps: &World, components: &CompositeComponents, id: InoxNodeUuid) { + fn draw_composite(&mut self, as_mask: bool, comps: &World, components: &CompositeComponents, id: InoxNodeUuid) { let render_ctx = comps.get::(id).unwrap(); if render_ctx.zsorted_children_list.is_empty() { // Optimization: Nothing to be drawn, skip context switching @@ -341,7 +354,9 @@ impl InoxRendererExt for T { /// For example, maybe the caller still need to transfer content from a texture buffer to the screen surface buffer. /// - The provided `InoxRender` implementation is wrong. /// - `puppet` here does not belong to the `model` this `renderer` is initialized with. This will likely result in panics for non-existent node uuids. - fn draw(&self, puppet: &Puppet) { + fn draw(&mut self, puppet: &Puppet) { + self.begin_render(); + for uuid in &puppet .render_ctx .as_ref() @@ -350,5 +365,7 @@ impl InoxRendererExt for T { { self.draw_drawable(false, &puppet.node_comps, *uuid); } + + self.end_render_and_flush(); } } From 96f52c0bb3a30dccdd83ddfe19a820338a1f4c59 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Mon, 23 Feb 2026 01:11:44 +0000 Subject: [PATCH 02/49] Make `on_begin_draw`/`on_end_draw` official InoxRenderer trait methods. --- examples/render-opengl/src/main.rs | 4 +- inox2d-opengl/src/lib.rs | 69 +++++++++++++++--------------- inox2d/src/render.rs | 18 ++++---- 3 files changed, 45 insertions(+), 46 deletions(-) diff --git a/examples/render-opengl/src/main.rs b/examples/render-opengl/src/main.rs index 3409136..c744f03 100644 --- a/examples/render-opengl/src/main.rs +++ b/examples/render-opengl/src/main.rs @@ -135,9 +135,7 @@ impl App for Inox2dOpenglExampleApp { // Just that physics simulation will run for the provided time, which may be big and causes a startup delay. puppet.end_frame(scene_ctrl.dt()); - renderer.on_begin_draw(puppet); - renderer.draw(puppet); - renderer.on_end_draw(puppet); + renderer.draw(puppet).expect("successful draw"); } fn handle_window_event(&mut self, event: WindowEvent, elwt: &EventLoopWindowTarget<()>) { diff --git a/inox2d-opengl/src/lib.rs b/inox2d-opengl/src/lib.rs index 3234991..193ae63 100644 --- a/inox2d-opengl/src/lib.rs +++ b/inox2d-opengl/src/lib.rs @@ -6,6 +6,7 @@ pub mod texture; use std::cell::RefCell; use std::mem; use std::ops::Deref; +use std::error::Error; use glam::{uvec2, UVec2, Vec3}; use glow::HasContext; @@ -410,6 +411,39 @@ impl OpenglRenderer { } impl InoxRenderer for OpenglRenderer { + /// Update the renderer with latest puppet data. + fn on_begin_draw(&mut self, puppet: &Puppet) -> Result<(), Box> { + self.push_debug_group("inox2d - begin draw"); + + let gl = &self.gl; + + // TODO: calculate this matrix only once per draw pass. + // let matrix = self.camera.matrix(self.viewport.as_vec2()); + + unsafe { + gl.bind_vertex_array(Some(self.vao)); + upload_deforms_to_gl( + gl, + puppet + .render_ctx + .as_ref() + .expect("Rendering for a puppet must be initialized by now.") + .vertex_buffers + .deforms + .as_slice(), + self.deform_buffer, + ); + gl.enable(glow::BLEND); + gl.disable(glow::DEPTH_TEST); + } + + self.pop_debug_group(); + + self.push_debug_group("inox2d - draw"); + + Ok(()) + } + fn on_begin_masks(&mut self, masks: &Masks) { self.push_debug_group("inox2d - begin masks"); @@ -621,42 +655,9 @@ impl InoxRenderer for OpenglRenderer { self.pop_debug_group(); } -} - -impl OpenglRenderer { - /// Update the renderer with latest puppet data. - pub fn on_begin_draw(&self, puppet: &Puppet) { - self.push_debug_group("inox2d - begin draw"); - - let gl = &self.gl; - - // TODO: calculate this matrix only once per draw pass. - // let matrix = self.camera.matrix(self.viewport.as_vec2()); - - unsafe { - gl.bind_vertex_array(Some(self.vao)); - upload_deforms_to_gl( - gl, - puppet - .render_ctx - .as_ref() - .expect("Rendering for a puppet must be initialized by now.") - .vertex_buffers - .deforms - .as_slice(), - self.deform_buffer, - ); - gl.enable(glow::BLEND); - gl.disable(glow::DEPTH_TEST); - } - - self.pop_debug_group(); - - self.push_debug_group("inox2d - draw"); - } /// Renderer cleaning up after one frame. - pub fn on_end_draw(&self, _puppet: &Puppet) { + fn on_end_draw(&mut self, _puppet: &Puppet) { self.pop_debug_group(); self.push_debug_group("inox2d - end draw"); diff --git a/inox2d/src/render.rs b/inox2d/src/render.rs index ecfa453..a0e46d8 100644 --- a/inox2d/src/render.rs +++ b/inox2d/src/render.rs @@ -218,11 +218,9 @@ impl RenderCtx { pub trait InoxRenderer { /// Begin rendering a whole puppet. /// - /// Calls to `begin_render()` and `end_render_and_flush()` must be + /// Calls to `on_begin_draw()` and `on_end_draw()` must be /// balanced. Failure to balance these calls will result in a panic. - fn begin_render(&mut self) -> Result<(), Box> { - Ok(()) - } + fn on_begin_draw(&mut self, puppet: &Puppet) -> Result<(), Box>; /// Begin masking. /// @@ -272,7 +270,7 @@ pub trait InoxRenderer { /// Finish rendering, flush any pending operations, and present the puppet /// to the given display. - fn end_render_and_flush(&mut self) {} + fn on_end_draw(&mut self, puppet: &Puppet); } pub trait InoxRendererExt { @@ -286,7 +284,7 @@ pub trait InoxRendererExt { /// and make draw calls correspondingly. /// /// This effectively draws the complete puppet. - fn draw(&mut self, puppet: &Puppet); + fn draw(&mut self, puppet: &Puppet) -> Result<(), Box>; } impl InoxRendererExt for T { @@ -354,8 +352,8 @@ impl InoxRendererExt for T { /// For example, maybe the caller still need to transfer content from a texture buffer to the screen surface buffer. /// - The provided `InoxRender` implementation is wrong. /// - `puppet` here does not belong to the `model` this `renderer` is initialized with. This will likely result in panics for non-existent node uuids. - fn draw(&mut self, puppet: &Puppet) { - self.begin_render(); + fn draw(&mut self, puppet: &Puppet) -> Result<(), Box> { + self.on_begin_draw(puppet)?; for uuid in &puppet .render_ctx @@ -366,6 +364,8 @@ impl InoxRendererExt for T { self.draw_drawable(false, &puppet.node_comps, *uuid); } - self.end_render_and_flush(); + self.on_end_draw(puppet); + + Ok(()) } } From 3e7968f857db76ff419421899f1e9ba13ef32268 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Tue, 24 Feb 2026 23:23:05 +0000 Subject: [PATCH 03/49] Break InoxRenderer into a separate renderer and draw-session trait. This ensures that: * The renderer does not have to hold empty space for temporaries that are only used during the draw process * `on_begin_draw` is always called before any drawing operations can occur * Drawing is always cleaned up, either in `on_end_draw` or `Drop` (our impls currently use `on_end_draw`) --- inox2d-opengl/src/lib.rs | 111 +++++++++++++++++++++------------------ inox2d/src/render.rs | 30 +++++++---- 2 files changed, 81 insertions(+), 60 deletions(-) diff --git a/inox2d-opengl/src/lib.rs b/inox2d-opengl/src/lib.rs index 193ae63..484d00c 100644 --- a/inox2d-opengl/src/lib.rs +++ b/inox2d-opengl/src/lib.rs @@ -4,9 +4,9 @@ mod shaders; pub mod texture; use std::cell::RefCell; +use std::error::Error; use std::mem; use std::ops::Deref; -use std::error::Error; use glam::{uvec2, UVec2, Vec3}; use glow::HasContext; @@ -19,7 +19,7 @@ use inox2d::node::{ InoxNodeUuid, }; use inox2d::puppet::Puppet; -use inox2d::render::{CompositeRenderCtx, InoxRenderer, TexturedMeshRenderCtx}; +use inox2d::render::{CompositeRenderCtx, DrawSession, InoxRenderer, TexturedMeshRenderCtx}; use inox2d::texture::{decode_model_textures, TextureId}; use self::shader::ShaderCompileError; @@ -411,8 +411,13 @@ impl OpenglRenderer { } impl InoxRenderer for OpenglRenderer { + type Draw<'a> + = OpenGlSession<'a> + where + Self: 'a; + /// Update the renderer with latest puppet data. - fn on_begin_draw(&mut self, puppet: &Puppet) -> Result<(), Box> { + fn on_begin_draw<'a>(&'a mut self, puppet: &Puppet) -> Result, Box> { self.push_debug_group("inox2d - begin draw"); let gl = &self.gl; @@ -441,13 +446,19 @@ impl InoxRenderer for OpenglRenderer { self.push_debug_group("inox2d - draw"); - Ok(()) + Ok(OpenGlSession { render: self }) } +} +pub struct OpenGlSession<'a> { + render: &'a mut OpenglRenderer, +} + +impl<'a> DrawSession<'a> for OpenGlSession<'a> { fn on_begin_masks(&mut self, masks: &Masks) { - self.push_debug_group("inox2d - begin masks"); + self.render.push_debug_group("inox2d - begin masks"); - let gl = &self.gl; + let gl = &self.render.gl; unsafe { gl.enable(glow::STENCIL_TEST); @@ -459,26 +470,26 @@ impl InoxRenderer for OpenglRenderer { gl.stencil_mask(0xff); } - let part_mask_shader = &self.part_mask_shader; - self.bind_shader(part_mask_shader); + let part_mask_shader = &self.render.part_mask_shader; + self.render.bind_shader(part_mask_shader); part_mask_shader.set_threshold(gl, masks.threshold.clamp(0.0, 1.0)); - self.pop_debug_group(); + self.render.pop_debug_group(); } fn on_begin_mask(&mut self, mask: &Mask) { - self.push_debug_group("inox2d - begin mask"); + self.render.push_debug_group("inox2d - begin mask"); - let gl = &self.gl; + let gl = &self.render.gl; unsafe { gl.stencil_func(glow::ALWAYS, (mask.mode == MaskMode::Mask) as i32, 0xff); } } fn on_begin_masked_content(&mut self) { - self.push_debug_group("inox2d - begin masked content"); + self.render.push_debug_group("inox2d - begin masked content"); - let gl = &self.gl; + let gl = &self.render.gl; unsafe { gl.stencil_func(glow::EQUAL, 1, 0xff); gl.stencil_mask(0x00); @@ -486,18 +497,18 @@ impl InoxRenderer for OpenglRenderer { gl.color_mask(true, true, true, true); } - self.pop_debug_group(); + self.render.pop_debug_group(); } fn on_end_mask(&mut self) { - let gl = &self.gl; + let gl = &self.render.gl; unsafe { gl.stencil_mask(0xff); gl.stencil_func(glow::ALWAYS, 1, 0xff); gl.disable(glow::STENCIL_TEST); } - - self.pop_debug_group(); + + self.render.pop_debug_group(); } fn draw_textured_mesh_content( @@ -507,9 +518,9 @@ impl InoxRenderer for OpenglRenderer { render_ctx: &TexturedMeshRenderCtx, _id: InoxNodeUuid, ) { - self.push_debug_group("inox2d - draw textured content"); + self.render.push_debug_group("inox2d - draw textured content"); - let gl = &self.gl; + let gl = &self.render.gl; // TODO: plain masks, meshes as masks without textures /* @@ -529,10 +540,10 @@ impl InoxRenderer for OpenglRenderer { glDisableVertexAttribArray(0); */ - self.bind_part_textures(components.texture); - self.set_blend_mode(components.drawable.blending.mode); + self.render.bind_part_textures(components.texture); + self.render.set_blend_mode(components.drawable.blending.mode); - let mvp = self.camera.matrix(self.viewport.as_vec2()) * *components.transform; + let mvp = self.render.camera.matrix(self.render.viewport.as_vec2()) * *components.transform; if as_mask { // if as_mask is set, in .on_begin_masks(): @@ -540,10 +551,10 @@ impl InoxRenderer for OpenglRenderer { // - mask threshold must have been uploaded. // vert uniforms - self.part_mask_shader.set_mvp(gl, mvp); + self.render.part_mask_shader.set_mvp(gl, mvp); } else { - let part_shader = &self.part_shader; - self.bind_shader(part_shader); + let part_shader = &self.render.part_shader; + self.render.bind_shader(part_shader); // vert uniforms part_shader.set_mvp(gl, mvp); @@ -563,7 +574,7 @@ impl InoxRenderer for OpenglRenderer { ); } - self.pop_debug_group(); + self.render.pop_debug_group(); } fn begin_composite_content( @@ -573,13 +584,13 @@ impl InoxRenderer for OpenglRenderer { _render_ctx: &CompositeRenderCtx, _id: InoxNodeUuid, ) { - self.push_debug_group("inox2d - begin composite content"); + self.render.push_debug_group("inox2d - begin composite content"); - self.clear_texture_cache(); + self.render.clear_texture_cache(); - let gl = &self.gl; + let gl = &self.render.gl; unsafe { - gl.bind_framebuffer(glow::DRAW_FRAMEBUFFER, Some(self.composite_framebuffer)); + gl.bind_framebuffer(glow::DRAW_FRAMEBUFFER, Some(self.render.composite_framebuffer)); gl.disable(glow::DEPTH_TEST); gl.draw_buffers(&[ glow::COLOR_ATTACHMENT0, @@ -594,9 +605,9 @@ impl InoxRenderer for OpenglRenderer { gl.blend_func(glow::ONE, glow::ONE_MINUS_SRC_ALPHA); } - self.pop_debug_group(); + self.render.pop_debug_group(); - self.push_debug_group("inox2d - composite content"); + self.render.push_debug_group("inox2d - composite content"); } fn finish_composite_content( @@ -606,13 +617,13 @@ impl InoxRenderer for OpenglRenderer { _render_ctx: &CompositeRenderCtx, _id: InoxNodeUuid, ) { - self.pop_debug_group(); + self.render.pop_debug_group(); - self.push_debug_group("inox2d - finish composite content"); + self.render.push_debug_group("inox2d - finish composite content"); - let gl = &self.gl; + let gl = &self.render.gl; - self.clear_texture_cache(); + self.render.clear_texture_cache(); unsafe { gl.bind_framebuffer(glow::FRAMEBUFFER, None); } @@ -629,21 +640,21 @@ impl InoxRenderer for OpenglRenderer { } else { unsafe { gl.active_texture(glow::TEXTURE0); - gl.bind_texture(glow::TEXTURE_2D, Some(self.cf_albedo)); + gl.bind_texture(glow::TEXTURE_2D, Some(self.render.cf_albedo)); gl.active_texture(glow::TEXTURE1); - gl.bind_texture(glow::TEXTURE_2D, Some(self.cf_emissive)); + gl.bind_texture(glow::TEXTURE_2D, Some(self.render.cf_emissive)); gl.active_texture(glow::TEXTURE2); - gl.bind_texture(glow::TEXTURE_2D, Some(self.cf_bump)); + gl.bind_texture(glow::TEXTURE_2D, Some(self.render.cf_bump)); } - self.set_blend_mode(blending.mode); + self.render.set_blend_mode(blending.mode); let opacity = blending.opacity.clamp(0.0, 1.0); let tint = blending.tint.clamp(Vec3::ZERO, Vec3::ONE); let screen_tint = blending.screen_tint.clamp(Vec3::ZERO, Vec3::ONE); - let composite_shader = &self.composite_shader; - self.bind_shader(composite_shader); + let composite_shader = &self.render.composite_shader; + self.render.bind_shader(composite_shader); composite_shader.set_opacity(gl, opacity); composite_shader.set_mult_color(gl, tint); composite_shader.set_screen_color(gl, screen_tint); @@ -653,20 +664,20 @@ impl InoxRenderer for OpenglRenderer { gl.draw_elements(glow::TRIANGLES, 6, glow::UNSIGNED_INT, 0); } - self.pop_debug_group(); + self.render.pop_debug_group(); } /// Renderer cleaning up after one frame. - fn on_end_draw(&mut self, _puppet: &Puppet) { - self.pop_debug_group(); + fn on_end_draw(self, _puppet: &Puppet) { + self.render.pop_debug_group(); - self.push_debug_group("inox2d - end draw"); - - let gl = &self.gl; + self.render.push_debug_group("inox2d - end draw"); + + let gl = &self.render.gl; unsafe { gl.bind_vertex_array(None); } - - self.pop_debug_group(); + + self.render.pop_debug_group(); } } diff --git a/inox2d/src/render.rs b/inox2d/src/render.rs index a0e46d8..3561551 100644 --- a/inox2d/src/render.rs +++ b/inox2d/src/render.rs @@ -2,8 +2,8 @@ mod deform_stack; mod vertex_buffers; use std::collections::HashSet; -use std::mem::swap; use std::error::Error; +use std::mem::swap; use crate::node::{ components::{DeformStack, Mask, Masks, ZSort}, @@ -216,12 +216,18 @@ impl RenderCtx { /// /// Either way, the point is Inox2D will implement a `draw()` method for any `impl InoxRenderer`, dispatching calls based on puppet structure according to Inochi2D standard. pub trait InoxRenderer { + type Draw<'a>: DrawSession<'a> + where + Self: 'a; + /// Begin rendering a whole puppet. - /// + /// /// Calls to `on_begin_draw()` and `on_end_draw()` must be /// balanced. Failure to balance these calls will result in a panic. - fn on_begin_draw(&mut self, puppet: &Puppet) -> Result<(), Box>; + fn on_begin_draw<'a>(&'a mut self, puppet: &Puppet) -> Result, Box>; +} +pub trait DrawSession<'a> { /// Begin masking. /// /// Ref impl: Clear and start writing to the stencil buffer, lock the color buffer. @@ -270,16 +276,18 @@ pub trait InoxRenderer { /// Finish rendering, flush any pending operations, and present the puppet /// to the given display. - fn on_end_draw(&mut self, puppet: &Puppet); + fn on_end_draw(self, puppet: &Puppet); } -pub trait InoxRendererExt { +pub trait DrawSessionExt { /// Draw a Drawable, which is potentially masked. fn draw_drawable(&mut self, as_mask: bool, comps: &World, id: InoxNodeUuid); /// Draw one composite. `components` must be referencing `comps`. fn draw_composite(&mut self, as_mask: bool, comps: &World, components: &CompositeComponents, id: InoxNodeUuid); +} +pub trait InoxRendererExt { /// Iterate over top-level drawables (excluding masks) in zsort order, /// and make draw calls correspondingly. /// @@ -287,7 +295,7 @@ pub trait InoxRendererExt { fn draw(&mut self, puppet: &Puppet) -> Result<(), Box>; } -impl InoxRendererExt for T { +impl<'a, T: DrawSession<'a>> DrawSessionExt for T { fn draw_drawable(&mut self, as_mask: bool, comps: &World, id: InoxNodeUuid) { let drawable_kind = DrawableKind::new(id, comps, false).expect("Node must be a Drawable."); let masks = match drawable_kind { @@ -341,19 +349,21 @@ impl InoxRendererExt for T { self.finish_composite_content(as_mask, components, render_ctx, id); } +} +impl InoxRendererExt for T { /// Dispatches draw calls for all nodes of `puppet` /// - with provided renderer implementation, /// - in Inochi2D standard defined order. /// /// This does not guarantee the display of a puppet on screen due to these possible reasons: /// - Only provided `InoxRenderer` method implementations are called. - /// + /// /// For example, maybe the caller still need to transfer content from a texture buffer to the screen surface buffer. /// - The provided `InoxRender` implementation is wrong. /// - `puppet` here does not belong to the `model` this `renderer` is initialized with. This will likely result in panics for non-existent node uuids. fn draw(&mut self, puppet: &Puppet) -> Result<(), Box> { - self.on_begin_draw(puppet)?; + let mut draw = self.on_begin_draw(puppet)?; for uuid in &puppet .render_ctx @@ -361,10 +371,10 @@ impl InoxRendererExt for T { .expect("RenderCtx of puppet must be initialized before calling draw().") .root_drawables_zsorted { - self.draw_drawable(false, &puppet.node_comps, *uuid); + draw.draw_drawable(false, &puppet.node_comps, *uuid); } - self.on_end_draw(puppet); + draw.on_end_draw(puppet); Ok(()) } From 1da8d35d2f3c9d91125305e36beeee9dbaaf9df1 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Mon, 23 Feb 2026 23:57:19 +0000 Subject: [PATCH 04/49] `DrawSessionExt` methods should have access to the puppet being rendered. --- inox2d/src/render.rs | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/inox2d/src/render.rs b/inox2d/src/render.rs index 3561551..6b937cc 100644 --- a/inox2d/src/render.rs +++ b/inox2d/src/render.rs @@ -281,10 +281,17 @@ pub trait DrawSession<'a> { pub trait DrawSessionExt { /// Draw a Drawable, which is potentially masked. - fn draw_drawable(&mut self, as_mask: bool, comps: &World, id: InoxNodeUuid); + fn draw_drawable(&mut self, puppet: &Puppet, as_mask: bool, comps: &World, id: InoxNodeUuid); /// Draw one composite. `components` must be referencing `comps`. - fn draw_composite(&mut self, as_mask: bool, comps: &World, components: &CompositeComponents, id: InoxNodeUuid); + fn draw_composite( + &mut self, + puppet: &Puppet, + as_mask: bool, + comps: &World, + components: &CompositeComponents, + id: InoxNodeUuid, + ); } pub trait InoxRendererExt { @@ -296,7 +303,7 @@ pub trait InoxRendererExt { } impl<'a, T: DrawSession<'a>> DrawSessionExt for T { - fn draw_drawable(&mut self, as_mask: bool, comps: &World, id: InoxNodeUuid) { + fn draw_drawable(&mut self, puppet: &Puppet, as_mask: bool, comps: &World, id: InoxNodeUuid) { let drawable_kind = DrawableKind::new(id, comps, false).expect("Node must be a Drawable."); let masks = match drawable_kind { DrawableKind::TexturedMesh(ref components) => &components.drawable.masks, @@ -310,7 +317,7 @@ impl<'a, T: DrawSession<'a>> DrawSessionExt for T { for mask in &masks.masks { self.on_begin_mask(mask); - self.draw_drawable(true, comps, mask.source); + self.draw_drawable(puppet, true, comps, mask.source); } self.on_begin_masked_content(); } @@ -319,7 +326,7 @@ impl<'a, T: DrawSession<'a>> DrawSessionExt for T { DrawableKind::TexturedMesh(ref components) => { self.draw_textured_mesh_content(as_mask, components, comps.get(id).unwrap(), id) } - DrawableKind::Composite(ref components) => self.draw_composite(as_mask, comps, components, id), + DrawableKind::Composite(ref components) => self.draw_composite(puppet, as_mask, comps, components, id), } if has_masks { @@ -327,13 +334,26 @@ impl<'a, T: DrawSession<'a>> DrawSessionExt for T { } } - fn draw_composite(&mut self, as_mask: bool, comps: &World, components: &CompositeComponents, id: InoxNodeUuid) { + fn draw_composite( + &mut self, + puppet: &Puppet, + as_mask: bool, + comps: &World, + components: &CompositeComponents, + id: InoxNodeUuid, + ) { let render_ctx = comps.get::(id).unwrap(); if render_ctx.zsorted_children_list.is_empty() { // Optimization: Nothing to be drawn, skip context switching return; } + let is_enabled = puppet.nodes.get_node(id).unwrap().enabled; + if !is_enabled && !as_mask { + // Disabled nodes don't render, but they can still be used as masks. + return; + } + self.begin_composite_content(as_mask, components, render_ctx, id); for uuid in &render_ctx.zsorted_children_list { @@ -371,7 +391,7 @@ impl InoxRendererExt for T { .expect("RenderCtx of puppet must be initialized before calling draw().") .root_drawables_zsorted { - draw.draw_drawable(false, &puppet.node_comps, *uuid); + draw.draw_drawable(puppet, false, &puppet.node_comps, *uuid); } draw.on_end_draw(puppet); From c368e65d8f71938fed626ee468b04b433562a7cf Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Wed, 25 Feb 2026 00:14:54 +0000 Subject: [PATCH 05/49] Fix webGL renderer --- examples/render-webgl/src/main.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/examples/render-webgl/src/main.rs b/examples/render-webgl/src/main.rs index 3bc79cf..c15475f 100644 --- a/examples/render-webgl/src/main.rs +++ b/examples/render-webgl/src/main.rs @@ -137,9 +137,7 @@ async fn run() -> Result<(), Box> { // Just that physics simulation will run for the provided time, which may be big and causes a startup delay. puppet.end_frame(scene_ctrl.borrow().dt()); - renderer.borrow().on_begin_draw(&puppet); - renderer.borrow().draw(&puppet); - renderer.borrow().on_end_draw(&puppet); + renderer.borrow_mut().draw(&puppet); } request_animation_frame(anim_loop_f.borrow().as_ref().unwrap()); From 1b2b5f8e053c707e2aa3578f3a3de295d1be6cb4 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Sat, 14 Feb 2026 18:39:47 +0000 Subject: [PATCH 06/49] Add a stub of a WGPU renderer. This necessitates newer GLSL shaders that use explicit uniform blocks and other things not in use in the OpenGL version of these shaders. They may be copied back to their OpenGL versions in the future. --- Cargo.toml | 1 + inox2d-wgpu/.gitignore | 1 + inox2d-wgpu/Cargo.toml | 18 +++++ inox2d-wgpu/build.rs | 67 +++++++++++++++++++ inox2d-wgpu/src/lib.rs | 32 +++++++++ inox2d-wgpu/src/shaders/basic/anim.vert | 27 ++++++++ inox2d-wgpu/src/shaders/basic/basic-mask.frag | 22 ++++++ inox2d-wgpu/src/shaders/basic/basic.frag | 43 ++++++++++++ inox2d-wgpu/src/shaders/basic/basic.vert | 23 +++++++ .../src/shaders/basic/composite-mask.frag | 23 +++++++ inox2d-wgpu/src/shaders/basic/composite.frag | 41 ++++++++++++ inox2d-wgpu/src/shaders/basic/composite.vert | 21 ++++++ inox2d-wgpu/src/shaders/dbg.vert | 19 ++++++ inox2d-wgpu/src/shaders/dbgline.frag | 16 +++++ inox2d-wgpu/src/shaders/dbgpoint.frag | 28 ++++++++ inox2d-wgpu/src/shaders/lighting.frag | 56 ++++++++++++++++ inox2d-wgpu/src/shaders/mask.frag | 13 ++++ inox2d-wgpu/src/shaders/mask.vert | 20 ++++++ inox2d-wgpu/src/shaders/scene.frag | 19 ++++++ inox2d-wgpu/src/shaders/scene.vert | 21 ++++++ 20 files changed, 511 insertions(+) create mode 100644 inox2d-wgpu/.gitignore create mode 100644 inox2d-wgpu/Cargo.toml create mode 100644 inox2d-wgpu/build.rs create mode 100644 inox2d-wgpu/src/lib.rs create mode 100644 inox2d-wgpu/src/shaders/basic/anim.vert create mode 100644 inox2d-wgpu/src/shaders/basic/basic-mask.frag create mode 100644 inox2d-wgpu/src/shaders/basic/basic.frag create mode 100644 inox2d-wgpu/src/shaders/basic/basic.vert create mode 100644 inox2d-wgpu/src/shaders/basic/composite-mask.frag create mode 100644 inox2d-wgpu/src/shaders/basic/composite.frag create mode 100644 inox2d-wgpu/src/shaders/basic/composite.vert create mode 100644 inox2d-wgpu/src/shaders/dbg.vert create mode 100644 inox2d-wgpu/src/shaders/dbgline.frag create mode 100644 inox2d-wgpu/src/shaders/dbgpoint.frag create mode 100644 inox2d-wgpu/src/shaders/lighting.frag create mode 100644 inox2d-wgpu/src/shaders/mask.frag create mode 100644 inox2d-wgpu/src/shaders/mask.vert create mode 100644 inox2d-wgpu/src/shaders/scene.frag create mode 100644 inox2d-wgpu/src/shaders/scene.vert diff --git a/Cargo.toml b/Cargo.toml index 49a2811..b564911 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,5 +3,6 @@ resolver = "2" members = [ "inox2d", "inox2d-opengl", + "inox2d-wgpu", "examples/*", ] diff --git a/inox2d-wgpu/.gitignore b/inox2d-wgpu/.gitignore new file mode 100644 index 0000000..c795b05 --- /dev/null +++ b/inox2d-wgpu/.gitignore @@ -0,0 +1 @@ +build \ No newline at end of file diff --git a/inox2d-wgpu/Cargo.toml b/inox2d-wgpu/Cargo.toml new file mode 100644 index 0000000..37fdbb5 --- /dev/null +++ b/inox2d-wgpu/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "inox2d-wgpu" +description = "WGPU renderer for Inox2D" +authors = ["Arcturus Emrys"] +version = "0.3.0" +edition = "2024" +repository = "https://github.com/Inochi2D/inox2d" +license = "BSD-2-Clause" +keywords = ["gamedev", "graphics", "inochi2d", "vtuber", "opengl"] +categories = ["graphics", "rendering"] + +[dependencies] +inox2d = { path = "../inox2d", version = "0.3.0" } +wgpu = "28.0.0" +thiserror = "1.0.39" + +[build-dependencies] +shaderc = { version = "0.10", features=["build-from-source"] } \ No newline at end of file diff --git a/inox2d-wgpu/build.rs b/inox2d-wgpu/build.rs new file mode 100644 index 0000000..a12be59 --- /dev/null +++ b/inox2d-wgpu/build.rs @@ -0,0 +1,67 @@ +extern crate shaderc; + +use std::{fs, path}; +use std::error::Error; +use std::borrow::Cow; + +fn compile_dir(shader_path: &path::Path, output_path: &path::Path, compiler: &shaderc::Compiler, options: &shaderc::CompileOptions) -> Result<(), Box> { + for entry in fs::read_dir(shader_path)? { + let entry = entry?; + + let in_path = entry.path(); + let item_filename = in_path.file_name().expect("file to have name"); + let new_out_path = output_path.join(item_filename); + + if entry.file_type()?.is_file() { + let shaderkind = in_path + .extension() + .and_then(|ext| match ext.to_string_lossy().as_ref() { + "vert" => Some(shaderc::ShaderKind::Vertex), + "frag" => Some(shaderc::ShaderKind::Fragment), + _ => None + }); + + if let Some(shaderkind) = shaderkind { + let source_text = fs::read_to_string(&in_path)?; + let binary = compiler.compile_into_spirv( + &source_text, + shaderkind, + &in_path.file_name().map(|o| o.to_string_lossy()).unwrap_or(Cow::Borrowed("source.glsl")), + "main", + Some(&options), + )?; + + let out_path = new_out_path.with_extension(match shaderkind { + shaderc::ShaderKind::Vertex => "vert.spv", + shaderc::ShaderKind::Fragment => "frag.spv", + _ => unreachable!() + }); + fs::create_dir_all(out_path.parent().expect("file to have dir"))?; + fs::write(&out_path, &binary.as_binary_u8())?; + } + } else if entry.file_type()?.is_dir() { + compile_dir(&in_path, &new_out_path, compiler, options)?; + } + } + + Ok(()) +} + +/// Build script to compile GLSL shaders from the OpenGL version into SPIR-V +/// for the WGPU version. +/// +/// Due to stupid Apple nonsense, we'll probably also need to compile to WGSL +/// at some point. +fn main() -> Result<(), Box> { + let shader_path = path::absolute("src/shaders")?; + let output_path = path::absolute("build/spirv")?; + + println!("cargo:rerun-if-changed={}", shader_path.to_string_lossy()); + + let compiler = shaderc::Compiler::new()?; + let options = shaderc::CompileOptions::new()?; + + compile_dir(&shader_path, &output_path, &compiler, &options)?; + + Ok(()) +} diff --git a/inox2d-wgpu/src/lib.rs b/inox2d-wgpu/src/lib.rs new file mode 100644 index 0000000..72c8948 --- /dev/null +++ b/inox2d-wgpu/src/lib.rs @@ -0,0 +1,32 @@ +use wgpu; +use inox2d::model::Model; + +#[derive(Debug, thiserror::Error)] +#[error("Could not initialize wgpu renderer: {0}")] +pub enum WgpuRendererError { + CreateSurfaceError(#[from] wgpu::CreateSurfaceError), + RequestAdapterError(#[from] wgpu::RequestAdapterError), + RequestDeviceError(#[from] wgpu::RequestDeviceError), +} + +pub struct WgpuRenderer<'window> { + surface: wgpu::Surface<'window>, +} + +impl<'window> WgpuRenderer<'window> { + pub async fn new( + target: impl Into>, + model: &Model, + ) -> Result { + let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::from_env_or_default()); + let surface = instance.create_surface(target)?; + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions { + compatible_surface: Some(&surface), + ..Default::default() + }) + .await?; + let (device, queue) = adapter.request_device(&wgpu::DeviceDescriptor::default()).await?; + Ok(WgpuRenderer { surface }) + } +} diff --git a/inox2d-wgpu/src/shaders/basic/anim.vert b/inox2d-wgpu/src/shaders/basic/anim.vert new file mode 100644 index 0000000..dac4456 --- /dev/null +++ b/inox2d-wgpu/src/shaders/basic/anim.vert @@ -0,0 +1,27 @@ +/* + Copyright © 2020, Inochi2D Project + Distributed under the 2-Clause BSD License, see LICENSE file. + + Authors: Luna Nielsen +*/ +#version 440 + +layout(binding = 0) uniform Input { + mat4 mvp; + vec2 offset; + + uniform vec2 splits; + uniform float animation; + uniform float frame; +} uni_in; + +layout(location = 0) in vec2 verts; +layout(location = 1) in vec2 uvs; +layout(location = 2) in vec2 deform; + +layout(location = 0) out vec2 texUVs; + +void main() { + gl_Position = uni_in.mvp * vec4(verts + uni_in.offset + deform, 0, 1); + texUVs = vec2((uvs.x / uni_in.splits.x) * uni_in.frame, (uvs.y / uni_in.splits.y) * uni_in.animation); +} \ No newline at end of file diff --git a/inox2d-wgpu/src/shaders/basic/basic-mask.frag b/inox2d-wgpu/src/shaders/basic/basic-mask.frag new file mode 100644 index 0000000..d885b19 --- /dev/null +++ b/inox2d-wgpu/src/shaders/basic/basic-mask.frag @@ -0,0 +1,22 @@ +/* + Copyright © 2020, Inochi2D Project + Distributed under the 2-Clause BSD License, see LICENSE file. + + Authors: Luna Nielsen +*/ +#version 440 + +layout(location = 0) in vec2 texUVs; +layout(location = 0) out vec4 outColor; + +layout(binding = 0) uniform sampler2D tex; +layout(binding = 1) uniform Input { + float threshold; +} uni_in; + +void main() { + vec4 color = texture(tex, texUVs); + if (color.a <= uni_in.threshold) + discard; + outColor = vec4(1, 1, 1, 1); +} \ No newline at end of file diff --git a/inox2d-wgpu/src/shaders/basic/basic.frag b/inox2d-wgpu/src/shaders/basic/basic.frag new file mode 100644 index 0000000..4925e37 --- /dev/null +++ b/inox2d-wgpu/src/shaders/basic/basic.frag @@ -0,0 +1,43 @@ +/* + Copyright © 2020, Inochi2D Project + Distributed under the 2-Clause BSD License, see LICENSE file. + + Authors: Luna Nielsen +*/ +#version 440 +layout(location = 0) in vec2 texUVs; + +layout(location = 0) out vec4 outAlbedo; +layout(location = 1) out vec4 outEmissive; +layout(location = 2) out vec4 outBump; + +layout(binding = 0) uniform sampler2D albedo; +layout(binding = 1) uniform sampler2D emissive; +layout(binding = 2) uniform sampler2D bumpmap; + +layout(binding = 3) uniform Input { + uniform float opacity; + uniform vec3 multColor; + uniform vec3 screenColor; + uniform float emissionStrength; +} uni_in; + +void main() { + // Sample texture + vec4 texColor = texture(albedo, texUVs); + + // Screen color math + vec3 screenOut = vec3(1.0) - ((vec3(1.0) - (texColor.xyz)) * + (vec3(1.0) - (uni_in.screenColor * texColor.a))); + + // Multiply color math + opacity application. + outAlbedo = + vec4(screenOut.xyz, texColor.a) * vec4(uni_in.multColor.xyz, 1) * uni_in.opacity; + + // Emissive + outEmissive = + vec4(texture(emissive, texUVs).xyz * uni_in.emissionStrength, 1) * outAlbedo.a; + + // Bumpmap + outBump = vec4(texture(bumpmap, texUVs).xyz, 1) * outAlbedo.a; +} \ No newline at end of file diff --git a/inox2d-wgpu/src/shaders/basic/basic.vert b/inox2d-wgpu/src/shaders/basic/basic.vert new file mode 100644 index 0000000..96e85c8 --- /dev/null +++ b/inox2d-wgpu/src/shaders/basic/basic.vert @@ -0,0 +1,23 @@ +/* + Copyright © 2020, Inochi2D Project + Distributed under the 2-Clause BSD License, see LICENSE file. + + Authors: Luna Nielsen +*/ +#version 440 + +layout(binding = 0) uniform Input { + mat4 mvp; + vec2 offset; +} uni_in; + +layout(location = 0) in vec2 verts; +layout(location = 1) in vec2 uvs; +layout(location = 2) in vec2 deform; + +layout(location = 0) out vec2 texUVs; + +void main() { + gl_Position = uni_in.mvp * vec4(verts - uni_in.offset + deform, 0, 1); + texUVs = uvs; +} \ No newline at end of file diff --git a/inox2d-wgpu/src/shaders/basic/composite-mask.frag b/inox2d-wgpu/src/shaders/basic/composite-mask.frag new file mode 100644 index 0000000..8b4c760 --- /dev/null +++ b/inox2d-wgpu/src/shaders/basic/composite-mask.frag @@ -0,0 +1,23 @@ +/* + Copyright © 2020, Inochi2D Project + Distributed under the 2-Clause BSD License, see LICENSE file. + + Authors: Luna Nielsen +*/ +#version 440 + +layout(location = 0) in vec2 texUVs; +layout(location = 0) out vec4 outColor; + +layout(binding = 0) uniform sampler2D tex; +layout(binding = 1) uniform Input { + float threshold; + float opacity; +} uni_in; + +void main() { + vec4 color = texture(tex, texUVs) * vec4(1, 1, 1, uni_in.opacity); + if (color.a <= uni_in.threshold) + discard; + outColor = vec4(1, 1, 1, 1); +} \ No newline at end of file diff --git a/inox2d-wgpu/src/shaders/basic/composite.frag b/inox2d-wgpu/src/shaders/basic/composite.frag new file mode 100644 index 0000000..cdcc427 --- /dev/null +++ b/inox2d-wgpu/src/shaders/basic/composite.frag @@ -0,0 +1,41 @@ +/* + Copyright © 2020, Inochi2D Project + Distributed under the 2-Clause BSD License, see LICENSE file. + + Authors: Luna Nielsen +*/ +#version 440 +layout(location = 0) in vec2 texUVs; + +layout(location = 0) out vec4 outAlbedo; +layout(location = 1) out vec4 outEmissive; +layout(location = 2) out vec4 outBump; + +layout(binding = 0) uniform sampler2D albedo; +layout(binding = 1) uniform sampler2D emissive; +layout(binding = 2) uniform sampler2D bumpmap; + +layout(binding = 3) uniform Input { + float opacity; + vec3 multColor; + vec3 screenColor; +} uni_in; + +void main() { + // Sample texture + vec4 texColor = texture(albedo, texUVs); + + // Screen color math + vec3 screenOut = vec3(1.0) - ((vec3(1.0) - (texColor.xyz)) * + (vec3(1.0) - (uni_in.screenColor * texColor.a))); + + // Multiply color math + opacity application. + outAlbedo = + vec4(screenOut.xyz, texColor.a) * vec4(uni_in.multColor.xyz, 1) * uni_in.opacity; + + // Emissive + outEmissive = texture(emissive, texUVs) * outAlbedo.a; + + // Bumpmap + outBump = texture(bumpmap, texUVs) * outAlbedo.a; +} \ No newline at end of file diff --git a/inox2d-wgpu/src/shaders/basic/composite.vert b/inox2d-wgpu/src/shaders/basic/composite.vert new file mode 100644 index 0000000..b466608 --- /dev/null +++ b/inox2d-wgpu/src/shaders/basic/composite.vert @@ -0,0 +1,21 @@ +/* + Copyright © 2020, Inochi2D Project + Distributed under the 2-Clause BSD License, see LICENSE file. + + Authors: Luna Nielsen +*/ +#version 440 + +layout(binding = 0) uniform Input { + mat4 mvp; +} uni_in; + +layout(location = 0) in vec2 verts; +layout(location = 1) in vec2 uvs; + +layout(location = 0) out vec2 texUVs; + +void main() { + gl_Position = vec4(verts, 0, 1); + texUVs = uvs; +} \ No newline at end of file diff --git a/inox2d-wgpu/src/shaders/dbg.vert b/inox2d-wgpu/src/shaders/dbg.vert new file mode 100644 index 0000000..2dda508 --- /dev/null +++ b/inox2d-wgpu/src/shaders/dbg.vert @@ -0,0 +1,19 @@ +/* + Copyright © 2020, Inochi2D Project + Distributed under the 2-Clause BSD License, see LICENSE file. + + Authors: Luna Nielsen +*/ +#version 440 + +layout(binding = 0) uniform Input { + mat4 mvp; +} uni_in; + +layout(location = 0) in vec3 verts; + +layout(location = 0) out vec2 texUVs; + +void main() { + gl_Position = uni_in.mvp * vec4(verts.x, verts.y, verts.z, 1); +} \ No newline at end of file diff --git a/inox2d-wgpu/src/shaders/dbgline.frag b/inox2d-wgpu/src/shaders/dbgline.frag new file mode 100644 index 0000000..71f1fa3 --- /dev/null +++ b/inox2d-wgpu/src/shaders/dbgline.frag @@ -0,0 +1,16 @@ +/* + Copyright © 2020, Inochi2D Project + Distributed under the 2-Clause BSD License, see LICENSE file. + + Authors: Luna Nielsen +*/ +#version 440 +layout(location = 0) out vec4 outColor; + +layout(binding=0) uniform Input { + vec4 color; +} uni_in; + +void main() { + outColor = uni_in.color; +} \ No newline at end of file diff --git a/inox2d-wgpu/src/shaders/dbgpoint.frag b/inox2d-wgpu/src/shaders/dbgpoint.frag new file mode 100644 index 0000000..8096a05 --- /dev/null +++ b/inox2d-wgpu/src/shaders/dbgpoint.frag @@ -0,0 +1,28 @@ +/* + Copyright © 2020, Inochi2D Project + Distributed under the 2-Clause BSD License, see LICENSE file. + + Authors: Luna Nielsen +*/ +#version 440 +layout(location = 0) out vec4 outColor; + +layout(binding = 0) uniform Input { + vec4 color; +} uni_in; + +void main() { + float r = 0.0; // radius + float alpha = 1.0; // alpha + + // r = point in circle compared against circle raidus + vec2 cxy = 2.0 * gl_PointCoord - 1.0; + r = dot(cxy, cxy); + + // epsilon width + float epsilon = fwidth(r) * 0.5; + + // apply delta + alpha = 1.0 - smoothstep(1.0 - epsilon, 1.0 + epsilon, r); + outColor = uni_in.color * alpha; +} \ No newline at end of file diff --git a/inox2d-wgpu/src/shaders/lighting.frag b/inox2d-wgpu/src/shaders/lighting.frag new file mode 100644 index 0000000..1c6ec90 --- /dev/null +++ b/inox2d-wgpu/src/shaders/lighting.frag @@ -0,0 +1,56 @@ +/* + Copyright © 2020, Inochi2D Project + Distributed under the 2-Clause BSD License, see LICENSE file. + + Authors: Luna Nielsen +*/ +#version 440 +layout(location = 0) in vec2 texUVs; + +layout(location = 0) out vec4 outAlbedo; +layout(location = 1) out vec4 outEmissive; +layout(location = 2) out vec4 outBump; + +layout(binding = 0) uniform Input { + uniform vec3 ambientLight; + uniform vec2 fbSize; + + uniform int LOD; // OLD DEFAULT: 2 + uniform int samples; // OLD DEFAULT: 25 +} uni_in; + +layout(binding = 1) uniform sampler2D albedo; +layout(binding = 2) uniform sampler2D emissive; +layout(binding = 3) uniform sampler2D bumpmap; + +// Gaussian +float gaussian(vec2 i, float sigma) { + return exp(-0.5 * dot(i /= sigma, i)) / (6.28 * sigma * sigma); +} + +// Bloom texture by blurring it +vec4 bloom(sampler2D sp, vec2 uv, vec2 scale) { + float sigma = float(uni_in.samples) * 0.25; + vec4 out_ = vec4(0); + int sLOD = 1 << uni_in.LOD; + int s = uni_in.samples / sLOD; + + for (int i = 0; i < s * s; i++) { + vec2 d = vec2(i % s, i / s) * float(sLOD) - float(uni_in.samples) / 2.0; + out_ += gaussian(d, sigma) * textureLod(sp, uv + scale * d, uni_in.LOD); + } + + return out_ / out_.a; +} + +void main() { + + // Bloom + outEmissive = bloom(emissive, texUVs, 1.0 / uni_in.fbSize); + + // Set color to the corrosponding pixel in the FBO + vec4 light = vec4(uni_in.ambientLight, 1) + outEmissive; + + outAlbedo = (texture(albedo, texUVs) * light); + outBump = texture(bumpmap, texUVs); +} \ No newline at end of file diff --git a/inox2d-wgpu/src/shaders/mask.frag b/inox2d-wgpu/src/shaders/mask.frag new file mode 100644 index 0000000..af09fb1 --- /dev/null +++ b/inox2d-wgpu/src/shaders/mask.frag @@ -0,0 +1,13 @@ +/* + Copyright © 2020, Inochi2D Project + Distributed under the 2-Clause BSD License, see LICENSE file. + + Authors: Luna Nielsen +*/ +#version 440 + +layout(location = 0) out vec4 outColor; + +void main() { + outColor = vec4(0, 0, 0, 1); +} \ No newline at end of file diff --git a/inox2d-wgpu/src/shaders/mask.vert b/inox2d-wgpu/src/shaders/mask.vert new file mode 100644 index 0000000..0d36bc2 --- /dev/null +++ b/inox2d-wgpu/src/shaders/mask.vert @@ -0,0 +1,20 @@ +/* + Copyright © 2020, Inochi2D Project + Distributed under the 2-Clause BSD License, see LICENSE file. + + Authors: Luna Nielsen +*/ +#version 440 + +layout(binding = 0) uniform Input { + mat4 mvp; + vec2 offset; +} uni_in; + +layout(location = 0) in vec2 verts; + +layout(location = 0) out vec2 texUVs; + +void main() { + gl_Position = uni_in.mvp * vec4(verts.x-uni_in.offset.x, verts.y-uni_in.offset.y, 0, 1); +} \ No newline at end of file diff --git a/inox2d-wgpu/src/shaders/scene.frag b/inox2d-wgpu/src/shaders/scene.frag new file mode 100644 index 0000000..b44575f --- /dev/null +++ b/inox2d-wgpu/src/shaders/scene.frag @@ -0,0 +1,19 @@ +/* + Copyright © 2020, Inochi2D Project + Distributed under the 2-Clause BSD License, see LICENSE file. + + Authors: Luna Nielsen +*/ +#version 440 + +layout(location = 0) in vec2 texUVs; +layout(location = 0) out vec4 outColor; + +layout(binding = 0) uniform sampler2D fbo; + +void main() { + // Set color to the corrosponding pixel in the FBO + vec4 color = texture(fbo, texUVs); + outColor = + vec4(color.r * color.a, color.g * color.a, color.b * color.a, color.a); +} \ No newline at end of file diff --git a/inox2d-wgpu/src/shaders/scene.vert b/inox2d-wgpu/src/shaders/scene.vert new file mode 100644 index 0000000..99226ff --- /dev/null +++ b/inox2d-wgpu/src/shaders/scene.vert @@ -0,0 +1,21 @@ +/* + Copyright © 2020, Inochi2D Project + Distributed under the 2-Clause BSD License, see LICENSE file. + + Authors: Luna Nielsen +*/ +#version 440 + +layout(binding = 0) uniform Input { + mat4 mvp; +} uni_in; + +layout(location = 0) in vec2 verts; +layout(location = 1) in vec2 uvs; + +layout(location = 0) out vec2 texUVs; + +void main() { + gl_Position = uni_in.mvp * vec4(verts.x, verts.y, 0, 1); + texUVs = uvs; +} \ No newline at end of file From 67cf864b8ab27905ca15134227db8cd58aa877b7 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Sat, 14 Feb 2026 20:20:14 +0000 Subject: [PATCH 07/49] First attempt at a SPIR-V shim generator --- inox2d-wgpu/.gitignore | 6 +- inox2d-wgpu/Cargo.toml | 5 +- inox2d-wgpu/build.rs | 190 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 196 insertions(+), 5 deletions(-) diff --git a/inox2d-wgpu/.gitignore b/inox2d-wgpu/.gitignore index c795b05..05cd515 100644 --- a/inox2d-wgpu/.gitignore +++ b/inox2d-wgpu/.gitignore @@ -1 +1,5 @@ -build \ No newline at end of file +build + +#These are autogenerated shader introspection files +src/shaders/**/*_vert.rs +src/shaders/**/*_frag.rs \ No newline at end of file diff --git a/inox2d-wgpu/Cargo.toml b/inox2d-wgpu/Cargo.toml index 37fdbb5..e8ec0e4 100644 --- a/inox2d-wgpu/Cargo.toml +++ b/inox2d-wgpu/Cargo.toml @@ -11,8 +11,9 @@ categories = ["graphics", "rendering"] [dependencies] inox2d = { path = "../inox2d", version = "0.3.0" } -wgpu = "28.0.0" +wgpu = { version = "28.0.0", features=["spirv"] } thiserror = "1.0.39" [build-dependencies] -shaderc = { version = "0.10", features=["build-from-source"] } \ No newline at end of file +shaderc = { version = "0.10", features=["build-from-source"] } +spirv-reflect = { git = "https://github.com/gwihlidal/spirv-reflect-rs.git", rev="97298067e3b1c9ce05633d78f1183c14a3cc6acc"} \ No newline at end of file diff --git a/inox2d-wgpu/build.rs b/inox2d-wgpu/build.rs index a12be59..2ce2d6c 100644 --- a/inox2d-wgpu/build.rs +++ b/inox2d-wgpu/build.rs @@ -1,8 +1,168 @@ -extern crate shaderc; +use shaderc; +use spirv_reflect; +use spirv_reflect::types::{ReflectBlockVariable, ReflectDescriptorType, ReflectTypeDescription, ReflectTypeFlags}; use std::{fs, path}; use std::error::Error; use std::borrow::Cow; +use std::ffi::OsString; +use std::fmt::Write; + +fn describe_block_struct(out: &mut String, blockvar: &ReflectBlockVariable, typevar: &ReflectTypeDescription) -> Result<(), Box> { + writeln!(out, "struct {} {{", typevar.type_name)?; + + for (blockmember, typemember) in blockvar.members.iter().zip(typevar.members.iter()) { + writeln!(out, " /// name: {}", blockmember.name)?; + writeln!(out, " /// type: {}", typemember.type_name)?; + writeln!(out, " /// offset: {}", blockmember.offset)?; + writeln!(out, " /// Storage class: {:?}", typemember.storage_class)?; + writeln!(out, " /// Type Flags: {:?}", typemember.type_flags)?; + writeln!(out, " /// Decoration Flags: {:?}", typemember.decoration_flags)?; + writeln!(out, " /// Traits: {:?}", typemember.traits)?; + + let base_type = if typemember.type_flags.contains(ReflectTypeFlags::FLOAT) { + match typemember.traits.numeric.scalar.width { + 32 => "f32", + _ => { + writeln!(out, "/// UNIMPLEMENTED {}", typemember.traits.numeric.scalar.width)?; + "unimplemented" + } + } + } else { + writeln!(out, "/// UNIMPLEMENTED")?; + "unimplemented" + }; + + if typemember.type_flags.contains(ReflectTypeFlags::MATRIX) { + writeln!(out, " {}: [[{}; {}]; {}],", blockmember.name, base_type, typemember.traits.numeric.matrix.column_count, typemember.traits.numeric.matrix.row_count)?; + } else if typemember.type_flags.contains(ReflectTypeFlags::VECTOR) { + //Represent vectors as arrays. + writeln!(out, " {}: [{}; {}],", blockmember.name, base_type, typemember.traits.numeric.vector.component_count)?; + } else { + //Single + writeln!(out, " {}: {},", blockmember.name, base_type)?; + } + } + + writeln!(out, "}}")?; + writeln!(out)?; + + writeln!(out, "impl {} {{", typevar.type_name)?; + writeln!(out, " fn into_uniform_buffer(self) -> [u32; {}] {{", blockvar.size)?; + //TODO: Codegen a copy + writeln!(out, " }}")?; + writeln!(out, "}}")?; + writeln!(out)?; + + Ok(()) +} + +fn introspect_spirv(out: &mut String, snake_case_name: &str, filepath: &str, module: &spirv_reflect::ShaderModule) -> Result<(), Box> { + writeln!(out, "use wgpu;")?; + writeln!(out, "use wgpu::include_spirv;")?; + + for entrypoint in module.enumerate_entry_points()? { + writeln!(out, "/// Entry point {}", entrypoint.name)?; + writeln!(out, "/// Execution model {:?}", entrypoint.spirv_execution_model)?; + writeln!(out, "/// Shader stage {:?}", entrypoint.shader_stage)?; + + // Most of these are stubs. + // We will eventually have this print Rust structs and consts. + for var in entrypoint.input_variables { + writeln!(out, "/// input {}", var.name)?; + writeln!(out, "/// location {}", var.location)?; + writeln!(out, "/// semantic {}", var.semantic)?; + writeln!(out, "/// Decoration Flags: {:?}", var.decoration_flags)?; + writeln!(out, "/// Builtins: {:?}", var.built_in)?; + writeln!(out, "/// Format: {:?}", var.format)?; + writeln!(out, "/// members:")?; + + for var in var.members { + writeln!(out, " /// {}", var.name)?; + writeln!(out, " /// location {}", var.location)?; + writeln!(out, " /// semantic {}", var.semantic)?; + writeln!(out, " /// Decoration Flags: {:?}", var.decoration_flags)?; + writeln!(out, " /// Builtins: {:?}", var.built_in)?; + writeln!(out, " /// Format: {:?}", var.format)?; + } + writeln!(out, "/// END members:")?; + } + + for var in entrypoint.output_variables { + writeln!(out, "/// output {}", var.name)?; + writeln!(out, "/// location {}", var.location)?; + writeln!(out, "/// semantic {}", var.semantic)?; + writeln!(out, "/// Decoration Flags: {:?}", var.decoration_flags)?; + writeln!(out, "/// Builtins: {:?}", var.built_in)?; + writeln!(out, "/// Format: {:?}", var.format)?; + writeln!(out, "/// members:")?; + + for var in var.members { + writeln!(out, " /// {}", var.name)?; + writeln!(out, " /// location {}", var.location)?; + writeln!(out, " /// semantic {}", var.semantic)?; + writeln!(out, " /// Decoration Flags: {:?}", var.decoration_flags)?; + writeln!(out, " /// Builtins: {:?}", var.built_in)?; + writeln!(out, " /// Format: {:?}", var.format)?; + } + writeln!(out, "/// END members:")?; + } + + for descriptor_set in entrypoint.descriptor_sets { + writeln!(out, "/// descriptor set {}", descriptor_set.set)?; + + for binding in descriptor_set.bindings { + writeln!(out, "/// descriptor {} (binding {})", binding.name, binding.binding)?; + + match binding.descriptor_type { + ReflectDescriptorType::UniformBuffer => { + if let Some(typevar) = binding.type_description { + writeln!(out, "/// UNIFORM BUFFER of type {}", typevar.type_name)?; + writeln!(out, "/// Struct member name: {}", typevar.struct_member_name)?; + writeln!(out, "/// Storage class: {:?}", typevar.storage_class)?; + writeln!(out, "/// Type Flags: {:?}", typevar.type_flags)?; + writeln!(out, "/// Decoration Flags: {:?}", typevar.decoration_flags)?; + writeln!(out, "/// Traits: {:?}", typevar.traits)?; + describe_block_struct(out, &binding.block, &typevar)?; + } else { + writeln!(out, "/// UNIFORM BUFFER of unknown type name {}", binding.name)?; + writeln!(out, "const BINDING_{}: u32 = {};", binding.name.to_uppercase(), binding.binding)?; + } + } + _ => { + writeln!(out, "/// unknown type {:?}", binding.descriptor_type)?; + writeln!(out, "const BINDING_{}: u32 = {};", binding.name.to_uppercase(), binding.binding)?; + } + } + } + } + + for uniform_id in entrypoint.used_uniforms { + writeln!(out, "/// uniform ID {}", uniform_id)?; + } + + for uniform_id in entrypoint.used_push_constants { + writeln!(out, "/// push constant ID {}", uniform_id)?; + } + + writeln!(out)?; + writeln!(out, "const {} : wgpu::ShaderModuleDescriptor = include_spirv!(\"{}\");", snake_case_name, filepath.replace("\\", "\\\\"))?; + writeln!(out)?; + writeln!(out, "pub struct Shader {{")?; + writeln!(out, " {}: wgpu::ShaderModule", entrypoint.name)?; + writeln!(out, "}}")?; + writeln!(out)?; + writeln!(out, "impl Shader {{")?; + writeln!(out, " pub fn new(device: &wgpu::Device) -> Self {{")?; + writeln!(out, " Self {{")?; + writeln!(out, " {}: device.create_shader_module({})", entrypoint.name, snake_case_name)?; + writeln!(out, " }}")?; + writeln!(out, " }}")?; + writeln!(out, "}}")?; + } + + Ok(()) +} fn compile_dir(shader_path: &path::Path, output_path: &path::Path, compiler: &shaderc::Compiler, options: &shaderc::CompileOptions) -> Result<(), Box> { for entry in fs::read_dir(shader_path)? { @@ -30,14 +190,40 @@ fn compile_dir(shader_path: &path::Path, output_path: &path::Path, compiler: &sh "main", Some(&options), )?; + let data = binary.as_binary_u8(); let out_path = new_out_path.with_extension(match shaderkind { shaderc::ShaderKind::Vertex => "vert.spv", shaderc::ShaderKind::Fragment => "frag.spv", _ => unreachable!() }); + fs::create_dir_all(out_path.parent().expect("file to have dir"))?; - fs::write(&out_path, &binary.as_binary_u8())?; + fs::write(&out_path, &data)?; + + let reflection = spirv_reflect::ShaderModule::load_u8_data(&data)?; + let filename_but_with_the_shaderkind = { + let kind = match shaderkind { + shaderc::ShaderKind::Vertex => "_vert", + shaderc::ShaderKind::Fragment => "_frag", + _ => unreachable!() + }; + let extless = in_path.with_extension("").file_name().expect("ya gotta have a filename").to_string_lossy().replace("-", "_"); + let mut ret = OsString::with_capacity(extless.len() + kind.len()); + ret.push(extless); + ret.push(kind); + ret + }; + let reflect_out_path = in_path.with_file_name(filename_but_with_the_shaderkind.clone()).with_extension("rs"); + + let mut reflect_data = String::new(); + + writeln!(&mut reflect_data, "/// Automatically generated introspection data for {}", item_filename.to_string_lossy())?; + let snake_case_name = filename_but_with_the_shaderkind.to_string_lossy(); + let snake_case_name = snake_case_name.to_uppercase(); + introspect_spirv(&mut reflect_data, &snake_case_name, &out_path.to_string_lossy(), &reflection)?; + + fs::write(&reflect_out_path, reflect_data)?; } } else if entry.file_type()?.is_dir() { compile_dir(&in_path, &new_out_path, compiler, options)?; From fac757514ed15581109b6279d95e25b8b633f2e7 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Sat, 14 Feb 2026 20:29:36 +0000 Subject: [PATCH 08/49] Autogenerate module files for the shaders as well --- inox2d-wgpu/.gitignore | 4 ++-- inox2d-wgpu/build.rs | 20 +++++++++++++++++--- inox2d-wgpu/src/lib.rs | 2 ++ 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/inox2d-wgpu/.gitignore b/inox2d-wgpu/.gitignore index 05cd515..da84dff 100644 --- a/inox2d-wgpu/.gitignore +++ b/inox2d-wgpu/.gitignore @@ -1,5 +1,5 @@ build #These are autogenerated shader introspection files -src/shaders/**/*_vert.rs -src/shaders/**/*_frag.rs \ No newline at end of file +src/shaders/**/*.rs +src/shaders.rs \ No newline at end of file diff --git a/inox2d-wgpu/build.rs b/inox2d-wgpu/build.rs index 2ce2d6c..de721de 100644 --- a/inox2d-wgpu/build.rs +++ b/inox2d-wgpu/build.rs @@ -164,7 +164,7 @@ fn introspect_spirv(out: &mut String, snake_case_name: &str, filepath: &str, mod Ok(()) } -fn compile_dir(shader_path: &path::Path, output_path: &path::Path, compiler: &shaderc::Compiler, options: &shaderc::CompileOptions) -> Result<(), Box> { +fn compile_dir(shader_path: &path::Path, output_path: &path::Path, compiler: &shaderc::Compiler, options: &shaderc::CompileOptions, parent_module_rust_src: &mut String) -> Result<(), Box> { for entry in fs::read_dir(shader_path)? { let entry = entry?; @@ -220,13 +220,23 @@ fn compile_dir(shader_path: &path::Path, output_path: &path::Path, compiler: &sh writeln!(&mut reflect_data, "/// Automatically generated introspection data for {}", item_filename.to_string_lossy())?; let snake_case_name = filename_but_with_the_shaderkind.to_string_lossy(); + writeln!(parent_module_rust_src, "pub mod {};", snake_case_name)?; + let snake_case_name = snake_case_name.to_uppercase(); introspect_spirv(&mut reflect_data, &snake_case_name, &out_path.to_string_lossy(), &reflection)?; fs::write(&reflect_out_path, reflect_data)?; } } else if entry.file_type()?.is_dir() { - compile_dir(&in_path, &new_out_path, compiler, options)?; + let snake_case_name = item_filename.to_string_lossy(); + writeln!(parent_module_rust_src, "pub mod {};", snake_case_name)?; + + let corresponding_mod_file = in_path.with_extension("rs"); + let mut rust_src = "/// AUTO GENERATED SOURCE DO NOT EDIT\n".to_string(); + + compile_dir(&in_path, &new_out_path, compiler, options, &mut rust_src)?; + + fs::write(&corresponding_mod_file, rust_src)?; } } @@ -247,7 +257,11 @@ fn main() -> Result<(), Box> { let compiler = shaderc::Compiler::new()?; let options = shaderc::CompileOptions::new()?; - compile_dir(&shader_path, &output_path, &compiler, &options)?; + let corresponding_mod_file = shader_path.with_extension("rs"); + let mut rust_src = "/// AUTO GENERATED SOURCE DO NOT EDIT\n".to_string(); + + compile_dir(&shader_path, &output_path, &compiler, &options, &mut rust_src)?; + fs::write(&corresponding_mod_file, rust_src)?; Ok(()) } diff --git a/inox2d-wgpu/src/lib.rs b/inox2d-wgpu/src/lib.rs index 72c8948..ce2dbf2 100644 --- a/inox2d-wgpu/src/lib.rs +++ b/inox2d-wgpu/src/lib.rs @@ -1,6 +1,8 @@ use wgpu; use inox2d::model::Model; +mod shaders; + #[derive(Debug, thiserror::Error)] #[error("Could not initialize wgpu renderer: {0}")] pub enum WgpuRendererError { From 88ad2f1feaaf311cca9024352015ef5dcb2093cb Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Sat, 14 Feb 2026 20:54:43 +0000 Subject: [PATCH 09/49] Add automatic copy to uniform buffer to generated shader types --- inox2d-wgpu/build.rs | 593 +++++++++++++++++++++++++------------------ 1 file changed, 352 insertions(+), 241 deletions(-) diff --git a/inox2d-wgpu/build.rs b/inox2d-wgpu/build.rs index de721de..3f80a07 100644 --- a/inox2d-wgpu/build.rs +++ b/inox2d-wgpu/build.rs @@ -2,245 +2,356 @@ use shaderc; use spirv_reflect; use spirv_reflect::types::{ReflectBlockVariable, ReflectDescriptorType, ReflectTypeDescription, ReflectTypeFlags}; -use std::{fs, path}; -use std::error::Error; use std::borrow::Cow; +use std::error::Error; use std::ffi::OsString; use std::fmt::Write; +use std::{fs, path}; -fn describe_block_struct(out: &mut String, blockvar: &ReflectBlockVariable, typevar: &ReflectTypeDescription) -> Result<(), Box> { - writeln!(out, "struct {} {{", typevar.type_name)?; - - for (blockmember, typemember) in blockvar.members.iter().zip(typevar.members.iter()) { - writeln!(out, " /// name: {}", blockmember.name)?; - writeln!(out, " /// type: {}", typemember.type_name)?; - writeln!(out, " /// offset: {}", blockmember.offset)?; - writeln!(out, " /// Storage class: {:?}", typemember.storage_class)?; - writeln!(out, " /// Type Flags: {:?}", typemember.type_flags)?; - writeln!(out, " /// Decoration Flags: {:?}", typemember.decoration_flags)?; - writeln!(out, " /// Traits: {:?}", typemember.traits)?; - - let base_type = if typemember.type_flags.contains(ReflectTypeFlags::FLOAT) { - match typemember.traits.numeric.scalar.width { - 32 => "f32", - _ => { - writeln!(out, "/// UNIMPLEMENTED {}", typemember.traits.numeric.scalar.width)?; - "unimplemented" - } - } - } else { - writeln!(out, "/// UNIMPLEMENTED")?; - "unimplemented" - }; - - if typemember.type_flags.contains(ReflectTypeFlags::MATRIX) { - writeln!(out, " {}: [[{}; {}]; {}],", blockmember.name, base_type, typemember.traits.numeric.matrix.column_count, typemember.traits.numeric.matrix.row_count)?; - } else if typemember.type_flags.contains(ReflectTypeFlags::VECTOR) { - //Represent vectors as arrays. - writeln!(out, " {}: [{}; {}],", blockmember.name, base_type, typemember.traits.numeric.vector.component_count)?; - } else { - //Single - writeln!(out, " {}: {},", blockmember.name, base_type)?; - } - } - - writeln!(out, "}}")?; - writeln!(out)?; - - writeln!(out, "impl {} {{", typevar.type_name)?; - writeln!(out, " fn into_uniform_buffer(self) -> [u32; {}] {{", blockvar.size)?; - //TODO: Codegen a copy - writeln!(out, " }}")?; - writeln!(out, "}}")?; - writeln!(out)?; - - Ok(()) +fn describe_block_struct( + out: &mut String, + blockvar: &ReflectBlockVariable, + typevar: &ReflectTypeDescription, +) -> Result<(), Box> { + writeln!(out, "#[allow(non_snake_case)]")?; //I'm too lazy to write a to_snake_case fn + writeln!(out, "struct {} {{", typevar.type_name)?; + + for (blockmember, typemember) in blockvar.members.iter().zip(typevar.members.iter()) { + writeln!(out, " /// name: {}", blockmember.name)?; + writeln!(out, " /// type: {}", typemember.type_name)?; + writeln!(out, " /// offset: {}", blockmember.offset)?; + writeln!(out, " /// Storage class: {:?}", typemember.storage_class)?; + writeln!(out, " /// Type Flags: {:?}", typemember.type_flags)?; + writeln!(out, " /// Decoration Flags: {:?}", typemember.decoration_flags)?; + writeln!(out, " /// Traits: {:?}", typemember.traits)?; + + let base_type = if typemember.type_flags.contains(ReflectTypeFlags::FLOAT) { + match typemember.traits.numeric.scalar.width { + 32 => "f32", + _ => { + writeln!(out, "/// UNIMPLEMENTED {}", typemember.traits.numeric.scalar.width)?; + "unimplemented" + } + } + } else if typemember.type_flags.contains(ReflectTypeFlags::INT) { + match ( + typemember.traits.numeric.scalar.width, + typemember.traits.numeric.scalar.signedness, + ) { + (8, 1) => "i8", + (16, 1) => "i16", + (32, 1) => "i32", + (8, 0) => "u8", + (16, 0) => "u16", + (32, 0) => "u32", + _ => { + writeln!(out, "/// UNIMPLEMENTED {}", typemember.traits.numeric.scalar.width)?; + "unimplemented" + } + } + } else { + writeln!(out, "/// UNIMPLEMENTED")?; + "unimplemented" + }; + + if typemember.type_flags.contains(ReflectTypeFlags::MATRIX) { + writeln!( + out, + " {}: [[{}; {}]; {}],", + blockmember.name, + base_type, + typemember.traits.numeric.matrix.column_count, + typemember.traits.numeric.matrix.row_count + )?; + } else if typemember.type_flags.contains(ReflectTypeFlags::VECTOR) { + //Represent vectors as arrays. + writeln!( + out, + " {}: [{}; {}],", + blockmember.name, base_type, typemember.traits.numeric.vector.component_count + )?; + } else { + //Single + writeln!(out, " {}: {},", blockmember.name, base_type)?; + } + } + + writeln!(out, "}}")?; + writeln!(out)?; + + writeln!(out, "impl {} {{", typevar.type_name)?; + writeln!(out, " fn into_uniform_buffer(self) -> [u8; {}] {{", blockvar.size)?; + writeln!(out, " let mut out = [0; {}];", blockvar.size)?; + + for (blockmember, typemember) in blockvar.members.iter().zip(typevar.members.iter()) { + if typemember.type_flags.contains(ReflectTypeFlags::MATRIX) { + writeln!( + out, + " out[{}..{}].copy_from_slice(&self.{}.iter().map(|c| c.iter().map(|c2| c2.to_ne_bytes()).flatten()).flatten().collect::>());", + blockmember.offset, + blockmember.offset + blockmember.size, + blockmember.name + )?; + } else if typemember.type_flags.contains(ReflectTypeFlags::VECTOR) { + writeln!( + out, + " out[{}..{}].copy_from_slice(&self.{}.iter().map(|c| c.to_ne_bytes()).flatten().collect::>());", + blockmember.offset, + blockmember.offset + blockmember.size, + blockmember.name + )?; + } else { + writeln!( + out, + " out[{}..{}].copy_from_slice(&self.{}.to_ne_bytes());", + blockmember.offset, + blockmember.offset + blockmember.size, + blockmember.name + )?; + } + } + + writeln!(out, " out")?; + writeln!(out, " }}")?; + writeln!(out, "}}")?; + writeln!(out)?; + + Ok(()) } -fn introspect_spirv(out: &mut String, snake_case_name: &str, filepath: &str, module: &spirv_reflect::ShaderModule) -> Result<(), Box> { - writeln!(out, "use wgpu;")?; - writeln!(out, "use wgpu::include_spirv;")?; - - for entrypoint in module.enumerate_entry_points()? { - writeln!(out, "/// Entry point {}", entrypoint.name)?; - writeln!(out, "/// Execution model {:?}", entrypoint.spirv_execution_model)?; - writeln!(out, "/// Shader stage {:?}", entrypoint.shader_stage)?; - - // Most of these are stubs. - // We will eventually have this print Rust structs and consts. - for var in entrypoint.input_variables { - writeln!(out, "/// input {}", var.name)?; - writeln!(out, "/// location {}", var.location)?; - writeln!(out, "/// semantic {}", var.semantic)?; - writeln!(out, "/// Decoration Flags: {:?}", var.decoration_flags)?; - writeln!(out, "/// Builtins: {:?}", var.built_in)?; - writeln!(out, "/// Format: {:?}", var.format)?; - writeln!(out, "/// members:")?; - - for var in var.members { - writeln!(out, " /// {}", var.name)?; - writeln!(out, " /// location {}", var.location)?; - writeln!(out, " /// semantic {}", var.semantic)?; - writeln!(out, " /// Decoration Flags: {:?}", var.decoration_flags)?; - writeln!(out, " /// Builtins: {:?}", var.built_in)?; - writeln!(out, " /// Format: {:?}", var.format)?; - } - writeln!(out, "/// END members:")?; - } - - for var in entrypoint.output_variables { - writeln!(out, "/// output {}", var.name)?; - writeln!(out, "/// location {}", var.location)?; - writeln!(out, "/// semantic {}", var.semantic)?; - writeln!(out, "/// Decoration Flags: {:?}", var.decoration_flags)?; - writeln!(out, "/// Builtins: {:?}", var.built_in)?; - writeln!(out, "/// Format: {:?}", var.format)?; - writeln!(out, "/// members:")?; - - for var in var.members { - writeln!(out, " /// {}", var.name)?; - writeln!(out, " /// location {}", var.location)?; - writeln!(out, " /// semantic {}", var.semantic)?; - writeln!(out, " /// Decoration Flags: {:?}", var.decoration_flags)?; - writeln!(out, " /// Builtins: {:?}", var.built_in)?; - writeln!(out, " /// Format: {:?}", var.format)?; - } - writeln!(out, "/// END members:")?; - } - - for descriptor_set in entrypoint.descriptor_sets { - writeln!(out, "/// descriptor set {}", descriptor_set.set)?; - - for binding in descriptor_set.bindings { - writeln!(out, "/// descriptor {} (binding {})", binding.name, binding.binding)?; - - match binding.descriptor_type { - ReflectDescriptorType::UniformBuffer => { - if let Some(typevar) = binding.type_description { - writeln!(out, "/// UNIFORM BUFFER of type {}", typevar.type_name)?; - writeln!(out, "/// Struct member name: {}", typevar.struct_member_name)?; - writeln!(out, "/// Storage class: {:?}", typevar.storage_class)?; - writeln!(out, "/// Type Flags: {:?}", typevar.type_flags)?; - writeln!(out, "/// Decoration Flags: {:?}", typevar.decoration_flags)?; - writeln!(out, "/// Traits: {:?}", typevar.traits)?; - describe_block_struct(out, &binding.block, &typevar)?; - } else { - writeln!(out, "/// UNIFORM BUFFER of unknown type name {}", binding.name)?; - writeln!(out, "const BINDING_{}: u32 = {};", binding.name.to_uppercase(), binding.binding)?; - } - } - _ => { - writeln!(out, "/// unknown type {:?}", binding.descriptor_type)?; - writeln!(out, "const BINDING_{}: u32 = {};", binding.name.to_uppercase(), binding.binding)?; - } - } - } - } - - for uniform_id in entrypoint.used_uniforms { - writeln!(out, "/// uniform ID {}", uniform_id)?; - } - - for uniform_id in entrypoint.used_push_constants { - writeln!(out, "/// push constant ID {}", uniform_id)?; - } - - writeln!(out)?; - writeln!(out, "const {} : wgpu::ShaderModuleDescriptor = include_spirv!(\"{}\");", snake_case_name, filepath.replace("\\", "\\\\"))?; - writeln!(out)?; - writeln!(out, "pub struct Shader {{")?; - writeln!(out, " {}: wgpu::ShaderModule", entrypoint.name)?; - writeln!(out, "}}")?; - writeln!(out)?; - writeln!(out, "impl Shader {{")?; - writeln!(out, " pub fn new(device: &wgpu::Device) -> Self {{")?; - writeln!(out, " Self {{")?; - writeln!(out, " {}: device.create_shader_module({})", entrypoint.name, snake_case_name)?; - writeln!(out, " }}")?; - writeln!(out, " }}")?; - writeln!(out, "}}")?; - } - - Ok(()) +fn introspect_spirv( + out: &mut String, + snake_case_name: &str, + filepath: &str, + module: &spirv_reflect::ShaderModule, +) -> Result<(), Box> { + writeln!(out, "use wgpu;")?; + writeln!(out, "use wgpu::include_spirv;")?; + + for entrypoint in module.enumerate_entry_points()? { + writeln!(out, "/// Entry point {}", entrypoint.name)?; + writeln!(out, "/// Execution model {:?}", entrypoint.spirv_execution_model)?; + writeln!(out, "/// Shader stage {:?}", entrypoint.shader_stage)?; + + // Most of these are stubs. + // We will eventually have this print Rust structs and consts. + for var in entrypoint.input_variables { + writeln!(out, "/// input {}", var.name)?; + writeln!(out, "/// location {}", var.location)?; + writeln!(out, "/// semantic {}", var.semantic)?; + writeln!(out, "/// Decoration Flags: {:?}", var.decoration_flags)?; + writeln!(out, "/// Builtins: {:?}", var.built_in)?; + writeln!(out, "/// Format: {:?}", var.format)?; + writeln!(out, "/// members:")?; + + for var in var.members { + writeln!(out, " /// {}", var.name)?; + writeln!(out, " /// location {}", var.location)?; + writeln!(out, " /// semantic {}", var.semantic)?; + writeln!(out, " /// Decoration Flags: {:?}", var.decoration_flags)?; + writeln!(out, " /// Builtins: {:?}", var.built_in)?; + writeln!(out, " /// Format: {:?}", var.format)?; + } + writeln!(out, "/// END members:")?; + } + + for var in entrypoint.output_variables { + writeln!(out, "/// output {}", var.name)?; + writeln!(out, "/// location {}", var.location)?; + writeln!(out, "/// semantic {}", var.semantic)?; + writeln!(out, "/// Decoration Flags: {:?}", var.decoration_flags)?; + writeln!(out, "/// Builtins: {:?}", var.built_in)?; + writeln!(out, "/// Format: {:?}", var.format)?; + writeln!(out, "/// members:")?; + + for var in var.members { + writeln!(out, " /// {}", var.name)?; + writeln!(out, " /// location {}", var.location)?; + writeln!(out, " /// semantic {}", var.semantic)?; + writeln!(out, " /// Decoration Flags: {:?}", var.decoration_flags)?; + writeln!(out, " /// Builtins: {:?}", var.built_in)?; + writeln!(out, " /// Format: {:?}", var.format)?; + } + writeln!(out, "/// END members:")?; + } + + for descriptor_set in entrypoint.descriptor_sets { + writeln!(out, "/// descriptor set {}", descriptor_set.set)?; + + for binding in descriptor_set.bindings { + writeln!(out, "/// descriptor {} (binding {})", binding.name, binding.binding)?; + + match binding.descriptor_type { + ReflectDescriptorType::UniformBuffer => { + if let Some(typevar) = binding.type_description { + writeln!(out, "/// UNIFORM BUFFER of type {}", typevar.type_name)?; + writeln!(out, "/// Struct member name: {}", typevar.struct_member_name)?; + writeln!(out, "/// Storage class: {:?}", typevar.storage_class)?; + writeln!(out, "/// Type Flags: {:?}", typevar.type_flags)?; + writeln!(out, "/// Decoration Flags: {:?}", typevar.decoration_flags)?; + writeln!(out, "/// Traits: {:?}", typevar.traits)?; + describe_block_struct(out, &binding.block, &typevar)?; + } else { + writeln!(out, "/// UNIFORM BUFFER of unknown type name {}", binding.name)?; + writeln!( + out, + "const BINDING_{}: u32 = {};", + binding.name.to_uppercase(), + binding.binding + )?; + } + } + _ => { + writeln!(out, "/// unknown type {:?}", binding.descriptor_type)?; + writeln!( + out, + "const BINDING_{}: u32 = {};", + binding.name.to_uppercase(), + binding.binding + )?; + } + } + } + } + + for uniform_id in entrypoint.used_uniforms { + writeln!(out, "/// uniform ID {}", uniform_id)?; + } + + for uniform_id in entrypoint.used_push_constants { + writeln!(out, "/// push constant ID {}", uniform_id)?; + } + + writeln!(out)?; + writeln!( + out, + "const {} : wgpu::ShaderModuleDescriptor = include_spirv!(\"{}\");", + snake_case_name, + filepath.replace("\\", "\\\\") + )?; + writeln!(out)?; + writeln!(out, "pub struct Shader {{")?; + writeln!(out, " {}: wgpu::ShaderModule", entrypoint.name)?; + writeln!(out, "}}")?; + writeln!(out)?; + writeln!(out, "impl Shader {{")?; + writeln!(out, " pub fn new(device: &wgpu::Device) -> Self {{")?; + writeln!(out, " Self {{")?; + writeln!( + out, + " {}: device.create_shader_module({})", + entrypoint.name, snake_case_name + )?; + writeln!(out, " }}")?; + writeln!(out, " }}")?; + writeln!(out, "}}")?; + } + + Ok(()) } -fn compile_dir(shader_path: &path::Path, output_path: &path::Path, compiler: &shaderc::Compiler, options: &shaderc::CompileOptions, parent_module_rust_src: &mut String) -> Result<(), Box> { - for entry in fs::read_dir(shader_path)? { - let entry = entry?; - - let in_path = entry.path(); - let item_filename = in_path.file_name().expect("file to have name"); - let new_out_path = output_path.join(item_filename); - - if entry.file_type()?.is_file() { - let shaderkind = in_path - .extension() - .and_then(|ext| match ext.to_string_lossy().as_ref() { - "vert" => Some(shaderc::ShaderKind::Vertex), - "frag" => Some(shaderc::ShaderKind::Fragment), - _ => None - }); - - if let Some(shaderkind) = shaderkind { - let source_text = fs::read_to_string(&in_path)?; - let binary = compiler.compile_into_spirv( - &source_text, - shaderkind, - &in_path.file_name().map(|o| o.to_string_lossy()).unwrap_or(Cow::Borrowed("source.glsl")), - "main", - Some(&options), - )?; - let data = binary.as_binary_u8(); - - let out_path = new_out_path.with_extension(match shaderkind { - shaderc::ShaderKind::Vertex => "vert.spv", - shaderc::ShaderKind::Fragment => "frag.spv", - _ => unreachable!() - }); - - fs::create_dir_all(out_path.parent().expect("file to have dir"))?; - fs::write(&out_path, &data)?; - - let reflection = spirv_reflect::ShaderModule::load_u8_data(&data)?; - let filename_but_with_the_shaderkind = { - let kind = match shaderkind { - shaderc::ShaderKind::Vertex => "_vert", - shaderc::ShaderKind::Fragment => "_frag", - _ => unreachable!() - }; - let extless = in_path.with_extension("").file_name().expect("ya gotta have a filename").to_string_lossy().replace("-", "_"); - let mut ret = OsString::with_capacity(extless.len() + kind.len()); - ret.push(extless); - ret.push(kind); - ret - }; - let reflect_out_path = in_path.with_file_name(filename_but_with_the_shaderkind.clone()).with_extension("rs"); - - let mut reflect_data = String::new(); - - writeln!(&mut reflect_data, "/// Automatically generated introspection data for {}", item_filename.to_string_lossy())?; - let snake_case_name = filename_but_with_the_shaderkind.to_string_lossy(); - writeln!(parent_module_rust_src, "pub mod {};", snake_case_name)?; - - let snake_case_name = snake_case_name.to_uppercase(); - introspect_spirv(&mut reflect_data, &snake_case_name, &out_path.to_string_lossy(), &reflection)?; - - fs::write(&reflect_out_path, reflect_data)?; - } - } else if entry.file_type()?.is_dir() { - let snake_case_name = item_filename.to_string_lossy(); - writeln!(parent_module_rust_src, "pub mod {};", snake_case_name)?; - - let corresponding_mod_file = in_path.with_extension("rs"); - let mut rust_src = "/// AUTO GENERATED SOURCE DO NOT EDIT\n".to_string(); - - compile_dir(&in_path, &new_out_path, compiler, options, &mut rust_src)?; - - fs::write(&corresponding_mod_file, rust_src)?; - } - } - - Ok(()) +fn compile_dir( + shader_path: &path::Path, + output_path: &path::Path, + compiler: &shaderc::Compiler, + options: &shaderc::CompileOptions, + parent_module_rust_src: &mut String, +) -> Result<(), Box> { + for entry in fs::read_dir(shader_path)? { + let entry = entry?; + + let in_path = entry.path(); + let item_filename = in_path.file_name().expect("file to have name"); + let new_out_path = output_path.join(item_filename); + + if entry.file_type()?.is_file() { + let shaderkind = in_path + .extension() + .and_then(|ext| match ext.to_string_lossy().as_ref() { + "vert" => Some(shaderc::ShaderKind::Vertex), + "frag" => Some(shaderc::ShaderKind::Fragment), + _ => None, + }); + + if let Some(shaderkind) = shaderkind { + let source_text = fs::read_to_string(&in_path)?; + let binary = compiler.compile_into_spirv( + &source_text, + shaderkind, + &in_path + .file_name() + .map(|o| o.to_string_lossy()) + .unwrap_or(Cow::Borrowed("source.glsl")), + "main", + Some(&options), + )?; + let data = binary.as_binary_u8(); + + let out_path = new_out_path.with_extension(match shaderkind { + shaderc::ShaderKind::Vertex => "vert.spv", + shaderc::ShaderKind::Fragment => "frag.spv", + _ => unreachable!(), + }); + + fs::create_dir_all(out_path.parent().expect("file to have dir"))?; + fs::write(&out_path, &data)?; + + let reflection = spirv_reflect::ShaderModule::load_u8_data(&data)?; + let filename_but_with_the_shaderkind = { + let kind = match shaderkind { + shaderc::ShaderKind::Vertex => "_vert", + shaderc::ShaderKind::Fragment => "_frag", + _ => unreachable!(), + }; + let extless = in_path + .with_extension("") + .file_name() + .expect("ya gotta have a filename") + .to_string_lossy() + .replace("-", "_"); + let mut ret = OsString::with_capacity(extless.len() + kind.len()); + ret.push(extless); + ret.push(kind); + ret + }; + let reflect_out_path = in_path + .with_file_name(filename_but_with_the_shaderkind.clone()) + .with_extension("rs"); + + let mut reflect_data = String::new(); + + writeln!( + &mut reflect_data, + "/// Automatically generated introspection data for {}", + item_filename.to_string_lossy() + )?; + let snake_case_name = filename_but_with_the_shaderkind.to_string_lossy(); + writeln!(parent_module_rust_src, "pub mod {};", snake_case_name)?; + + let snake_case_name = snake_case_name.to_uppercase(); + introspect_spirv( + &mut reflect_data, + &snake_case_name, + &out_path.to_string_lossy(), + &reflection, + )?; + + fs::write(&reflect_out_path, reflect_data)?; + } + } else if entry.file_type()?.is_dir() { + let snake_case_name = item_filename.to_string_lossy(); + writeln!(parent_module_rust_src, "pub mod {};", snake_case_name)?; + + let corresponding_mod_file = in_path.with_extension("rs"); + let mut rust_src = "/// AUTO GENERATED SOURCE DO NOT EDIT\n".to_string(); + + compile_dir(&in_path, &new_out_path, compiler, options, &mut rust_src)?; + + fs::write(&corresponding_mod_file, rust_src)?; + } + } + + Ok(()) } /// Build script to compile GLSL shaders from the OpenGL version into SPIR-V @@ -249,19 +360,19 @@ fn compile_dir(shader_path: &path::Path, output_path: &path::Path, compiler: &sh /// Due to stupid Apple nonsense, we'll probably also need to compile to WGSL /// at some point. fn main() -> Result<(), Box> { - let shader_path = path::absolute("src/shaders")?; - let output_path = path::absolute("build/spirv")?; + let shader_path = path::absolute("src/shaders")?; + let output_path = path::absolute("build/spirv")?; - println!("cargo:rerun-if-changed={}", shader_path.to_string_lossy()); + println!("cargo:rerun-if-changed={}", shader_path.to_string_lossy()); - let compiler = shaderc::Compiler::new()?; - let options = shaderc::CompileOptions::new()?; + let compiler = shaderc::Compiler::new()?; + let options = shaderc::CompileOptions::new()?; - let corresponding_mod_file = shader_path.with_extension("rs"); - let mut rust_src = "/// AUTO GENERATED SOURCE DO NOT EDIT\n".to_string(); + let corresponding_mod_file = shader_path.with_extension("rs"); + let mut rust_src = "/// AUTO GENERATED SOURCE DO NOT EDIT\n".to_string(); - compile_dir(&shader_path, &output_path, &compiler, &options, &mut rust_src)?; - fs::write(&corresponding_mod_file, rust_src)?; + compile_dir(&shader_path, &output_path, &compiler, &options, &mut rust_src)?; + fs::write(&corresponding_mod_file, rust_src)?; - Ok(()) + Ok(()) } From 446a40a960a4bf8129031267e7b3e24229f1039d Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Mon, 16 Feb 2026 00:35:24 +0000 Subject: [PATCH 10/49] Add support for autogenerated bind group layouts --- inox2d-wgpu/build.rs | 322 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 263 insertions(+), 59 deletions(-) diff --git a/inox2d-wgpu/build.rs b/inox2d-wgpu/build.rs index 3f80a07..a9fc1c5 100644 --- a/inox2d-wgpu/build.rs +++ b/inox2d-wgpu/build.rs @@ -8,6 +8,78 @@ use std::ffi::OsString; use std::fmt::Write; use std::{fs, path}; +fn spirv_to_rust_type(typemember: &ReflectTypeDescription) -> Result, Box> { + let base_type = if typemember.type_flags.contains(ReflectTypeFlags::FLOAT) { + match typemember.traits.numeric.scalar.width { + 32 => "f32", + _ => "unimplemented", + } + } else if typemember.type_flags.contains(ReflectTypeFlags::INT) { + match ( + typemember.traits.numeric.scalar.width, + typemember.traits.numeric.scalar.signedness, + ) { + (8, 1) => "i8", + (16, 1) => "i16", + (32, 1) => "i32", + (8, 0) => "u8", + (16, 0) => "u16", + (32, 0) => "u32", + _ => "unimplemented", + } + } else { + "unimplemented" + }; + + if typemember.type_flags.contains(ReflectTypeFlags::MATRIX) { + Ok(format!( + "[[{}; {}]; {}]", + base_type, typemember.traits.numeric.matrix.column_count, typemember.traits.numeric.matrix.row_count + ) + .into()) + } else if typemember.type_flags.contains(ReflectTypeFlags::VECTOR) { + //Represent vectors as arrays. + Ok(format!("[{}; {}]", base_type, typemember.traits.numeric.vector.component_count).into()) + } else { + //Single + Ok(base_type.into()) + } +} + +fn spirv_to_wgpu_vertex_format(typemember: &ReflectTypeDescription) -> Result, Box> { + let base_type = if typemember.type_flags.contains(ReflectTypeFlags::FLOAT) { + match typemember.traits.numeric.scalar.width { + 32 => "Float32", + _ => "unimplemented", + } + } else if typemember.type_flags.contains(ReflectTypeFlags::INT) { + match ( + typemember.traits.numeric.scalar.width, + typemember.traits.numeric.scalar.signedness, + ) { + (8, 1) => "Sint8", + (16, 1) => "Sint16", + (32, 1) => "Sint32", + (8, 0) => "Uint8", + (16, 0) => "Uint16", + (32, 0) => "Uint32", + _ => "unimplemented", + } + } else { + "unimplemented" + }; + + if typemember.type_flags.contains(ReflectTypeFlags::MATRIX) { + Ok("matrix not supported".into()) + } else if typemember.type_flags.contains(ReflectTypeFlags::VECTOR) { + //Represent vectors as arrays. + Ok(format!("{}x{}", base_type, typemember.traits.numeric.vector.component_count).into()) + } else { + //Single + Ok(base_type.into()) + } +} + fn describe_block_struct( out: &mut String, blockvar: &ReflectBlockVariable, @@ -25,55 +97,8 @@ fn describe_block_struct( writeln!(out, " /// Decoration Flags: {:?}", typemember.decoration_flags)?; writeln!(out, " /// Traits: {:?}", typemember.traits)?; - let base_type = if typemember.type_flags.contains(ReflectTypeFlags::FLOAT) { - match typemember.traits.numeric.scalar.width { - 32 => "f32", - _ => { - writeln!(out, "/// UNIMPLEMENTED {}", typemember.traits.numeric.scalar.width)?; - "unimplemented" - } - } - } else if typemember.type_flags.contains(ReflectTypeFlags::INT) { - match ( - typemember.traits.numeric.scalar.width, - typemember.traits.numeric.scalar.signedness, - ) { - (8, 1) => "i8", - (16, 1) => "i16", - (32, 1) => "i32", - (8, 0) => "u8", - (16, 0) => "u16", - (32, 0) => "u32", - _ => { - writeln!(out, "/// UNIMPLEMENTED {}", typemember.traits.numeric.scalar.width)?; - "unimplemented" - } - } - } else { - writeln!(out, "/// UNIMPLEMENTED")?; - "unimplemented" - }; - - if typemember.type_flags.contains(ReflectTypeFlags::MATRIX) { - writeln!( - out, - " {}: [[{}; {}]; {}],", - blockmember.name, - base_type, - typemember.traits.numeric.matrix.column_count, - typemember.traits.numeric.matrix.row_count - )?; - } else if typemember.type_flags.contains(ReflectTypeFlags::VECTOR) { - //Represent vectors as arrays. - writeln!( - out, - " {}: [{}; {}],", - blockmember.name, base_type, typemember.traits.numeric.vector.component_count - )?; - } else { - //Single - writeln!(out, " {}: {},", blockmember.name, base_type)?; - } + let rust_type = spirv_to_rust_type(typemember)?; + writeln!(out, " {}: {},", blockmember.name, rust_type)?; } writeln!(out, "}}")?; @@ -122,11 +147,16 @@ fn describe_block_struct( fn introspect_spirv( out: &mut String, snake_case_name: &str, + filename: &str, filepath: &str, module: &spirv_reflect::ShaderModule, ) -> Result<(), Box> { + writeln!(out, "/// Automatically generated introspection data for {}", filename)?; + writeln!(out, "use wgpu;")?; writeln!(out, "use wgpu::include_spirv;")?; + writeln!(out)?; + writeln!(out, "use std::num::NonZero;")?; for entrypoint in module.enumerate_entry_points()? { writeln!(out, "/// Entry point {}", entrypoint.name)?; @@ -135,7 +165,7 @@ fn introspect_spirv( // Most of these are stubs. // We will eventually have this print Rust structs and consts. - for var in entrypoint.input_variables { + for var in &entrypoint.input_variables { writeln!(out, "/// input {}", var.name)?; writeln!(out, "/// location {}", var.location)?; writeln!(out, "/// semantic {}", var.semantic)?; @@ -144,7 +174,7 @@ fn introspect_spirv( writeln!(out, "/// Format: {:?}", var.format)?; writeln!(out, "/// members:")?; - for var in var.members { + for var in &var.members { writeln!(out, " /// {}", var.name)?; writeln!(out, " /// location {}", var.location)?; writeln!(out, " /// semantic {}", var.semantic)?; @@ -153,6 +183,12 @@ fn introspect_spirv( writeln!(out, " /// Format: {:?}", var.format)?; } writeln!(out, "/// END members:")?; + writeln!( + out, + "const INPUT_LOCATION_{}: u32 = {};", + var.name.to_uppercase(), + var.location + )?; } for var in entrypoint.output_variables { @@ -173,17 +209,23 @@ fn introspect_spirv( writeln!(out, " /// Format: {:?}", var.format)?; } writeln!(out, "/// END members:")?; + writeln!( + out, + "const OUTPUT_LOCATION_{}: u32 = {};", + var.name.to_uppercase(), + var.location + )?; } - for descriptor_set in entrypoint.descriptor_sets { + for descriptor_set in &entrypoint.descriptor_sets { writeln!(out, "/// descriptor set {}", descriptor_set.set)?; - for binding in descriptor_set.bindings { + for binding in &descriptor_set.bindings { writeln!(out, "/// descriptor {} (binding {})", binding.name, binding.binding)?; match binding.descriptor_type { ReflectDescriptorType::UniformBuffer => { - if let Some(typevar) = binding.type_description { + if let Some(typevar) = &binding.type_description { writeln!(out, "/// UNIFORM BUFFER of type {}", typevar.type_name)?; writeln!(out, "/// Struct member name: {}", typevar.struct_member_name)?; writeln!(out, "/// Storage class: {:?}", typevar.storage_class)?; @@ -191,6 +233,12 @@ fn introspect_spirv( writeln!(out, "/// Decoration Flags: {:?}", typevar.decoration_flags)?; writeln!(out, "/// Traits: {:?}", typevar.traits)?; describe_block_struct(out, &binding.block, &typevar)?; + writeln!( + out, + "const BINDING_{}: u32 = {};", + binding.name.to_uppercase(), + binding.binding + )?; } else { writeln!(out, "/// UNIFORM BUFFER of unknown type name {}", binding.name)?; writeln!( @@ -244,6 +292,166 @@ fn introspect_spirv( )?; writeln!(out, " }}")?; writeln!(out, " }}")?; + + //TODO: What about vert/frag visible uniform blocks? + let visibility = if entrypoint + .shader_stage + .contains(spirv_reflect::types::ReflectShaderStageFlags::VERTEX) + { + "wgpu::ShaderStages::VERTEX" + } else if entrypoint + .shader_stage + .contains(spirv_reflect::types::ReflectShaderStageFlags::FRAGMENT) + { + "wgpu::ShaderStages::FRAGMENT" + } else { + "wgpu::ShaderStages::NONE" + }; + + writeln!(out)?; + writeln!( + out, + " fn bind_group_layout(self, device: &wgpu::Device) -> wgpu::BindGroupLayout {{" + )?; + writeln!( + out, + " device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {{" + )?; + writeln!(out, " entries: &[")?; + for descriptor_set in entrypoint.descriptor_sets { + writeln!(out, " // descriptor set {}", descriptor_set.set)?; + for binding in descriptor_set.bindings { + writeln!(out, " wgpu::BindGroupLayoutEntry {{")?; + writeln!(out, " binding: {},", binding.binding)?; + writeln!(out, " count: None,")?; //TODO: Array support + writeln!(out, " visibility: {},", visibility)?; + + match binding.descriptor_type { + ReflectDescriptorType::UniformBuffer => { + writeln!(out, " ty: wgpu::BindingType::Buffer {{")?; + writeln!(out, " ty: wgpu::BufferBindingType::Uniform,")?; + writeln!(out, " has_dynamic_offset: false,")?; + + if binding.block.size > 0 { + writeln!( + out, + " min_binding_size: Some(NonZero::new({}).expect(\"nonzero type\")),", + binding.block.size + )?; + } else { + writeln!(out, " min_binding_size: None,")?; + } + writeln!(out, " }},")?; + } + + ReflectDescriptorType::Sampler | ReflectDescriptorType::CombinedImageSampler => { + //TODO: How do we ask what filtering type to use? + writeln!( + out, + " ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering)," + )?; + } + + //TODO: generate bindings for all of these + ReflectDescriptorType::Undefined + | ReflectDescriptorType::SampledImage + | ReflectDescriptorType::StorageImage + | ReflectDescriptorType::UniformTexelBuffer + | ReflectDescriptorType::StorageTexelBuffer + | ReflectDescriptorType::StorageBuffer + | ReflectDescriptorType::UniformBufferDynamic + | ReflectDescriptorType::StorageBufferDynamic + | ReflectDescriptorType::InputAttachment + | ReflectDescriptorType::AccelerationStructureKHR => { + writeln!(out, "///TODO: Unknown type {:?}", binding.descriptor_type)?; + } + } + + writeln!(out, " }},")?; + } + } + writeln!(out, " ],")?; + writeln!(out, " label: Some(\"{}::{}\")", filename, entrypoint.name)?; + writeln!(out, " }})")?; + writeln!(out, " }}")?; + + if entrypoint + .shader_stage + .contains(spirv_reflect::types::ReflectShaderStageFlags::VERTEX) + { + writeln!(out)?; + writeln!(out, " pub fn as_vertex_stage(&self) -> wgpu::VertexState {{")?; + writeln!(out, " wgpu::VertexState {{")?; + writeln!(out, " module: &self.{},", entrypoint.name)?; + writeln!(out, " entry_point: Some(\"{}\"),", entrypoint.name)?; + writeln!(out, " buffers: &[")?; + + for (index, input) in entrypoint.input_variables.iter().enumerate() { + //TODO: This creates one buffer per input, since that matches + //how inox2d-opengl used its buffers. + //In the future we may want packed buffers??? + let is_last = index == entrypoint.input_variables.len() - 1; + + if let Some(typedesc) = &input.type_description { + let rust_type = spirv_to_rust_type(&typedesc)?; + let comma = if is_last { "" } else { "," }; + let vertex_format = spirv_to_wgpu_vertex_format(&typedesc)?; + + writeln!(out, " wgpu::VertexBufferLayout {{")?; + writeln!( + out, + " array_stride: std::mem::size_of::<{}>() as wgpu::BufferAddress,", + rust_type + )?; + writeln!(out, " step_mode: wgpu::VertexStepMode::Vertex,")?; + writeln!(out, " attributes: &[")?; + writeln!(out, " wgpu::VertexAttribute {{")?; + writeln!(out, " offset: 0,")?; + writeln!( + out, + " shader_location: INPUT_LOCATION_{},", + input.name.to_uppercase() + )?; + writeln!( + out, + " format: wgpu::VertexFormat::{}", + vertex_format + )?; + writeln!(out, " }}")?; + writeln!(out, " ]")?; + writeln!(out, " }}{}", comma)?; + } else { + writeln!(out, "/// ERROR! WHAT KIND OF BUFFER TYPE LACKS A DESCRIPTOR?!")?; + } + } + + writeln!(out, " ],")?; + writeln!( + out, + " compilation_options: wgpu::PipelineCompilationOptions::default()" + )?; + writeln!(out, " }}")?; + writeln!(out, " }}")?; + } + + if entrypoint + .shader_stage + .contains(spirv_reflect::types::ReflectShaderStageFlags::FRAGMENT) + { + writeln!(out)?; + writeln!(out, " pub fn as_fragment_stage(&self) -> wgpu::FragmentState {{")?; + writeln!(out, " wgpu::FragmentState {{")?; + writeln!(out, " module: &self.{},", entrypoint.name)?; + writeln!(out, " entry_point: Some(\"{}\"),", entrypoint.name)?; + writeln!(out, " targets: &[],")?; + writeln!( + out, + " compilation_options: wgpu::PipelineCompilationOptions::default()" + )?; + writeln!(out, " }}")?; + writeln!(out, " }}")?; + } + writeln!(out, "}}")?; } @@ -320,11 +528,6 @@ fn compile_dir( let mut reflect_data = String::new(); - writeln!( - &mut reflect_data, - "/// Automatically generated introspection data for {}", - item_filename.to_string_lossy() - )?; let snake_case_name = filename_but_with_the_shaderkind.to_string_lossy(); writeln!(parent_module_rust_src, "pub mod {};", snake_case_name)?; @@ -332,6 +535,7 @@ fn compile_dir( introspect_spirv( &mut reflect_data, &snake_case_name, + &item_filename.to_string_lossy(), &out_path.to_string_lossy(), &reflection, )?; From 5fbe0e9a3b9ce71f25606cd77298d270fd0595b2 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Mon, 16 Feb 2026 01:49:07 +0000 Subject: [PATCH 11/49] Shader blocks should write to a user-provided buffer slice --- inox2d-wgpu/build.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/inox2d-wgpu/build.rs b/inox2d-wgpu/build.rs index a9fc1c5..789737f 100644 --- a/inox2d-wgpu/build.rs +++ b/inox2d-wgpu/build.rs @@ -105,8 +105,7 @@ fn describe_block_struct( writeln!(out)?; writeln!(out, "impl {} {{", typevar.type_name)?; - writeln!(out, " fn into_uniform_buffer(self) -> [u8; {}] {{", blockvar.size)?; - writeln!(out, " let mut out = [0; {}];", blockvar.size)?; + writeln!(out, " fn write_buffer(self, out: &mut [u8; {}]) {{", blockvar.size)?; for (blockmember, typemember) in blockvar.members.iter().zip(typevar.members.iter()) { if typemember.type_flags.contains(ReflectTypeFlags::MATRIX) { @@ -136,7 +135,6 @@ fn describe_block_struct( } } - writeln!(out, " out")?; writeln!(out, " }}")?; writeln!(out, "}}")?; writeln!(out)?; From 6305f51c2a228a25f60a7f38dff565efc5252303 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Mon, 16 Feb 2026 03:33:06 +0000 Subject: [PATCH 12/49] Add bind method to generate bind groups for shaders --- inox2d-wgpu/build.rs | 436 +++++++++++++++++++++++++++---------------- 1 file changed, 280 insertions(+), 156 deletions(-) diff --git a/inox2d-wgpu/build.rs b/inox2d-wgpu/build.rs index 789737f..0de588b 100644 --- a/inox2d-wgpu/build.rs +++ b/inox2d-wgpu/build.rs @@ -1,6 +1,8 @@ use shaderc; use spirv_reflect; -use spirv_reflect::types::{ReflectBlockVariable, ReflectDescriptorType, ReflectTypeDescription, ReflectTypeFlags}; +use spirv_reflect::types::{ + ReflectBlockVariable, ReflectDescriptorType, ReflectEntryPoint, ReflectTypeDescription, ReflectTypeFlags, +}; use std::borrow::Cow; use std::error::Error; @@ -142,6 +144,273 @@ fn describe_block_struct( Ok(()) } +fn gen_shader_new( + out: &mut String, + snake_case_name: &str, + filename: &str, + entrypoint: &ReflectEntryPoint, +) -> Result<(), Box> { + //TODO: What about vert/frag visible uniform blocks? + let visibility = if entrypoint + .shader_stage + .contains(spirv_reflect::types::ReflectShaderStageFlags::VERTEX) + { + "wgpu::ShaderStages::VERTEX" + } else if entrypoint + .shader_stage + .contains(spirv_reflect::types::ReflectShaderStageFlags::FRAGMENT) + { + "wgpu::ShaderStages::FRAGMENT" + } else { + "wgpu::ShaderStages::NONE" + }; + + writeln!(out, " pub fn new(device: &wgpu::Device) -> Self {{")?; + writeln!(out, " Self {{")?; + writeln!( + out, + " {}: device.create_shader_module({}),", + entrypoint.name, snake_case_name + )?; + writeln!( + out, + " bindgroup_layout: device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {{" + )?; + writeln!(out, " entries: &[")?; + for descriptor_set in &entrypoint.descriptor_sets { + writeln!(out, " // descriptor set {}", descriptor_set.set)?; + for binding in &descriptor_set.bindings { + writeln!(out, " wgpu::BindGroupLayoutEntry {{")?; + writeln!( + out, + " binding: BINDING_{},", + binding.name.to_uppercase() + )?; + writeln!(out, " count: None,")?; //TODO: Array support + writeln!(out, " visibility: {},", visibility)?; + + match binding.descriptor_type { + ReflectDescriptorType::UniformBuffer => { + writeln!(out, " ty: wgpu::BindingType::Buffer {{")?; + writeln!(out, " ty: wgpu::BufferBindingType::Uniform,")?; + writeln!(out, " has_dynamic_offset: false,")?; + + if binding.block.size > 0 { + writeln!( + out, + " min_binding_size: Some(NonZero::new({}).expect(\"nonzero type\")),", + binding.block.size + )?; + } else { + writeln!(out, " min_binding_size: None,")?; + } + writeln!(out, " }},")?; + } + + ReflectDescriptorType::Sampler | ReflectDescriptorType::CombinedImageSampler => { + //TODO: How do we ask what filtering type to use? + writeln!( + out, + " ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering)," + )?; + } + + //TODO: generate bindings for all of these + ReflectDescriptorType::Undefined + | ReflectDescriptorType::SampledImage + | ReflectDescriptorType::StorageImage + | ReflectDescriptorType::UniformTexelBuffer + | ReflectDescriptorType::StorageTexelBuffer + | ReflectDescriptorType::StorageBuffer + | ReflectDescriptorType::UniformBufferDynamic + | ReflectDescriptorType::StorageBufferDynamic + | ReflectDescriptorType::InputAttachment + | ReflectDescriptorType::AccelerationStructureKHR => { + writeln!(out, "///TODO: Unknown type {:?}", binding.descriptor_type)?; + } + } + + writeln!(out, " }},")?; + } + } + writeln!(out, " ],")?; + writeln!( + out, + " label: Some(\"{}::{}\")", + filename, entrypoint.name + )?; + writeln!(out, " }})")?; + writeln!(out, " }}")?; + writeln!(out, " }}")?; + + Ok(()) +} + +/// Generate the code to create a bindgroup for a given shader entrypoint. +fn gen_shader_bind(out: &mut String, filename: &str, entrypoint: &ReflectEntryPoint) -> Result<(), Box> { + let mut bind_params = String::new(); + for descriptor_set in &entrypoint.descriptor_sets { + for binding in &descriptor_set.bindings { + match binding.descriptor_type { + ReflectDescriptorType::UniformBuffer => { + write!(&mut bind_params, ", {}: &wgpu::Buffer", binding.name)?; + } + + ReflectDescriptorType::Sampler | ReflectDescriptorType::CombinedImageSampler => { + write!(&mut bind_params, ", {}: &wgpu::Sampler", binding.name)?; + } + + //TODO: generate bindings for all of these + ReflectDescriptorType::Undefined + | ReflectDescriptorType::SampledImage + | ReflectDescriptorType::StorageImage + | ReflectDescriptorType::UniformTexelBuffer + | ReflectDescriptorType::StorageTexelBuffer + | ReflectDescriptorType::StorageBuffer + | ReflectDescriptorType::UniformBufferDynamic + | ReflectDescriptorType::StorageBufferDynamic + | ReflectDescriptorType::InputAttachment + | ReflectDescriptorType::AccelerationStructureKHR => { + writeln!(out, "///TODO: Unknown type {:?}", binding.descriptor_type)?; + } + } + } + } + + writeln!( + out, + " fn bind(self, device: &wgpu::Device{}) -> wgpu::BindGroup {{", + bind_params + )?; + writeln!(out, " device.create_bind_group(&wgpu::BindGroupDescriptor {{")?; + writeln!(out, " label: Some(\"{}::{}\"),", filename, entrypoint.name)?; + writeln!(out, " layout: &self.bindgroup_layout,")?; + writeln!(out, " entries: &[")?; + + for descriptor_set in &entrypoint.descriptor_sets { + writeln!(out, " // descriptor set {}", descriptor_set.set)?; + for binding in &descriptor_set.bindings { + writeln!(out, " wgpu::BindGroupEntry {{")?; + writeln!( + out, + " binding: BINDING_{},", + binding.name.to_uppercase() + )?; + match binding.descriptor_type { + ReflectDescriptorType::UniformBuffer => { + writeln!( + out, + " resource: {}.as_entire_binding()", + binding.name + )?; + } + + ReflectDescriptorType::Sampler | ReflectDescriptorType::CombinedImageSampler => { + writeln!( + out, + " resource: wgpu::BindingResource::Sampler({})", + binding.name + )?; + } + + //TODO: generate bindings for all of these + ReflectDescriptorType::Undefined + | ReflectDescriptorType::SampledImage + | ReflectDescriptorType::StorageImage + | ReflectDescriptorType::UniformTexelBuffer + | ReflectDescriptorType::StorageTexelBuffer + | ReflectDescriptorType::StorageBuffer + | ReflectDescriptorType::UniformBufferDynamic + | ReflectDescriptorType::StorageBufferDynamic + | ReflectDescriptorType::InputAttachment + | ReflectDescriptorType::AccelerationStructureKHR => { + writeln!(out, "///TODO: Unknown type {:?}", binding.descriptor_type)?; + } + } + writeln!(out, " }},")?; + } + } + + writeln!(out, " ]")?; + writeln!(out, " }})")?; + writeln!(out, " }}")?; + + Ok(()) +} + +fn gen_shader_vertex_stage(out: &mut String, entrypoint: &ReflectEntryPoint) -> Result<(), Box> { + writeln!(out, " pub fn as_vertex_stage(&self) -> wgpu::VertexState {{")?; + writeln!(out, " wgpu::VertexState {{")?; + writeln!(out, " module: &self.{},", entrypoint.name)?; + writeln!(out, " entry_point: Some(\"{}\"),", entrypoint.name)?; + writeln!(out, " buffers: &[")?; + + for (index, input) in entrypoint.input_variables.iter().enumerate() { + //TODO: This creates one buffer per input, since that matches + //how inox2d-opengl used its buffers. + //In the future we may want packed buffers??? + let is_last = index == entrypoint.input_variables.len() - 1; + + if let Some(typedesc) = &input.type_description { + let rust_type = spirv_to_rust_type(&typedesc)?; + let comma = if is_last { "" } else { "," }; + let vertex_format = spirv_to_wgpu_vertex_format(&typedesc)?; + + writeln!(out, " wgpu::VertexBufferLayout {{")?; + writeln!( + out, + " array_stride: std::mem::size_of::<{}>() as wgpu::BufferAddress,", + rust_type + )?; + writeln!(out, " step_mode: wgpu::VertexStepMode::Vertex,")?; + writeln!(out, " attributes: &[")?; + writeln!(out, " wgpu::VertexAttribute {{")?; + writeln!(out, " offset: 0,")?; + writeln!( + out, + " shader_location: INPUT_LOCATION_{},", + input.name.to_uppercase() + )?; + writeln!( + out, + " format: wgpu::VertexFormat::{}", + vertex_format + )?; + writeln!(out, " }}")?; + writeln!(out, " ]")?; + writeln!(out, " }}{}", comma)?; + } else { + writeln!(out, "/// ERROR! WHAT KIND OF BUFFER TYPE LACKS A DESCRIPTOR?!")?; + } + } + + writeln!(out, " ],")?; + writeln!( + out, + " compilation_options: wgpu::PipelineCompilationOptions::default()" + )?; + writeln!(out, " }}")?; + writeln!(out, " }}")?; + + Ok(()) +} + +fn gen_shader_fragment_stage(out: &mut String, entrypoint: &ReflectEntryPoint) -> Result<(), Box> { + writeln!(out, " pub fn as_fragment_stage(&self) -> wgpu::FragmentState {{")?; + writeln!(out, " wgpu::FragmentState {{")?; + writeln!(out, " module: &self.{},", entrypoint.name)?; + writeln!(out, " entry_point: Some(\"{}\"),", entrypoint.name)?; + writeln!(out, " targets: &[],")?; + writeln!( + out, + " compilation_options: wgpu::PipelineCompilationOptions::default()" + )?; + writeln!(out, " }}")?; + writeln!(out, " }}")?; + + Ok(()) +} + fn introspect_spirv( out: &mut String, snake_case_name: &str, @@ -189,7 +458,7 @@ fn introspect_spirv( )?; } - for var in entrypoint.output_variables { + for var in &entrypoint.output_variables { writeln!(out, "/// output {}", var.name)?; writeln!(out, "/// location {}", var.location)?; writeln!(out, "/// semantic {}", var.semantic)?; @@ -198,7 +467,7 @@ fn introspect_spirv( writeln!(out, "/// Format: {:?}", var.format)?; writeln!(out, "/// members:")?; - for var in var.members { + for var in &var.members { writeln!(out, " /// {}", var.name)?; writeln!(out, " /// location {}", var.location)?; writeln!(out, " /// semantic {}", var.semantic)?; @@ -260,11 +529,11 @@ fn introspect_spirv( } } - for uniform_id in entrypoint.used_uniforms { + for uniform_id in &entrypoint.used_uniforms { writeln!(out, "/// uniform ID {}", uniform_id)?; } - for uniform_id in entrypoint.used_push_constants { + for uniform_id in &entrypoint.used_push_constants { writeln!(out, "/// push constant ID {}", uniform_id)?; } @@ -277,159 +546,24 @@ fn introspect_spirv( )?; writeln!(out)?; writeln!(out, "pub struct Shader {{")?; - writeln!(out, " {}: wgpu::ShaderModule", entrypoint.name)?; + writeln!(out, " {}: wgpu::ShaderModule,", entrypoint.name)?; + writeln!(out, " bindgroup_layout: wgpu::BindGroupLayout")?; writeln!(out, "}}")?; writeln!(out)?; writeln!(out, "impl Shader {{")?; - writeln!(out, " pub fn new(device: &wgpu::Device) -> Self {{")?; - writeln!(out, " Self {{")?; - writeln!( - out, - " {}: device.create_shader_module({})", - entrypoint.name, snake_case_name - )?; - writeln!(out, " }}")?; - writeln!(out, " }}")?; - //TODO: What about vert/frag visible uniform blocks? - let visibility = if entrypoint - .shader_stage - .contains(spirv_reflect::types::ReflectShaderStageFlags::VERTEX) - { - "wgpu::ShaderStages::VERTEX" - } else if entrypoint - .shader_stage - .contains(spirv_reflect::types::ReflectShaderStageFlags::FRAGMENT) - { - "wgpu::ShaderStages::FRAGMENT" - } else { - "wgpu::ShaderStages::NONE" - }; + gen_shader_new(out, snake_case_name, filename, &entrypoint)?; writeln!(out)?; - writeln!( - out, - " fn bind_group_layout(self, device: &wgpu::Device) -> wgpu::BindGroupLayout {{" - )?; - writeln!( - out, - " device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {{" - )?; - writeln!(out, " entries: &[")?; - for descriptor_set in entrypoint.descriptor_sets { - writeln!(out, " // descriptor set {}", descriptor_set.set)?; - for binding in descriptor_set.bindings { - writeln!(out, " wgpu::BindGroupLayoutEntry {{")?; - writeln!(out, " binding: {},", binding.binding)?; - writeln!(out, " count: None,")?; //TODO: Array support - writeln!(out, " visibility: {},", visibility)?; - match binding.descriptor_type { - ReflectDescriptorType::UniformBuffer => { - writeln!(out, " ty: wgpu::BindingType::Buffer {{")?; - writeln!(out, " ty: wgpu::BufferBindingType::Uniform,")?; - writeln!(out, " has_dynamic_offset: false,")?; - - if binding.block.size > 0 { - writeln!( - out, - " min_binding_size: Some(NonZero::new({}).expect(\"nonzero type\")),", - binding.block.size - )?; - } else { - writeln!(out, " min_binding_size: None,")?; - } - writeln!(out, " }},")?; - } - - ReflectDescriptorType::Sampler | ReflectDescriptorType::CombinedImageSampler => { - //TODO: How do we ask what filtering type to use? - writeln!( - out, - " ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering)," - )?; - } - - //TODO: generate bindings for all of these - ReflectDescriptorType::Undefined - | ReflectDescriptorType::SampledImage - | ReflectDescriptorType::StorageImage - | ReflectDescriptorType::UniformTexelBuffer - | ReflectDescriptorType::StorageTexelBuffer - | ReflectDescriptorType::StorageBuffer - | ReflectDescriptorType::UniformBufferDynamic - | ReflectDescriptorType::StorageBufferDynamic - | ReflectDescriptorType::InputAttachment - | ReflectDescriptorType::AccelerationStructureKHR => { - writeln!(out, "///TODO: Unknown type {:?}", binding.descriptor_type)?; - } - } - - writeln!(out, " }},")?; - } - } - writeln!(out, " ],")?; - writeln!(out, " label: Some(\"{}::{}\")", filename, entrypoint.name)?; - writeln!(out, " }})")?; - writeln!(out, " }}")?; + gen_shader_bind(out, filename, &entrypoint)?; if entrypoint .shader_stage .contains(spirv_reflect::types::ReflectShaderStageFlags::VERTEX) { writeln!(out)?; - writeln!(out, " pub fn as_vertex_stage(&self) -> wgpu::VertexState {{")?; - writeln!(out, " wgpu::VertexState {{")?; - writeln!(out, " module: &self.{},", entrypoint.name)?; - writeln!(out, " entry_point: Some(\"{}\"),", entrypoint.name)?; - writeln!(out, " buffers: &[")?; - - for (index, input) in entrypoint.input_variables.iter().enumerate() { - //TODO: This creates one buffer per input, since that matches - //how inox2d-opengl used its buffers. - //In the future we may want packed buffers??? - let is_last = index == entrypoint.input_variables.len() - 1; - - if let Some(typedesc) = &input.type_description { - let rust_type = spirv_to_rust_type(&typedesc)?; - let comma = if is_last { "" } else { "," }; - let vertex_format = spirv_to_wgpu_vertex_format(&typedesc)?; - - writeln!(out, " wgpu::VertexBufferLayout {{")?; - writeln!( - out, - " array_stride: std::mem::size_of::<{}>() as wgpu::BufferAddress,", - rust_type - )?; - writeln!(out, " step_mode: wgpu::VertexStepMode::Vertex,")?; - writeln!(out, " attributes: &[")?; - writeln!(out, " wgpu::VertexAttribute {{")?; - writeln!(out, " offset: 0,")?; - writeln!( - out, - " shader_location: INPUT_LOCATION_{},", - input.name.to_uppercase() - )?; - writeln!( - out, - " format: wgpu::VertexFormat::{}", - vertex_format - )?; - writeln!(out, " }}")?; - writeln!(out, " ]")?; - writeln!(out, " }}{}", comma)?; - } else { - writeln!(out, "/// ERROR! WHAT KIND OF BUFFER TYPE LACKS A DESCRIPTOR?!")?; - } - } - - writeln!(out, " ],")?; - writeln!( - out, - " compilation_options: wgpu::PipelineCompilationOptions::default()" - )?; - writeln!(out, " }}")?; - writeln!(out, " }}")?; + gen_shader_vertex_stage(out, &entrypoint)?; } if entrypoint @@ -437,17 +571,7 @@ fn introspect_spirv( .contains(spirv_reflect::types::ReflectShaderStageFlags::FRAGMENT) { writeln!(out)?; - writeln!(out, " pub fn as_fragment_stage(&self) -> wgpu::FragmentState {{")?; - writeln!(out, " wgpu::FragmentState {{")?; - writeln!(out, " module: &self.{},", entrypoint.name)?; - writeln!(out, " entry_point: Some(\"{}\"),", entrypoint.name)?; - writeln!(out, " targets: &[],")?; - writeln!( - out, - " compilation_options: wgpu::PipelineCompilationOptions::default()" - )?; - writeln!(out, " }}")?; - writeln!(out, " }}")?; + gen_shader_fragment_stage(out, &entrypoint)?; } writeln!(out, "}}")?; From 04cf242adfe5c9b6fa6183c658c81e6bfc82d602 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Mon, 16 Feb 2026 03:40:00 +0000 Subject: [PATCH 13/49] Add a method to access a Shader's bind group layout --- inox2d-wgpu/build.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/inox2d-wgpu/build.rs b/inox2d-wgpu/build.rs index 0de588b..cc9ba88 100644 --- a/inox2d-wgpu/build.rs +++ b/inox2d-wgpu/build.rs @@ -411,6 +411,14 @@ fn gen_shader_fragment_stage(out: &mut String, entrypoint: &ReflectEntryPoint) - Ok(()) } +fn gen_shader_access_methods(out: &mut String) -> Result<(), Box> { + writeln!(out, " pub fn bindgroup_layout(&self) -> &wgpu::BindGroupLayout {{")?; + writeln!(out, " &self.bindgroup_layout")?; + writeln!(out, " }}")?; + + Ok(()) +} + fn introspect_spirv( out: &mut String, snake_case_name: &str, @@ -553,10 +561,10 @@ fn introspect_spirv( writeln!(out, "impl Shader {{")?; gen_shader_new(out, snake_case_name, filename, &entrypoint)?; - writeln!(out)?; - gen_shader_bind(out, filename, &entrypoint)?; + writeln!(out)?; + gen_shader_access_methods(out)?; if entrypoint .shader_stage From e2069b58c62670e47da183eb8a94098a5e1e4cb4 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Mon, 16 Feb 2026 03:56:01 +0000 Subject: [PATCH 14/49] Add traits for shaders. --- inox2d-wgpu/build.rs | 45 ++++++++++++++++++++++++++++----------- inox2d-wgpu/src/lib.rs | 1 + inox2d-wgpu/src/shader.rs | 13 +++++++++++ 3 files changed, 46 insertions(+), 13 deletions(-) create mode 100644 inox2d-wgpu/src/shader.rs diff --git a/inox2d-wgpu/build.rs b/inox2d-wgpu/build.rs index cc9ba88..72f9053 100644 --- a/inox2d-wgpu/build.rs +++ b/inox2d-wgpu/build.rs @@ -338,8 +338,13 @@ fn gen_shader_bind(out: &mut String, filename: &str, entrypoint: &ReflectEntryPo Ok(()) } -fn gen_shader_vertex_stage(out: &mut String, entrypoint: &ReflectEntryPoint) -> Result<(), Box> { - writeln!(out, " pub fn as_vertex_stage(&self) -> wgpu::VertexState {{")?; +fn gen_vertexshader_trait_methods( + out: &mut String, + entrypoint: &ReflectEntryPoint, + struct_name: &str, +) -> Result<(), Box> { + writeln!(out, "impl shader::VertexShader for {} {{", struct_name)?; + writeln!(out, " fn as_vertex_state(&self) -> wgpu::VertexState {{")?; writeln!(out, " wgpu::VertexState {{")?; writeln!(out, " module: &self.{},", entrypoint.name)?; writeln!(out, " entry_point: Some(\"{}\"),", entrypoint.name)?; @@ -391,12 +396,18 @@ fn gen_shader_vertex_stage(out: &mut String, entrypoint: &ReflectEntryPoint) -> )?; writeln!(out, " }}")?; writeln!(out, " }}")?; + writeln!(out, "}}")?; Ok(()) } -fn gen_shader_fragment_stage(out: &mut String, entrypoint: &ReflectEntryPoint) -> Result<(), Box> { - writeln!(out, " pub fn as_fragment_stage(&self) -> wgpu::FragmentState {{")?; +fn gen_fragmentshader_trait_methods( + out: &mut String, + entrypoint: &ReflectEntryPoint, + struct_name: &str, +) -> Result<(), Box> { + writeln!(out, "impl shader::FragmentShader for {} {{", struct_name)?; + writeln!(out, " fn as_fragment_state(&self) -> wgpu::FragmentState {{")?; writeln!(out, " wgpu::FragmentState {{")?; writeln!(out, " module: &self.{},", entrypoint.name)?; writeln!(out, " entry_point: Some(\"{}\"),", entrypoint.name)?; @@ -407,14 +418,17 @@ fn gen_shader_fragment_stage(out: &mut String, entrypoint: &ReflectEntryPoint) - )?; writeln!(out, " }}")?; writeln!(out, " }}")?; + writeln!(out, "}}")?; Ok(()) } -fn gen_shader_access_methods(out: &mut String) -> Result<(), Box> { - writeln!(out, " pub fn bindgroup_layout(&self) -> &wgpu::BindGroupLayout {{")?; +fn gen_shader_trait_methods(out: &mut String, struct_name: &str) -> Result<(), Box> { + writeln!(out, "impl shader::Shader for {} {{", struct_name)?; + writeln!(out, " fn bindgroup_layout(&self) -> &wgpu::BindGroupLayout {{")?; writeln!(out, " &self.bindgroup_layout")?; writeln!(out, " }}")?; + writeln!(out, "}}")?; Ok(()) } @@ -432,6 +446,8 @@ fn introspect_spirv( writeln!(out, "use wgpu::include_spirv;")?; writeln!(out)?; writeln!(out, "use std::num::NonZero;")?; + writeln!(out)?; + writeln!(out, "use crate::shader;")?; for entrypoint in module.enumerate_entry_points()? { writeln!(out, "/// Entry point {}", entrypoint.name)?; @@ -553,25 +569,30 @@ fn introspect_spirv( filepath.replace("\\", "\\\\") )?; writeln!(out)?; - writeln!(out, "pub struct Shader {{")?; + + let struct_name = "Shader"; + + writeln!(out, "pub struct {} {{", struct_name)?; writeln!(out, " {}: wgpu::ShaderModule,", entrypoint.name)?; writeln!(out, " bindgroup_layout: wgpu::BindGroupLayout")?; writeln!(out, "}}")?; writeln!(out)?; - writeln!(out, "impl Shader {{")?; + writeln!(out, "impl {} {{", struct_name)?; gen_shader_new(out, snake_case_name, filename, &entrypoint)?; writeln!(out)?; gen_shader_bind(out, filename, &entrypoint)?; + writeln!(out, "}}")?; + writeln!(out)?; - gen_shader_access_methods(out)?; + gen_shader_trait_methods(out, &struct_name)?; if entrypoint .shader_stage .contains(spirv_reflect::types::ReflectShaderStageFlags::VERTEX) { writeln!(out)?; - gen_shader_vertex_stage(out, &entrypoint)?; + gen_vertexshader_trait_methods(out, &entrypoint, &struct_name)?; } if entrypoint @@ -579,10 +600,8 @@ fn introspect_spirv( .contains(spirv_reflect::types::ReflectShaderStageFlags::FRAGMENT) { writeln!(out)?; - gen_shader_fragment_stage(out, &entrypoint)?; + gen_fragmentshader_trait_methods(out, &entrypoint, &struct_name)?; } - - writeln!(out, "}}")?; } Ok(()) diff --git a/inox2d-wgpu/src/lib.rs b/inox2d-wgpu/src/lib.rs index ce2dbf2..60ebf31 100644 --- a/inox2d-wgpu/src/lib.rs +++ b/inox2d-wgpu/src/lib.rs @@ -1,6 +1,7 @@ use wgpu; use inox2d::model::Model; +mod shader; mod shaders; #[derive(Debug, thiserror::Error)] diff --git a/inox2d-wgpu/src/shader.rs b/inox2d-wgpu/src/shader.rs new file mode 100644 index 0000000..d6cbb79 --- /dev/null +++ b/inox2d-wgpu/src/shader.rs @@ -0,0 +1,13 @@ +use wgpu; + +pub trait Shader { + fn bindgroup_layout(&self) -> &wgpu::BindGroupLayout; +} + +pub trait VertexShader: Shader { + fn as_vertex_state(&self) -> wgpu::VertexState; +} + +pub trait FragmentShader: Shader { + fn as_fragment_state(&self) -> wgpu::FragmentState; +} From d32e3a63e3c5860014582e7e5ddd3ea099540aff Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Mon, 16 Feb 2026 04:31:22 +0000 Subject: [PATCH 15/49] Turns out descriptor sets are actually very important. I've decided to make the convention call that the vertex shader is always in slot 0, and the fragment shader in slot 1. More advanced build scripts and codegen would allow other configurations of the shaders, but I'm not sure if that time is justified just for this project. --- inox2d-wgpu/build.rs | 13 +++++++++---- inox2d-wgpu/src/shaders/basic/anim.vert | 2 +- inox2d-wgpu/src/shaders/basic/basic-mask.frag | 4 ++-- inox2d-wgpu/src/shaders/basic/basic.frag | 8 ++++---- inox2d-wgpu/src/shaders/basic/basic.vert | 2 +- inox2d-wgpu/src/shaders/basic/composite-mask.frag | 4 ++-- inox2d-wgpu/src/shaders/basic/composite.frag | 8 ++++---- inox2d-wgpu/src/shaders/basic/composite.vert | 2 +- inox2d-wgpu/src/shaders/dbg.vert | 2 +- inox2d-wgpu/src/shaders/dbgline.frag | 2 +- inox2d-wgpu/src/shaders/dbgpoint.frag | 2 +- inox2d-wgpu/src/shaders/lighting.frag | 8 ++++---- inox2d-wgpu/src/shaders/mask.vert | 2 +- inox2d-wgpu/src/shaders/scene.frag | 2 +- inox2d-wgpu/src/shaders/scene.vert | 2 +- 15 files changed, 34 insertions(+), 29 deletions(-) diff --git a/inox2d-wgpu/build.rs b/inox2d-wgpu/build.rs index 72f9053..9f213b6 100644 --- a/inox2d-wgpu/build.rs +++ b/inox2d-wgpu/build.rs @@ -450,10 +450,6 @@ fn introspect_spirv( writeln!(out, "use crate::shader;")?; for entrypoint in module.enumerate_entry_points()? { - writeln!(out, "/// Entry point {}", entrypoint.name)?; - writeln!(out, "/// Execution model {:?}", entrypoint.spirv_execution_model)?; - writeln!(out, "/// Shader stage {:?}", entrypoint.shader_stage)?; - // Most of these are stubs. // We will eventually have this print Rust structs and consts. for var in &entrypoint.input_variables { @@ -513,6 +509,12 @@ fn introspect_spirv( for binding in &descriptor_set.bindings { writeln!(out, "/// descriptor {} (binding {})", binding.name, binding.binding)?; + writeln!( + out, + "const BINDINGSET_{}: u32 = {};", + binding.name.to_uppercase(), + descriptor_set.set + )?; match binding.descriptor_type { ReflectDescriptorType::UniformBuffer => { @@ -572,6 +574,9 @@ fn introspect_spirv( let struct_name = "Shader"; + writeln!(out, "/// Entry point {}", entrypoint.name)?; + writeln!(out, "/// Execution model {:?}", entrypoint.spirv_execution_model)?; + writeln!(out, "/// Shader stage {:?}", entrypoint.shader_stage)?; writeln!(out, "pub struct {} {{", struct_name)?; writeln!(out, " {}: wgpu::ShaderModule,", entrypoint.name)?; writeln!(out, " bindgroup_layout: wgpu::BindGroupLayout")?; diff --git a/inox2d-wgpu/src/shaders/basic/anim.vert b/inox2d-wgpu/src/shaders/basic/anim.vert index dac4456..569f070 100644 --- a/inox2d-wgpu/src/shaders/basic/anim.vert +++ b/inox2d-wgpu/src/shaders/basic/anim.vert @@ -6,7 +6,7 @@ */ #version 440 -layout(binding = 0) uniform Input { +layout(set = 0, binding = 0) uniform Input { mat4 mvp; vec2 offset; diff --git a/inox2d-wgpu/src/shaders/basic/basic-mask.frag b/inox2d-wgpu/src/shaders/basic/basic-mask.frag index d885b19..ce5de23 100644 --- a/inox2d-wgpu/src/shaders/basic/basic-mask.frag +++ b/inox2d-wgpu/src/shaders/basic/basic-mask.frag @@ -9,8 +9,8 @@ layout(location = 0) in vec2 texUVs; layout(location = 0) out vec4 outColor; -layout(binding = 0) uniform sampler2D tex; -layout(binding = 1) uniform Input { +layout(set = 1, binding = 0) uniform sampler2D tex; +layout(set = 1, binding = 1) uniform Input { float threshold; } uni_in; diff --git a/inox2d-wgpu/src/shaders/basic/basic.frag b/inox2d-wgpu/src/shaders/basic/basic.frag index 4925e37..da35daa 100644 --- a/inox2d-wgpu/src/shaders/basic/basic.frag +++ b/inox2d-wgpu/src/shaders/basic/basic.frag @@ -11,11 +11,11 @@ layout(location = 0) out vec4 outAlbedo; layout(location = 1) out vec4 outEmissive; layout(location = 2) out vec4 outBump; -layout(binding = 0) uniform sampler2D albedo; -layout(binding = 1) uniform sampler2D emissive; -layout(binding = 2) uniform sampler2D bumpmap; +layout(set = 1, binding = 0) uniform sampler2D albedo; +layout(set = 1, binding = 1) uniform sampler2D emissive; +layout(set = 1, binding = 2) uniform sampler2D bumpmap; -layout(binding = 3) uniform Input { +layout(set = 1, binding = 3) uniform Input { uniform float opacity; uniform vec3 multColor; uniform vec3 screenColor; diff --git a/inox2d-wgpu/src/shaders/basic/basic.vert b/inox2d-wgpu/src/shaders/basic/basic.vert index 96e85c8..a01f412 100644 --- a/inox2d-wgpu/src/shaders/basic/basic.vert +++ b/inox2d-wgpu/src/shaders/basic/basic.vert @@ -6,7 +6,7 @@ */ #version 440 -layout(binding = 0) uniform Input { +layout(set = 0, binding = 0) uniform Input { mat4 mvp; vec2 offset; } uni_in; diff --git a/inox2d-wgpu/src/shaders/basic/composite-mask.frag b/inox2d-wgpu/src/shaders/basic/composite-mask.frag index 8b4c760..3e2a32c 100644 --- a/inox2d-wgpu/src/shaders/basic/composite-mask.frag +++ b/inox2d-wgpu/src/shaders/basic/composite-mask.frag @@ -9,8 +9,8 @@ layout(location = 0) in vec2 texUVs; layout(location = 0) out vec4 outColor; -layout(binding = 0) uniform sampler2D tex; -layout(binding = 1) uniform Input { +layout(set = 1, binding = 0) uniform sampler2D tex; +layout(set = 1, binding = 1) uniform Input { float threshold; float opacity; } uni_in; diff --git a/inox2d-wgpu/src/shaders/basic/composite.frag b/inox2d-wgpu/src/shaders/basic/composite.frag index cdcc427..7919518 100644 --- a/inox2d-wgpu/src/shaders/basic/composite.frag +++ b/inox2d-wgpu/src/shaders/basic/composite.frag @@ -11,11 +11,11 @@ layout(location = 0) out vec4 outAlbedo; layout(location = 1) out vec4 outEmissive; layout(location = 2) out vec4 outBump; -layout(binding = 0) uniform sampler2D albedo; -layout(binding = 1) uniform sampler2D emissive; -layout(binding = 2) uniform sampler2D bumpmap; +layout(set = 1, binding = 0) uniform sampler2D albedo; +layout(set = 1, binding = 1) uniform sampler2D emissive; +layout(set = 1, binding = 2) uniform sampler2D bumpmap; -layout(binding = 3) uniform Input { +layout(set = 1, binding = 3) uniform Input { float opacity; vec3 multColor; vec3 screenColor; diff --git a/inox2d-wgpu/src/shaders/basic/composite.vert b/inox2d-wgpu/src/shaders/basic/composite.vert index b466608..4903a59 100644 --- a/inox2d-wgpu/src/shaders/basic/composite.vert +++ b/inox2d-wgpu/src/shaders/basic/composite.vert @@ -6,7 +6,7 @@ */ #version 440 -layout(binding = 0) uniform Input { +layout(set = 0, binding = 0) uniform Input { mat4 mvp; } uni_in; diff --git a/inox2d-wgpu/src/shaders/dbg.vert b/inox2d-wgpu/src/shaders/dbg.vert index 2dda508..3ea4107 100644 --- a/inox2d-wgpu/src/shaders/dbg.vert +++ b/inox2d-wgpu/src/shaders/dbg.vert @@ -6,7 +6,7 @@ */ #version 440 -layout(binding = 0) uniform Input { +layout(set = 0, binding = 0) uniform Input { mat4 mvp; } uni_in; diff --git a/inox2d-wgpu/src/shaders/dbgline.frag b/inox2d-wgpu/src/shaders/dbgline.frag index 71f1fa3..a6fc828 100644 --- a/inox2d-wgpu/src/shaders/dbgline.frag +++ b/inox2d-wgpu/src/shaders/dbgline.frag @@ -7,7 +7,7 @@ #version 440 layout(location = 0) out vec4 outColor; -layout(binding=0) uniform Input { +layout(set = 1, binding=0) uniform Input { vec4 color; } uni_in; diff --git a/inox2d-wgpu/src/shaders/dbgpoint.frag b/inox2d-wgpu/src/shaders/dbgpoint.frag index 8096a05..7a2644a 100644 --- a/inox2d-wgpu/src/shaders/dbgpoint.frag +++ b/inox2d-wgpu/src/shaders/dbgpoint.frag @@ -7,7 +7,7 @@ #version 440 layout(location = 0) out vec4 outColor; -layout(binding = 0) uniform Input { +layout(set = 1, binding = 0) uniform Input { vec4 color; } uni_in; diff --git a/inox2d-wgpu/src/shaders/lighting.frag b/inox2d-wgpu/src/shaders/lighting.frag index 1c6ec90..f3a63e3 100644 --- a/inox2d-wgpu/src/shaders/lighting.frag +++ b/inox2d-wgpu/src/shaders/lighting.frag @@ -11,7 +11,7 @@ layout(location = 0) out vec4 outAlbedo; layout(location = 1) out vec4 outEmissive; layout(location = 2) out vec4 outBump; -layout(binding = 0) uniform Input { +layout(set = 1, binding = 0) uniform Input { uniform vec3 ambientLight; uniform vec2 fbSize; @@ -19,9 +19,9 @@ layout(binding = 0) uniform Input { uniform int samples; // OLD DEFAULT: 25 } uni_in; -layout(binding = 1) uniform sampler2D albedo; -layout(binding = 2) uniform sampler2D emissive; -layout(binding = 3) uniform sampler2D bumpmap; +layout(set = 1, binding = 1) uniform sampler2D albedo; +layout(set = 1, binding = 2) uniform sampler2D emissive; +layout(set = 1, binding = 3) uniform sampler2D bumpmap; // Gaussian float gaussian(vec2 i, float sigma) { diff --git a/inox2d-wgpu/src/shaders/mask.vert b/inox2d-wgpu/src/shaders/mask.vert index 0d36bc2..e0efb22 100644 --- a/inox2d-wgpu/src/shaders/mask.vert +++ b/inox2d-wgpu/src/shaders/mask.vert @@ -6,7 +6,7 @@ */ #version 440 -layout(binding = 0) uniform Input { +layout(set = 0, binding = 0) uniform Input { mat4 mvp; vec2 offset; } uni_in; diff --git a/inox2d-wgpu/src/shaders/scene.frag b/inox2d-wgpu/src/shaders/scene.frag index b44575f..3737326 100644 --- a/inox2d-wgpu/src/shaders/scene.frag +++ b/inox2d-wgpu/src/shaders/scene.frag @@ -9,7 +9,7 @@ layout(location = 0) in vec2 texUVs; layout(location = 0) out vec4 outColor; -layout(binding = 0) uniform sampler2D fbo; +layout(set = 1, binding = 0) uniform sampler2D fbo; void main() { // Set color to the corrosponding pixel in the FBO diff --git a/inox2d-wgpu/src/shaders/scene.vert b/inox2d-wgpu/src/shaders/scene.vert index 99226ff..e2e5129 100644 --- a/inox2d-wgpu/src/shaders/scene.vert +++ b/inox2d-wgpu/src/shaders/scene.vert @@ -6,7 +6,7 @@ */ #version 440 -layout(binding = 0) uniform Input { +layout(set = 0, binding = 0) uniform Input { mat4 mvp; } uni_in; From b8e71511da971252cf92abafb6a6d227f30a2e28 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Mon, 16 Feb 2026 04:41:32 +0000 Subject: [PATCH 16/49] Add a pipeline abstraction and make one out of our Part shaders. This one isn't code-genned, but if I wind up needing variadic parameters or whatever it might wind up being so in the future. --- inox2d-wgpu/src/lib.rs | 27 +++++++++++++-- inox2d-wgpu/src/pipeline.rs | 66 +++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 inox2d-wgpu/src/pipeline.rs diff --git a/inox2d-wgpu/src/lib.rs b/inox2d-wgpu/src/lib.rs index 60ebf31..5b7f6cf 100644 --- a/inox2d-wgpu/src/lib.rs +++ b/inox2d-wgpu/src/lib.rs @@ -1,9 +1,12 @@ -use wgpu; use inox2d::model::Model; +use wgpu; +mod pipeline; mod shader; mod shaders; +use shaders::basic::{basic_frag, basic_mask_frag, basic_vert}; + #[derive(Debug, thiserror::Error)] #[error("Could not initialize wgpu renderer: {0}")] pub enum WgpuRendererError { @@ -14,6 +17,9 @@ pub enum WgpuRendererError { pub struct WgpuRenderer<'window> { surface: wgpu::Surface<'window>, + + part_pipeline: pipeline::Pipeline, + part_mask_pipeline: pipeline::Pipeline, } impl<'window> WgpuRenderer<'window> { @@ -30,6 +36,23 @@ impl<'window> WgpuRenderer<'window> { }) .await?; let (device, queue) = adapter.request_device(&wgpu::DeviceDescriptor::default()).await?; - Ok(WgpuRenderer { surface }) + + // Compile all our shaders now. + let part_shader_vert = basic_vert::Shader::new(&device); + let part_shader_frag = basic_frag::Shader::new(&device); + let part_shader_mask_frag = basic_mask_frag::Shader::new(&device); + + let part_pipeline = pipeline::Pipeline::new(&device, &part_shader_vert, &part_shader_frag); + let part_mask_pipeline = pipeline::Pipeline::new(&device, &part_shader_vert, &part_shader_mask_frag); + + //TODO: Compositeshader, CompositeMaskShader + + //TODO: Upload model textures, verts, uvs, deforms, indicies + + Ok(WgpuRenderer { + surface, + part_pipeline, + part_mask_pipeline, + }) } } diff --git a/inox2d-wgpu/src/pipeline.rs b/inox2d-wgpu/src/pipeline.rs new file mode 100644 index 0000000..72d17ce --- /dev/null +++ b/inox2d-wgpu/src/pipeline.rs @@ -0,0 +1,66 @@ +use wgpu; + +use crate::shader::{FragmentShader, VertexShader}; +use std::marker::PhantomData; + +pub struct Pipeline +where + V: VertexShader, + F: FragmentShader, +{ + pipeline: wgpu::RenderPipeline, + phantom_vert: PhantomData, + phantom_frag: PhantomData, +} + +impl Pipeline +where + V: VertexShader, + F: FragmentShader, +{ + pub fn new(device: &wgpu::Device, vert: &V, frag: &F) -> Self { + let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("Pipeline"), + + // NOTE: This assumes vertex shaders always use set 0 and fragment shaders always use set 1. + bind_group_layouts: &[vert.bindgroup_layout(), frag.bindgroup_layout()], + immediate_size: 0, + }); + + Self { + pipeline: device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Pipeline"), + layout: Some(&layout), + vertex: vert.as_vertex_state(), + fragment: Some(frag.as_fragment_state()), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: Some(wgpu::Face::Back), //TODO: I'm pretty sure the GL renderer doesn't do this + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview_mask: None, + cache: None, + }), + phantom_frag: PhantomData::default(), + phantom_vert: PhantomData::default(), + } + } + + pub fn bind_vertex<'a, BG>(&self, render_pass: &mut wgpu::RenderPass, bind_group: BG) + where + Option<&'a wgpu::BindGroup>: From, + { + render_pass.set_bind_group(0, bind_group, &[]) + } + + pub fn bind_frag<'a, BG>(&self, render_pass: &mut wgpu::RenderPass, bind_group: BG) + where + Option<&'a wgpu::BindGroup>: From, + { + render_pass.set_bind_group(1, bind_group, &[]) + } +} From dd24f33a7d2bf89a654cc7798e3865b77bff74c8 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Mon, 16 Feb 2026 16:00:51 +0000 Subject: [PATCH 17/49] Also make the composite pipelines, too. --- inox2d-wgpu/src/lib.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/inox2d-wgpu/src/lib.rs b/inox2d-wgpu/src/lib.rs index 5b7f6cf..f848ecb 100644 --- a/inox2d-wgpu/src/lib.rs +++ b/inox2d-wgpu/src/lib.rs @@ -5,7 +5,7 @@ mod pipeline; mod shader; mod shaders; -use shaders::basic::{basic_frag, basic_mask_frag, basic_vert}; +use shaders::basic::{basic_frag, basic_mask_frag, basic_vert, composite_frag, composite_mask_frag, composite_vert}; #[derive(Debug, thiserror::Error)] #[error("Could not initialize wgpu renderer: {0}")] @@ -20,6 +20,9 @@ pub struct WgpuRenderer<'window> { part_pipeline: pipeline::Pipeline, part_mask_pipeline: pipeline::Pipeline, + + composite_pipeline: pipeline::Pipeline, + composite_mask_pipeline: pipeline::Pipeline, } impl<'window> WgpuRenderer<'window> { @@ -45,7 +48,13 @@ impl<'window> WgpuRenderer<'window> { let part_pipeline = pipeline::Pipeline::new(&device, &part_shader_vert, &part_shader_frag); let part_mask_pipeline = pipeline::Pipeline::new(&device, &part_shader_vert, &part_shader_mask_frag); - //TODO: Compositeshader, CompositeMaskShader + let composite_shader_vert = composite_vert::Shader::new(&device); + let composite_shader_frag = composite_frag::Shader::new(&device); + let composite_shader_mask_frag = composite_mask_frag::Shader::new(&device); + + let composite_pipeline = pipeline::Pipeline::new(&device, &composite_shader_vert, &composite_shader_frag); + let composite_mask_pipeline = + pipeline::Pipeline::new(&device, &composite_shader_vert, &composite_shader_mask_frag); //TODO: Upload model textures, verts, uvs, deforms, indicies @@ -53,6 +62,8 @@ impl<'window> WgpuRenderer<'window> { surface, part_pipeline, part_mask_pipeline, + composite_pipeline, + composite_mask_pipeline, }) } } From 49a7c8ff5f9a725b4d053689900fb931157cb651 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Mon, 16 Feb 2026 17:36:58 +0000 Subject: [PATCH 18/49] Add a trait for uniform block buffer generation --- inox2d-wgpu/build.rs | 14 +++++++++----- inox2d-wgpu/src/shader.rs | 16 ++++++++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/inox2d-wgpu/build.rs b/inox2d-wgpu/build.rs index 9f213b6..d04852e 100644 --- a/inox2d-wgpu/build.rs +++ b/inox2d-wgpu/build.rs @@ -88,7 +88,7 @@ fn describe_block_struct( typevar: &ReflectTypeDescription, ) -> Result<(), Box> { writeln!(out, "#[allow(non_snake_case)]")?; //I'm too lazy to write a to_snake_case fn - writeln!(out, "struct {} {{", typevar.type_name)?; + writeln!(out, "pub struct {} {{", typevar.type_name)?; for (blockmember, typemember) in blockvar.members.iter().zip(typevar.members.iter()) { writeln!(out, " /// name: {}", blockmember.name)?; @@ -100,14 +100,18 @@ fn describe_block_struct( writeln!(out, " /// Traits: {:?}", typemember.traits)?; let rust_type = spirv_to_rust_type(typemember)?; - writeln!(out, " {}: {},", blockmember.name, rust_type)?; + writeln!(out, " pub {}: {},", blockmember.name, rust_type)?; } writeln!(out, "}}")?; writeln!(out)?; - writeln!(out, "impl {} {{", typevar.type_name)?; - writeln!(out, " fn write_buffer(self, out: &mut [u8; {}]) {{", blockvar.size)?; + writeln!( + out, + "impl shader::UniformBlock<{}> for {} {{", + blockvar.size, typevar.type_name + )?; + writeln!(out, " fn write_buffer(&self, out: &mut [u8; {}]) {{", blockvar.size)?; for (blockmember, typemember) in blockvar.members.iter().zip(typevar.members.iter()) { if typemember.type_flags.contains(ReflectTypeFlags::MATRIX) { @@ -279,7 +283,7 @@ fn gen_shader_bind(out: &mut String, filename: &str, entrypoint: &ReflectEntryPo writeln!( out, - " fn bind(self, device: &wgpu::Device{}) -> wgpu::BindGroup {{", + " pub fn bind(&self, device: &wgpu::Device{}) -> wgpu::BindGroup {{", bind_params )?; writeln!(out, " device.create_bind_group(&wgpu::BindGroupDescriptor {{")?; diff --git a/inox2d-wgpu/src/shader.rs b/inox2d-wgpu/src/shader.rs index d6cbb79..26b56a0 100644 --- a/inox2d-wgpu/src/shader.rs +++ b/inox2d-wgpu/src/shader.rs @@ -1,4 +1,5 @@ use wgpu; +use wgpu::util::DeviceExt; pub trait Shader { fn bindgroup_layout(&self) -> &wgpu::BindGroupLayout; @@ -11,3 +12,18 @@ pub trait VertexShader: Shader { pub trait FragmentShader: Shader { fn as_fragment_state(&self) -> wgpu::FragmentState; } + +pub trait UniformBlock { + fn write_buffer(&self, out: &mut [u8; Size]); + + fn into_buffer(&self, device: &wgpu::Device) -> wgpu::Buffer { + let mut contents = [0; Size]; + self.write_buffer(&mut contents); + + device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("UniformBlock::into_buffer"), //TODO: Can we get a type name in here? + contents: &contents, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }) + } +} From 2d9ce9d1a14bc61340f29f21412c393955027ef4 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Mon, 16 Feb 2026 17:38:05 +0000 Subject: [PATCH 19/49] Stub impl of InoxRenderer for WGPU. --- inox2d-wgpu/Cargo.toml | 1 + inox2d-wgpu/src/lib.rs | 116 ++++++++++++++++++++++++++++++++++++ inox2d-wgpu/src/pipeline.rs | 4 ++ 3 files changed, 121 insertions(+) diff --git a/inox2d-wgpu/Cargo.toml b/inox2d-wgpu/Cargo.toml index e8ec0e4..3995fa8 100644 --- a/inox2d-wgpu/Cargo.toml +++ b/inox2d-wgpu/Cargo.toml @@ -11,6 +11,7 @@ categories = ["graphics", "rendering"] [dependencies] inox2d = { path = "../inox2d", version = "0.3.0" } +glam = "0.29.0" wgpu = { version = "28.0.0", features=["spirv"] } thiserror = "1.0.39" diff --git a/inox2d-wgpu/src/lib.rs b/inox2d-wgpu/src/lib.rs index f848ecb..7bcc63b 100644 --- a/inox2d-wgpu/src/lib.rs +++ b/inox2d-wgpu/src/lib.rs @@ -1,10 +1,14 @@ +use glam::Mat4; use inox2d::model::Model; +use inox2d::node::{InoxNodeUuid, components, drawables}; //hey wait a second that's just a u32 newtype! UUIDs are four of those! +use inox2d::render::{self, InoxRenderer}; use wgpu; mod pipeline; mod shader; mod shaders; +use shader::UniformBlock; use shaders::basic::{basic_frag, basic_mask_frag, basic_vert, composite_frag, composite_mask_frag, composite_vert}; #[derive(Debug, thiserror::Error)] @@ -18,11 +22,24 @@ pub enum WgpuRendererError { pub struct WgpuRenderer<'window> { surface: wgpu::Surface<'window>, + part_shader_vert: basic_vert::Shader, + part_shader_frag: basic_frag::Shader, + part_shader_mask_frag: basic_mask_frag::Shader, + part_pipeline: pipeline::Pipeline, part_mask_pipeline: pipeline::Pipeline, + composite_shader_vert: composite_vert::Shader, + composite_shader_frag: composite_frag::Shader, + composite_shader_mask_frag: composite_mask_frag::Shader, + composite_pipeline: pipeline::Pipeline, composite_mask_pipeline: pipeline::Pipeline, + + encoder: Option, + + device: wgpu::Device, + queue: wgpu::Queue, } impl<'window> WgpuRenderer<'window> { @@ -60,10 +77,109 @@ impl<'window> WgpuRenderer<'window> { Ok(WgpuRenderer { surface, + part_shader_vert, + part_shader_frag, + part_shader_mask_frag, part_pipeline, part_mask_pipeline, + composite_shader_vert, + composite_shader_frag, + composite_shader_mask_frag, composite_pipeline, composite_mask_pipeline, + encoder: None, + device, + queue, }) } } + +impl<'window> InoxRenderer for WgpuRenderer<'window> { + fn begin_render(&mut self) { + if self.encoder.is_some() { + panic!("Recursive rendering is not permitted."); + } + + self.encoder = Some(self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("Inox2DWGPU"), + })); + } + + fn on_begin_masks(&self, masks: &components::Masks) { + unimplemented!() + } + + fn on_begin_mask(&self, mask: &components::Mask) { + unimplemented!() + } + + fn on_begin_masked_content(&self) { + unimplemented!() + } + + fn on_end_mask(&self) { + unimplemented!() + } + + fn draw_textured_mesh_content( + &mut self, + as_mask: bool, + components: &drawables::TexturedMeshComponents, + render_ctx: &render::TexturedMeshRenderCtx, + id: InoxNodeUuid, + ) { + let encoder = self.encoder.as_mut().expect("encoder"); + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("WgpuRenderer::draw_textured_mesh_content"), + color_attachments: &[], //TODO: render target + depth_stencil_attachment: None, //TODO: MASKS + occlusion_query_set: None, + timestamp_writes: None, + multiview_mask: None, + }); + + if as_mask { + render_pass.set_pipeline(self.part_mask_pipeline.pipeline()); + let uni_in = basic_vert::Input { + // TODO: there is no provision for the renderer to learn the + // current camera/viewport matrix OpenGLRenderer just has a + // pub parameter for it which is dumb. + mvp: Mat4::IDENTITY.to_cols_array_2d(), + offset: [0.0; 2], + } + .into_buffer(&self.device); + + // TODO: We don't have good enough resource management to maintain + // one uniform buffer per object, so we have to create and dispose + // of them per frame. + + self.part_mask_pipeline.bind_vertex( + &mut render_pass, + Some(&self.part_shader_vert.bind(&self.device, &uni_in)), + ); + } + } + + fn begin_composite_content( + &self, + as_mask: bool, + components: &drawables::CompositeComponents, + render_ctx: &render::CompositeRenderCtx, + id: InoxNodeUuid, + ) { + } + + fn finish_composite_content( + &self, + as_mask: bool, + components: &drawables::CompositeComponents, + render_ctx: &render::CompositeRenderCtx, + id: InoxNodeUuid, + ) { + } + + fn end_render_and_flush(&mut self) { + let end = self.encoder.take().expect("encoder").finish(); + self.queue.submit(std::iter::once(end)); + } +} diff --git a/inox2d-wgpu/src/pipeline.rs b/inox2d-wgpu/src/pipeline.rs index 72d17ce..2cdae92 100644 --- a/inox2d-wgpu/src/pipeline.rs +++ b/inox2d-wgpu/src/pipeline.rs @@ -63,4 +63,8 @@ where { render_pass.set_bind_group(1, bind_group, &[]) } + + pub fn pipeline(&self) -> &wgpu::RenderPipeline { + &self.pipeline + } } From 52d5da8072f4acb7803637ef3bd5945553ee1540 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Thu, 19 Feb 2026 03:19:47 +0000 Subject: [PATCH 20/49] It turns out combined image/samplers are forbidden in WGPU land, so remove them all --- inox2d-wgpu/build.rs | 35 ++++++++++++++++--- inox2d-wgpu/src/shaders/basic/basic-mask.frag | 7 ++-- inox2d-wgpu/src/shaders/basic/basic.frag | 15 ++++---- .../src/shaders/basic/composite-mask.frag | 7 ++-- inox2d-wgpu/src/shaders/basic/composite.frag | 15 ++++---- inox2d-wgpu/src/shaders/lighting.frag | 17 ++++----- inox2d-wgpu/src/shaders/scene.frag | 5 +-- 7 files changed, 67 insertions(+), 34 deletions(-) diff --git a/inox2d-wgpu/build.rs b/inox2d-wgpu/build.rs index d04852e..7e36aef 100644 --- a/inox2d-wgpu/build.rs +++ b/inox2d-wgpu/build.rs @@ -211,17 +211,36 @@ fn gen_shader_new( writeln!(out, " }},")?; } - ReflectDescriptorType::Sampler | ReflectDescriptorType::CombinedImageSampler => { + //NOTE: Combined image samplers are NOT supported by WGPU! + ReflectDescriptorType::CombinedImageSampler => { + writeln!( + out, + " ty: // Combined image samplers are NOT supported by WGPU. Please remove them from your shader.", + )?; + } + ReflectDescriptorType::Sampler => { //TODO: How do we ask what filtering type to use? writeln!( out, " ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering)," )?; } + ReflectDescriptorType::SampledImage => { + writeln!(out, " ty: wgpu::BindingType::Texture {{")?; + writeln!(out, " multisampled: false,")?; + writeln!( + out, + " view_dimension: wgpu::TextureViewDimension::D2," + )?; //TODO: Support 1D/3D textures + writeln!( + out, + " sample_type: wgpu::TextureSampleType::Float {{ filterable: true }}," + )?; + writeln!(out, " }},")?; + } //TODO: generate bindings for all of these ReflectDescriptorType::Undefined - | ReflectDescriptorType::SampledImage | ReflectDescriptorType::StorageImage | ReflectDescriptorType::UniformTexelBuffer | ReflectDescriptorType::StorageTexelBuffer @@ -263,10 +282,12 @@ fn gen_shader_bind(out: &mut String, filename: &str, entrypoint: &ReflectEntryPo ReflectDescriptorType::Sampler | ReflectDescriptorType::CombinedImageSampler => { write!(&mut bind_params, ", {}: &wgpu::Sampler", binding.name)?; } + ReflectDescriptorType::SampledImage => { + write!(&mut bind_params, ", {}: &wgpu::TextureView", binding.name)?; + } //TODO: generate bindings for all of these ReflectDescriptorType::Undefined - | ReflectDescriptorType::SampledImage | ReflectDescriptorType::StorageImage | ReflectDescriptorType::UniformTexelBuffer | ReflectDescriptorType::StorageTexelBuffer @@ -316,10 +337,16 @@ fn gen_shader_bind(out: &mut String, filename: &str, entrypoint: &ReflectEntryPo binding.name )?; } + ReflectDescriptorType::SampledImage => { + writeln!( + out, + " resource: wgpu::BindingResource::TextureView({})", + binding.name + )?; + } //TODO: generate bindings for all of these ReflectDescriptorType::Undefined - | ReflectDescriptorType::SampledImage | ReflectDescriptorType::StorageImage | ReflectDescriptorType::UniformTexelBuffer | ReflectDescriptorType::StorageTexelBuffer diff --git a/inox2d-wgpu/src/shaders/basic/basic-mask.frag b/inox2d-wgpu/src/shaders/basic/basic-mask.frag index ce5de23..e4265f4 100644 --- a/inox2d-wgpu/src/shaders/basic/basic-mask.frag +++ b/inox2d-wgpu/src/shaders/basic/basic-mask.frag @@ -9,13 +9,14 @@ layout(location = 0) in vec2 texUVs; layout(location = 0) out vec4 outColor; -layout(set = 1, binding = 0) uniform sampler2D tex; -layout(set = 1, binding = 1) uniform Input { +layout(set = 1, binding = 0) uniform texture2D tex; +layout(set = 1, binding = 1) uniform sampler samp; +layout(set = 1, binding = 2) uniform Input { float threshold; } uni_in; void main() { - vec4 color = texture(tex, texUVs); + vec4 color = texture(sampler2D(tex, samp), texUVs); if (color.a <= uni_in.threshold) discard; outColor = vec4(1, 1, 1, 1); diff --git a/inox2d-wgpu/src/shaders/basic/basic.frag b/inox2d-wgpu/src/shaders/basic/basic.frag index da35daa..72bf39b 100644 --- a/inox2d-wgpu/src/shaders/basic/basic.frag +++ b/inox2d-wgpu/src/shaders/basic/basic.frag @@ -11,11 +11,12 @@ layout(location = 0) out vec4 outAlbedo; layout(location = 1) out vec4 outEmissive; layout(location = 2) out vec4 outBump; -layout(set = 1, binding = 0) uniform sampler2D albedo; -layout(set = 1, binding = 1) uniform sampler2D emissive; -layout(set = 1, binding = 2) uniform sampler2D bumpmap; +layout(set = 1, binding = 0) uniform texture2D albedo_tex; +layout(set = 1, binding = 1) uniform texture2D emissive_tex; +layout(set = 1, binding = 2) uniform texture2D bumpmap_tex; +layout(set = 1, binding = 3) uniform sampler samp; -layout(set = 1, binding = 3) uniform Input { +layout(set = 1, binding = 4) uniform Input { uniform float opacity; uniform vec3 multColor; uniform vec3 screenColor; @@ -24,7 +25,7 @@ layout(set = 1, binding = 3) uniform Input { void main() { // Sample texture - vec4 texColor = texture(albedo, texUVs); + vec4 texColor = texture(sampler2D(albedo_tex, samp), texUVs); // Screen color math vec3 screenOut = vec3(1.0) - ((vec3(1.0) - (texColor.xyz)) * @@ -36,8 +37,8 @@ void main() { // Emissive outEmissive = - vec4(texture(emissive, texUVs).xyz * uni_in.emissionStrength, 1) * outAlbedo.a; + vec4(texture(sampler2D(emissive_tex, samp), texUVs).xyz * uni_in.emissionStrength, 1) * outAlbedo.a; // Bumpmap - outBump = vec4(texture(bumpmap, texUVs).xyz, 1) * outAlbedo.a; + outBump = vec4(texture(sampler2D(bumpmap_tex, samp), texUVs).xyz, 1) * outAlbedo.a; } \ No newline at end of file diff --git a/inox2d-wgpu/src/shaders/basic/composite-mask.frag b/inox2d-wgpu/src/shaders/basic/composite-mask.frag index 3e2a32c..f95591b 100644 --- a/inox2d-wgpu/src/shaders/basic/composite-mask.frag +++ b/inox2d-wgpu/src/shaders/basic/composite-mask.frag @@ -9,14 +9,15 @@ layout(location = 0) in vec2 texUVs; layout(location = 0) out vec4 outColor; -layout(set = 1, binding = 0) uniform sampler2D tex; -layout(set = 1, binding = 1) uniform Input { +layout(set = 1, binding = 0) uniform texture2D tex; +layout(set = 1, binding = 1) uniform sampler samp; +layout(set = 1, binding = 2) uniform Input { float threshold; float opacity; } uni_in; void main() { - vec4 color = texture(tex, texUVs) * vec4(1, 1, 1, uni_in.opacity); + vec4 color = texture(sampler2D(tex, samp), texUVs) * vec4(1, 1, 1, uni_in.opacity); if (color.a <= uni_in.threshold) discard; outColor = vec4(1, 1, 1, 1); diff --git a/inox2d-wgpu/src/shaders/basic/composite.frag b/inox2d-wgpu/src/shaders/basic/composite.frag index 7919518..0250ebd 100644 --- a/inox2d-wgpu/src/shaders/basic/composite.frag +++ b/inox2d-wgpu/src/shaders/basic/composite.frag @@ -11,11 +11,12 @@ layout(location = 0) out vec4 outAlbedo; layout(location = 1) out vec4 outEmissive; layout(location = 2) out vec4 outBump; -layout(set = 1, binding = 0) uniform sampler2D albedo; -layout(set = 1, binding = 1) uniform sampler2D emissive; -layout(set = 1, binding = 2) uniform sampler2D bumpmap; +layout(set = 1, binding = 0) uniform texture2D albedo_tex; +layout(set = 1, binding = 1) uniform texture2D emissive_tex; +layout(set = 1, binding = 2) uniform texture2D bumpmap_tex; +layout(set = 1, binding = 3) uniform sampler samp; -layout(set = 1, binding = 3) uniform Input { +layout(set = 1, binding = 4) uniform Input { float opacity; vec3 multColor; vec3 screenColor; @@ -23,7 +24,7 @@ layout(set = 1, binding = 3) uniform Input { void main() { // Sample texture - vec4 texColor = texture(albedo, texUVs); + vec4 texColor = texture(sampler2D(albedo_tex, samp), texUVs); // Screen color math vec3 screenOut = vec3(1.0) - ((vec3(1.0) - (texColor.xyz)) * @@ -34,8 +35,8 @@ void main() { vec4(screenOut.xyz, texColor.a) * vec4(uni_in.multColor.xyz, 1) * uni_in.opacity; // Emissive - outEmissive = texture(emissive, texUVs) * outAlbedo.a; + outEmissive = texture(sampler2D(emissive_tex, samp), texUVs) * outAlbedo.a; // Bumpmap - outBump = texture(bumpmap, texUVs) * outAlbedo.a; + outBump = texture(sampler2D(bumpmap_tex, samp), texUVs) * outAlbedo.a; } \ No newline at end of file diff --git a/inox2d-wgpu/src/shaders/lighting.frag b/inox2d-wgpu/src/shaders/lighting.frag index f3a63e3..1d1ba3b 100644 --- a/inox2d-wgpu/src/shaders/lighting.frag +++ b/inox2d-wgpu/src/shaders/lighting.frag @@ -19,9 +19,10 @@ layout(set = 1, binding = 0) uniform Input { uniform int samples; // OLD DEFAULT: 25 } uni_in; -layout(set = 1, binding = 1) uniform sampler2D albedo; -layout(set = 1, binding = 2) uniform sampler2D emissive; -layout(set = 1, binding = 3) uniform sampler2D bumpmap; +layout(set = 1, binding = 1) uniform texture2D albedo_tex; +layout(set = 1, binding = 2) uniform texture2D emissive_tex; +layout(set = 1, binding = 3) uniform texture2D bumpmap_tex; +layout(set = 1, binding = 4) uniform sampler samp; // Gaussian float gaussian(vec2 i, float sigma) { @@ -29,7 +30,7 @@ float gaussian(vec2 i, float sigma) { } // Bloom texture by blurring it -vec4 bloom(sampler2D sp, vec2 uv, vec2 scale) { +vec4 bloom(texture2D tx, sampler smp, vec2 uv, vec2 scale) { float sigma = float(uni_in.samples) * 0.25; vec4 out_ = vec4(0); int sLOD = 1 << uni_in.LOD; @@ -37,7 +38,7 @@ vec4 bloom(sampler2D sp, vec2 uv, vec2 scale) { for (int i = 0; i < s * s; i++) { vec2 d = vec2(i % s, i / s) * float(sLOD) - float(uni_in.samples) / 2.0; - out_ += gaussian(d, sigma) * textureLod(sp, uv + scale * d, uni_in.LOD); + out_ += gaussian(d, sigma) * textureLod(sampler2D(tx, smp), uv + scale * d, uni_in.LOD); } return out_ / out_.a; @@ -46,11 +47,11 @@ vec4 bloom(sampler2D sp, vec2 uv, vec2 scale) { void main() { // Bloom - outEmissive = bloom(emissive, texUVs, 1.0 / uni_in.fbSize); + outEmissive = bloom(emissive_tex, samp, texUVs, 1.0 / uni_in.fbSize); // Set color to the corrosponding pixel in the FBO vec4 light = vec4(uni_in.ambientLight, 1) + outEmissive; - outAlbedo = (texture(albedo, texUVs) * light); - outBump = texture(bumpmap, texUVs); + outAlbedo = (texture(sampler2D(albedo_tex, samp), texUVs) * light); + outBump = texture(sampler2D(bumpmap_tex, samp), texUVs); } \ No newline at end of file diff --git a/inox2d-wgpu/src/shaders/scene.frag b/inox2d-wgpu/src/shaders/scene.frag index 3737326..41f77a3 100644 --- a/inox2d-wgpu/src/shaders/scene.frag +++ b/inox2d-wgpu/src/shaders/scene.frag @@ -9,11 +9,12 @@ layout(location = 0) in vec2 texUVs; layout(location = 0) out vec4 outColor; -layout(set = 1, binding = 0) uniform sampler2D fbo; +layout(set = 1, binding = 0) uniform texture2D fbo; +layout(set = 1, binding = 1) uniform sampler samp; void main() { // Set color to the corrosponding pixel in the FBO - vec4 color = texture(fbo, texUVs); + vec4 color = texture(sampler2D(fbo, samp), texUVs); outColor = vec4(color.r * color.a, color.g * color.a, color.b * color.a, color.a); } \ No newline at end of file From 9861a2819d89e092a1fc6ada6ff39eb92aeb66ac Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Thu, 19 Feb 2026 03:28:04 +0000 Subject: [PATCH 21/49] Upload all model textures at renderer init --- inox2d-wgpu/src/lib.rs | 26 ++++++++++++++ inox2d-wgpu/src/texture.rs | 70 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 inox2d-wgpu/src/texture.rs diff --git a/inox2d-wgpu/src/lib.rs b/inox2d-wgpu/src/lib.rs index 7bcc63b..ba7e735 100644 --- a/inox2d-wgpu/src/lib.rs +++ b/inox2d-wgpu/src/lib.rs @@ -2,12 +2,15 @@ use glam::Mat4; use inox2d::model::Model; use inox2d::node::{InoxNodeUuid, components, drawables}; //hey wait a second that's just a u32 newtype! UUIDs are four of those! use inox2d::render::{self, InoxRenderer}; +use inox2d::texture::decode_model_textures; use wgpu; mod pipeline; mod shader; mod shaders; +mod texture; +use crate::texture::DeviceTexture; use shader::UniformBlock; use shaders::basic::{basic_frag, basic_mask_frag, basic_vert, composite_frag, composite_mask_frag, composite_vert}; @@ -38,6 +41,9 @@ pub struct WgpuRenderer<'window> { encoder: Option, + model_textures: Vec, + model_sampler: wgpu::Sampler, + device: wgpu::Device, queue: wgpu::Queue, } @@ -74,6 +80,24 @@ impl<'window> WgpuRenderer<'window> { pipeline::Pipeline::new(&device, &composite_shader_vert, &composite_shader_mask_frag); //TODO: Upload model textures, verts, uvs, deforms, indicies + let decoded_textures = decode_model_textures(model.textures.iter()); + let mut texture_handles = vec![]; + for (index, texture) in decoded_textures.iter().enumerate() { + texture_handles.push(DeviceTexture::new_from_model(&device, &queue, model, index, texture)); + } + + let model_sampler = device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::ClampToBorder, + address_mode_v: wgpu::AddressMode::ClampToBorder, + address_mode_w: wgpu::AddressMode::ClampToBorder, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + ..Default::default() + }); + + // Flush all pending work. + // In wgpu, texture uploads etc will only execute at submit time + queue.submit([]); Ok(WgpuRenderer { surface, @@ -88,6 +112,8 @@ impl<'window> WgpuRenderer<'window> { composite_pipeline, composite_mask_pipeline, encoder: None, + model_textures: texture_handles, + model_sampler, device, queue, }) diff --git a/inox2d-wgpu/src/texture.rs b/inox2d-wgpu/src/texture.rs new file mode 100644 index 0000000..a27fd67 --- /dev/null +++ b/inox2d-wgpu/src/texture.rs @@ -0,0 +1,70 @@ +use wgpu; + +use inox2d::model::Model; +use inox2d::texture::ShallowTexture; + +pub struct DeviceTexture { + device_texture: wgpu::Texture, + view: wgpu::TextureView, +} + +impl DeviceTexture { + /// Submit a texture to be uploaded to the given WGPU device. + /// + /// Note that the upload will not complete until the next queue submission. + pub fn new_from_model( + device: &wgpu::Device, + queue: &wgpu::Queue, + model: &Model, + index: usize, + texture: &ShallowTexture, + ) -> Self { + let size = wgpu::Extent3d { + width: texture.width(), + height: texture.height(), + depth_or_array_layers: 1, + }; + let device_texture = device.create_texture(&wgpu::TextureDescriptor { + size, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8Uint, //TODO: SRGB? + usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, + label: Some(&format!( + "Puppet texture: {}::{}", + model.puppet.meta.name.as_deref().unwrap_or(""), + index + )), + view_formats: &[], + }); + + queue.write_texture( + wgpu::TexelCopyTextureInfo { + texture: &device_texture, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + texture.pixels(), + wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(4 * texture.width()), + rows_per_image: Some(texture.height()), + }, + size, + ); + + let view = device_texture.create_view(&wgpu::TextureViewDescriptor::default()); + + Self { device_texture, view } + } + + pub fn texture(&self) -> &wgpu::Texture { + &self.device_texture + } + + pub fn view(&self) -> &wgpu::TextureView { + &self.view + } +} From 7d628ef29de239a390ab03278dd95bf16e3aa9d3 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Thu, 19 Feb 2026 03:28:43 +0000 Subject: [PATCH 22/49] Use uploaded model texturers in the render passes we've already written --- inox2d-wgpu/src/lib.rs | 91 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 75 insertions(+), 16 deletions(-) diff --git a/inox2d-wgpu/src/lib.rs b/inox2d-wgpu/src/lib.rs index ba7e735..24d9208 100644 --- a/inox2d-wgpu/src/lib.rs +++ b/inox2d-wgpu/src/lib.rs @@ -1,5 +1,5 @@ use glam::Mat4; -use inox2d::model::Model; +use inox2d::model::{Model, ModelTexture}; use inox2d::node::{InoxNodeUuid, components, drawables}; //hey wait a second that's just a u32 newtype! UUIDs are four of those! use inox2d::render::{self, InoxRenderer}; use inox2d::texture::decode_model_textures; @@ -44,6 +44,8 @@ pub struct WgpuRenderer<'window> { model_textures: Vec, model_sampler: wgpu::Sampler, + last_mask_threshold: f32, + device: wgpu::Device, queue: wgpu::Queue, } @@ -114,10 +116,19 @@ impl<'window> WgpuRenderer<'window> { encoder: None, model_textures: texture_handles, model_sampler, + last_mask_threshold: 0.0, device, queue, }) } + + fn textures_for_part(&self, part: &components::TexturedMesh) -> (&DeviceTexture, &DeviceTexture, &DeviceTexture) { + ( + &self.model_textures[part.tex_albedo.raw()], + &self.model_textures[part.tex_bumpmap.raw()], + &self.model_textures[part.tex_emissive.raw()], + ) + } } impl<'window> InoxRenderer for WgpuRenderer<'window> { @@ -131,8 +142,10 @@ impl<'window> InoxRenderer for WgpuRenderer<'window> { })); } - fn on_begin_masks(&self, masks: &components::Masks) { - unimplemented!() + fn on_begin_masks(&mut self, masks: &components::Masks) { + self.last_mask_threshold = masks.threshold.clamp(0.0, 1.0); + + //TODO: Enable stencilling on the render target. } fn on_begin_mask(&self, mask: &components::Mask) { @@ -154,7 +167,8 @@ impl<'window> InoxRenderer for WgpuRenderer<'window> { render_ctx: &render::TexturedMeshRenderCtx, id: InoxNodeUuid, ) { - let encoder = self.encoder.as_mut().expect("encoder"); + //NOTE: borrowck doesn't want us borrowing the encoder, so we .take() it instead. + let mut encoder = self.encoder.take().expect("encoder"); let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("WgpuRenderer::draw_textured_mesh_content"), color_attachments: &[], //TODO: render target @@ -164,26 +178,71 @@ impl<'window> InoxRenderer for WgpuRenderer<'window> { multiview_mask: None, }); + let (albedo, bumpmap, emissive) = self.textures_for_part(components.texture); + + //TODO: set blend mode + let uni_in_vert = basic_vert::Input { + // TODO: there is no provision for the renderer to learn the + // current camera/viewport matrix OpenGLRenderer just has a + // pub parameter for it which is dumb. + mvp: Mat4::IDENTITY.to_cols_array_2d(), + offset: [0.0; 2], + } + .into_buffer(&self.device); + if as_mask { - render_pass.set_pipeline(self.part_mask_pipeline.pipeline()); - let uni_in = basic_vert::Input { - // TODO: there is no provision for the renderer to learn the - // current camera/viewport matrix OpenGLRenderer just has a - // pub parameter for it which is dumb. - mvp: Mat4::IDENTITY.to_cols_array_2d(), - offset: [0.0; 2], + let uni_in_frag = basic_mask_frag::Input { + threshold: self.last_mask_threshold, } .into_buffer(&self.device); - // TODO: We don't have good enough resource management to maintain - // one uniform buffer per object, so we have to create and dispose - // of them per frame. - + self.part_mask_pipeline.bind_frag( + &mut render_pass, + Some( + &self + .part_shader_mask_frag + .bind(&self.device, albedo.view(), &self.model_sampler, &uni_in_frag), + ), + ); self.part_mask_pipeline.bind_vertex( &mut render_pass, - Some(&self.part_shader_vert.bind(&self.device, &uni_in)), + Some(&self.part_shader_vert.bind(&self.device, &uni_in_vert)), ); + + render_pass.set_pipeline(self.part_mask_pipeline.pipeline()); + } else { + //Regular parts + let uni_in_frag = basic_frag::Input { + opacity: components.drawable.blending.opacity, + multColor: components.drawable.blending.tint.into(), + screenColor: components.drawable.blending.screen_tint.into(), + emissionStrength: 1.0, //NOTE: OpenGL never sets this. + } + .into_buffer(&self.device); + + self.part_pipeline.bind_frag( + &mut render_pass, + Some(&self.part_shader_frag.bind( + &self.device, + albedo.view(), + bumpmap.view(), + emissive.view(), + &self.model_sampler, + &uni_in_frag, + )), + ); + self.part_pipeline.bind_vertex( + &mut render_pass, + Some(&self.part_shader_vert.bind(&self.device, &uni_in_vert)), + ); + + render_pass.set_pipeline(self.part_pipeline.pipeline()); } + + //TODO: Actual draw elements call + + drop(render_pass); //NOTE: borrowck also needs us to do this + self.encoder = Some(encoder); } fn begin_composite_content( From 608c5775ff60f01aa0232aa847b61de8a497cf98 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Thu, 19 Feb 2026 04:30:50 +0000 Subject: [PATCH 23/49] Add a type to manage our internal render targets --- inox2d-wgpu/src/lib.rs | 31 +++++- inox2d-wgpu/src/texture.rs | 187 +++++++++++++++++++++++++++++++++++++ 2 files changed, 217 insertions(+), 1 deletion(-) diff --git a/inox2d-wgpu/src/lib.rs b/inox2d-wgpu/src/lib.rs index 24d9208..662603d 100644 --- a/inox2d-wgpu/src/lib.rs +++ b/inox2d-wgpu/src/lib.rs @@ -10,7 +10,7 @@ mod shader; mod shaders; mod texture; -use crate::texture::DeviceTexture; +use crate::texture::{DeviceTexture, GBuffer}; use shader::UniformBlock; use shaders::basic::{basic_frag, basic_mask_frag, basic_vert, composite_frag, composite_mask_frag, composite_vert}; @@ -24,6 +24,8 @@ pub enum WgpuRendererError { pub struct WgpuRenderer<'window> { surface: wgpu::Surface<'window>, + config: wgpu::SurfaceConfiguration, + gbuffer: Option, part_shader_vert: basic_vert::Shader, part_shader_frag: basic_frag::Shader, @@ -65,6 +67,22 @@ impl<'window> WgpuRenderer<'window> { .await?; let (device, queue) = adapter.request_device(&wgpu::DeviceDescriptor::default()).await?; + // Find a suitable surface configuration. + let surface_caps = surface.get_capabilities(&adapter); + let surface_format = surface_caps.formats[0]; //TODO: SRGB? + let config = wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format: surface_format, + + //TODO: We don't know the size of our surface at init time. + width: 640, + height: 480, + present_mode: surface_caps.present_modes[0], + alpha_mode: surface_caps.alpha_modes[0], + view_formats: vec![], + desired_maximum_frame_latency: 2, + }; + // Compile all our shaders now. let part_shader_vert = basic_vert::Shader::new(&device); let part_shader_frag = basic_frag::Shader::new(&device); @@ -103,6 +121,8 @@ impl<'window> WgpuRenderer<'window> { Ok(WgpuRenderer { surface, + config, + gbuffer: None, part_shader_vert, part_shader_frag, part_shader_mask_frag, @@ -122,6 +142,15 @@ impl<'window> WgpuRenderer<'window> { }) } + pub fn resize(&mut self, width: u32, height: u32) { + if width > 0 && height > 0 { + self.config.width = width; + self.config.height = height; + self.surface.configure(&self.device, &self.config); + self.gbuffer = Some(GBuffer::new(&self.device, &self.queue, width, height)); + } + } + fn textures_for_part(&self, part: &components::TexturedMesh) -> (&DeviceTexture, &DeviceTexture, &DeviceTexture) { ( &self.model_textures[part.tex_albedo.raw()], diff --git a/inox2d-wgpu/src/texture.rs b/inox2d-wgpu/src/texture.rs index a27fd67..672c1db 100644 --- a/inox2d-wgpu/src/texture.rs +++ b/inox2d-wgpu/src/texture.rs @@ -60,6 +60,146 @@ impl DeviceTexture { Self { device_texture, view } } + pub fn empty_render_target( + device: &wgpu::Device, + queue: &wgpu::Queue, + width: u32, + height: u32, + format: wgpu::TextureFormat, + ) -> Self { + let size = wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }; + let device_texture = device.create_texture(&wgpu::TextureDescriptor { + size, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format, + usage: wgpu::TextureUsages::TEXTURE_BINDING + | wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::COPY_DST, + label: Some("GBuffer"), + view_formats: &[], + }); + + let view = device_texture.create_view(&wgpu::TextureViewDescriptor::default()); + let empty = Self { device_texture, view }; + + empty.clear(device, queue); + empty + } + + // Clear the texture. + pub fn clear(&self, device: &wgpu::Device, queue: &wgpu::Queue) { + let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("Clear command encoder"), + }); + + let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("Clear RenderPass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: self.view(), + resolve_target: None, + depth_slice: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color { + r: 0.0, + g: 0.0, + b: 0.0, + a: 0.0, + }), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + occlusion_query_set: None, + timestamp_writes: None, + multiview_mask: None, + }); + + drop(render_pass); + queue.submit(std::iter::once(encoder.finish())); + } + + pub fn texture(&self) -> &wgpu::Texture { + &self.device_texture + } + + pub fn view(&self) -> &wgpu::TextureView { + &self.view + } +} + +pub struct DepthStencilBuffer { + device_texture: wgpu::Texture, + view: wgpu::TextureView, +} + +impl DepthStencilBuffer { + pub fn empty_render_target( + device: &wgpu::Device, + queue: &wgpu::Queue, + width: u32, + height: u32, + format: wgpu::TextureFormat, + ) -> Self { + let size = wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }; + let device_texture = device.create_texture(&wgpu::TextureDescriptor { + size, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format, + usage: wgpu::TextureUsages::TEXTURE_BINDING + | wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::COPY_DST, + label: Some("GBuffer"), + view_formats: &[], + }); + + let view = device_texture.create_view(&wgpu::TextureViewDescriptor::default()); + let empty = Self { device_texture, view }; + + empty.clear(device, queue); + empty + } + + // Clear the texture. + pub fn clear(&self, device: &wgpu::Device, queue: &wgpu::Queue) { + let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("Clear command encoder"), + }); + + let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("Clear RenderPass"), + color_attachments: &[], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &self.view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Clear(0.0), + store: wgpu::StoreOp::Store, + }), + stencil_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Clear(0), + store: wgpu::StoreOp::Store, + }), + }), + occlusion_query_set: None, + timestamp_writes: None, + multiview_mask: None, + }); + + drop(render_pass); + queue.submit(std::iter::once(encoder.finish())); + } + pub fn texture(&self) -> &wgpu::Texture { &self.device_texture } @@ -68,3 +208,50 @@ impl DeviceTexture { &self.view } } + +/// Structure that holds render targets for interim rendering results. +pub struct GBuffer { + albedo: DeviceTexture, + emissive: DeviceTexture, + bump: DeviceTexture, + stencil: DepthStencilBuffer, +} + +impl GBuffer { + pub fn new(device: &wgpu::Device, queue: &wgpu::Queue, width: u32, height: u32) -> Self { + Self { + albedo: DeviceTexture::empty_render_target(device, queue, width, height, wgpu::TextureFormat::Rgba8Uint), + emissive: DeviceTexture::empty_render_target( + device, + queue, + width, + height, + wgpu::TextureFormat::Rgba32Float, + ), + bump: DeviceTexture::empty_render_target(device, queue, width, height, wgpu::TextureFormat::Rgba8Uint), + stencil: DepthStencilBuffer::empty_render_target( + device, + queue, + width, + height, + wgpu::TextureFormat::Depth24PlusStencil8, + ), + } + } + + pub fn albedo(&self) -> &DeviceTexture { + &self.albedo + } + + pub fn emissive(&self) -> &DeviceTexture { + &self.emissive + } + + pub fn bump(&self) -> &DeviceTexture { + &self.bump + } + + pub fn stencil(&self) -> &DepthStencilBuffer { + &self.stencil + } +} From fc1353b96d84994b4f18eaa0c4aa7722a47e23b1 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Thu, 19 Feb 2026 04:50:23 +0000 Subject: [PATCH 24/49] Tie GBuffer into all our render passes --- inox2d-wgpu/src/lib.rs | 147 +++++++++++++++++++------------------ inox2d-wgpu/src/texture.rs | 38 ++++++++++ 2 files changed, 115 insertions(+), 70 deletions(-) diff --git a/inox2d-wgpu/src/lib.rs b/inox2d-wgpu/src/lib.rs index 662603d..53fe282 100644 --- a/inox2d-wgpu/src/lib.rs +++ b/inox2d-wgpu/src/lib.rs @@ -166,6 +166,10 @@ impl<'window> InoxRenderer for WgpuRenderer<'window> { panic!("Recursive rendering is not permitted."); } + if self.gbuffer.is_none() { + panic!("Buffer is not yet set up."); + } + self.encoder = Some(self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("Inox2DWGPU"), })); @@ -196,82 +200,85 @@ impl<'window> InoxRenderer for WgpuRenderer<'window> { render_ctx: &render::TexturedMeshRenderCtx, id: InoxNodeUuid, ) { - //NOTE: borrowck doesn't want us borrowing the encoder, so we .take() it instead. - let mut encoder = self.encoder.take().expect("encoder"); - let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("WgpuRenderer::draw_textured_mesh_content"), - color_attachments: &[], //TODO: render target - depth_stencil_attachment: None, //TODO: MASKS - occlusion_query_set: None, - timestamp_writes: None, - multiview_mask: None, - }); - - let (albedo, bumpmap, emissive) = self.textures_for_part(components.texture); - - //TODO: set blend mode - let uni_in_vert = basic_vert::Input { - // TODO: there is no provision for the renderer to learn the - // current camera/viewport matrix OpenGLRenderer just has a - // pub parameter for it which is dumb. - mvp: Mat4::IDENTITY.to_cols_array_2d(), - offset: [0.0; 2], - } - .into_buffer(&self.device); - - if as_mask { - let uni_in_frag = basic_mask_frag::Input { - threshold: self.last_mask_threshold, + if let Some(gbuffer) = self.gbuffer.as_ref() { + //NOTE: borrowck doesn't want us borrowing the encoder, so we .take() it instead. + let mut encoder = self.encoder.take().expect("encoder"); + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("WgpuRenderer::draw_textured_mesh_content"), + color_attachments: &gbuffer.as_color_attachments(), + depth_stencil_attachment: gbuffer.as_depth_stencil_attachment(), + occlusion_query_set: None, + timestamp_writes: None, + multiview_mask: None, + }); + + let (albedo, bumpmap, emissive) = self.textures_for_part(components.texture); + + //TODO: set blend mode + let uni_in_vert = basic_vert::Input { + // TODO: there is no provision for the renderer to learn the + // current camera/viewport matrix OpenGLRenderer just has a + // pub parameter for it which is dumb. + mvp: Mat4::IDENTITY.to_cols_array_2d(), + offset: [0.0; 2], } .into_buffer(&self.device); - self.part_mask_pipeline.bind_frag( - &mut render_pass, - Some( - &self - .part_shader_mask_frag - .bind(&self.device, albedo.view(), &self.model_sampler, &uni_in_frag), - ), - ); - self.part_mask_pipeline.bind_vertex( - &mut render_pass, - Some(&self.part_shader_vert.bind(&self.device, &uni_in_vert)), - ); - - render_pass.set_pipeline(self.part_mask_pipeline.pipeline()); - } else { - //Regular parts - let uni_in_frag = basic_frag::Input { - opacity: components.drawable.blending.opacity, - multColor: components.drawable.blending.tint.into(), - screenColor: components.drawable.blending.screen_tint.into(), - emissionStrength: 1.0, //NOTE: OpenGL never sets this. + if as_mask { + let uni_in_frag = basic_mask_frag::Input { + threshold: self.last_mask_threshold, + } + .into_buffer(&self.device); + + self.part_mask_pipeline.bind_frag( + &mut render_pass, + Some(&self.part_shader_mask_frag.bind( + &self.device, + albedo.view(), + &self.model_sampler, + &uni_in_frag, + )), + ); + self.part_mask_pipeline.bind_vertex( + &mut render_pass, + Some(&self.part_shader_vert.bind(&self.device, &uni_in_vert)), + ); + + render_pass.set_pipeline(self.part_mask_pipeline.pipeline()); + } else { + //Regular parts + let uni_in_frag = basic_frag::Input { + opacity: components.drawable.blending.opacity, + multColor: components.drawable.blending.tint.into(), + screenColor: components.drawable.blending.screen_tint.into(), + emissionStrength: 1.0, //NOTE: OpenGL never sets this. + } + .into_buffer(&self.device); + + self.part_pipeline.bind_frag( + &mut render_pass, + Some(&self.part_shader_frag.bind( + &self.device, + albedo.view(), + bumpmap.view(), + emissive.view(), + &self.model_sampler, + &uni_in_frag, + )), + ); + self.part_pipeline.bind_vertex( + &mut render_pass, + Some(&self.part_shader_vert.bind(&self.device, &uni_in_vert)), + ); + + render_pass.set_pipeline(self.part_pipeline.pipeline()); } - .into_buffer(&self.device); - self.part_pipeline.bind_frag( - &mut render_pass, - Some(&self.part_shader_frag.bind( - &self.device, - albedo.view(), - bumpmap.view(), - emissive.view(), - &self.model_sampler, - &uni_in_frag, - )), - ); - self.part_pipeline.bind_vertex( - &mut render_pass, - Some(&self.part_shader_vert.bind(&self.device, &uni_in_vert)), - ); - - render_pass.set_pipeline(self.part_pipeline.pipeline()); - } + //TODO: Actual draw elements call - //TODO: Actual draw elements call - - drop(render_pass); //NOTE: borrowck also needs us to do this - self.encoder = Some(encoder); + drop(render_pass); //NOTE: borrowck also needs us to do this + self.encoder = Some(encoder); + } } fn begin_composite_content( diff --git a/inox2d-wgpu/src/texture.rs b/inox2d-wgpu/src/texture.rs index 672c1db..0473cb2 100644 --- a/inox2d-wgpu/src/texture.rs +++ b/inox2d-wgpu/src/texture.rs @@ -131,6 +131,18 @@ impl DeviceTexture { pub fn view(&self) -> &wgpu::TextureView { &self.view } + + pub fn as_color_attachment(&self) -> wgpu::RenderPassColorAttachment<'_> { + wgpu::RenderPassColorAttachment { + view: &self.view, + resolve_target: None, + depth_slice: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + } + } } pub struct DepthStencilBuffer { @@ -207,6 +219,20 @@ impl DepthStencilBuffer { pub fn view(&self) -> &wgpu::TextureView { &self.view } + + pub fn as_depth_stencil_attachment(&self) -> wgpu::RenderPassDepthStencilAttachment<'_> { + wgpu::RenderPassDepthStencilAttachment { + view: &self.view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }), + stencil_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }), + } + } } /// Structure that holds render targets for interim rendering results. @@ -254,4 +280,16 @@ impl GBuffer { pub fn stencil(&self) -> &DepthStencilBuffer { &self.stencil } + + pub fn as_color_attachments(&self) -> [Option>; 3] { + [ + Some(self.albedo.as_color_attachment()), + Some(self.emissive.as_color_attachment()), + Some(self.bump.as_color_attachment()), + ] + } + + pub fn as_depth_stencil_attachment(&self) -> Option> { + Some(self.stencil.as_depth_stencil_attachment()) + } } From 5f4a143d5770459527d6ab8441fd99503f052c87 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Thu, 19 Feb 2026 05:04:24 +0000 Subject: [PATCH 25/49] Add target outputs to all our fragment shaders --- inox2d-wgpu/build.rs | 59 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/inox2d-wgpu/build.rs b/inox2d-wgpu/build.rs index 7e36aef..b7dc57f 100644 --- a/inox2d-wgpu/build.rs +++ b/inox2d-wgpu/build.rs @@ -1,7 +1,8 @@ use shaderc; use spirv_reflect; use spirv_reflect::types::{ - ReflectBlockVariable, ReflectDescriptorType, ReflectEntryPoint, ReflectTypeDescription, ReflectTypeFlags, + ReflectBlockVariable, ReflectDescriptorType, ReflectEntryPoint, ReflectFormat, ReflectTypeDescription, + ReflectTypeFlags, }; use std::borrow::Cow; @@ -442,7 +443,61 @@ fn gen_fragmentshader_trait_methods( writeln!(out, " wgpu::FragmentState {{")?; writeln!(out, " module: &self.{},", entrypoint.name)?; writeln!(out, " entry_point: Some(\"{}\"),", entrypoint.name)?; - writeln!(out, " targets: &[],")?; + writeln!(out, " targets: &[")?; + for var in &entrypoint.output_variables { + writeln!(out, " Some(wgpu::ColorTargetState {{")?; + match var.format { + ReflectFormat::Undefined => { + writeln!(out, " format: //Unknown!")?; + } + ReflectFormat::R32_UINT => { + writeln!(out, " format: wgpu::TextureFormat::R32Uint,")?; + } + ReflectFormat::R32_SINT => { + writeln!(out, " format: wgpu::TextureFormat::R32Sint,")?; + } + ReflectFormat::R32_SFLOAT => { + writeln!(out, " format: wgpu::TextureFormat::R32Float,")?; + } + ReflectFormat::R32G32_UINT => { + writeln!(out, " format: wgpu::TextureFormat::Rg32Uint,")?; + } + ReflectFormat::R32G32_SINT => { + writeln!(out, " format: wgpu::TextureFormat::Rg32Sint,")?; + } + ReflectFormat::R32G32_SFLOAT => { + writeln!(out, " format: wgpu::TextureFormat::Rg32Float,")?; + } + + // WARN: These don't actually exist in WGPU! + ReflectFormat::R32G32B32_UINT => { + writeln!(out, " format: //wgpu::TextureFormat::Rgb32Uint,")?; + } + ReflectFormat::R32G32B32_SINT => { + writeln!(out, " format: //wgpu::TextureFormat::Rgb32Sint,")?; + } + ReflectFormat::R32G32B32_SFLOAT => { + writeln!(out, " format: //wgpu::TextureFormat::Rgb32Float,")?; + } + + ReflectFormat::R32G32B32A32_UINT => { + writeln!(out, " format: wgpu::TextureFormat::Rgba32Uint,")?; + } + ReflectFormat::R32G32B32A32_SINT => { + writeln!(out, " format: wgpu::TextureFormat::Rgba32Sint,")?; + } + ReflectFormat::R32G32B32A32_SFLOAT => { + writeln!(out, " format: wgpu::TextureFormat::Rgba32Float,")?; + } + } + + //TODO: This method should have a blendstate param? + writeln!(out, " blend: None,")?; + writeln!(out, " write_mask: wgpu::ColorWrites::ALL,")?; + + writeln!(out, " }}),")?; + } + writeln!(out, " ],")?; writeln!( out, " compilation_options: wgpu::PipelineCompilationOptions::default()" From a830a498f6f14c7bf813f1e79713b210067f23d9 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Sat, 21 Feb 2026 00:17:06 +0000 Subject: [PATCH 26/49] Upload Inox vertex buffers --- inox2d-wgpu/src/lib.rs | 75 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/inox2d-wgpu/src/lib.rs b/inox2d-wgpu/src/lib.rs index 53fe282..c916e6c 100644 --- a/inox2d-wgpu/src/lib.rs +++ b/inox2d-wgpu/src/lib.rs @@ -4,6 +4,7 @@ use inox2d::node::{InoxNodeUuid, components, drawables}; //hey wait a second tha use inox2d::render::{self, InoxRenderer}; use inox2d::texture::decode_model_textures; use wgpu; +use wgpu::util::{BufferInitDescriptor, DeviceExt}; mod pipeline; mod shader; @@ -14,12 +15,37 @@ use crate::texture::{DeviceTexture, GBuffer}; use shader::UniformBlock; use shaders::basic::{basic_frag, basic_mask_frag, basic_vert, composite_frag, composite_mask_frag, composite_vert}; +/// Cast Vec2 to array. +/// +/// SAFETY: This inherits the safety considerations of glam's own +/// `upload_array_to_gl`. Specifically, we rely on the fact that it's own Vec2 +/// struct is plain-ol-data and we're only working with immutables. +/// +/// NOTE: At some point, rewrite inox2D's vertex arrays to use bytemuck and a +/// custom Vec2 struct. +pub fn cast_vec2(array: &[glam::Vec2]) -> &[u8] { + unsafe { std::slice::from_raw_parts(array.as_ptr() as *const u8, std::mem::size_of_val(array)) } +} + +/// Cast u16s to array. +/// +/// SAFETY: This inherits the safety considerations of glam's own +/// `upload_array_to_gl`. +/// +/// NOTE: This probably can already be bytemucked +pub fn cast_index(array: &[u16]) -> &[u8] { + unsafe { std::slice::from_raw_parts(array.as_ptr() as *const u8, std::mem::size_of_val(array)) } +} + #[derive(Debug, thiserror::Error)] #[error("Could not initialize wgpu renderer: {0}")] pub enum WgpuRendererError { CreateSurfaceError(#[from] wgpu::CreateSurfaceError), RequestAdapterError(#[from] wgpu::RequestAdapterError), RequestDeviceError(#[from] wgpu::RequestDeviceError), + + #[error("Model rendering not initialized")] + ModelRenderingNotInitialized, } pub struct WgpuRenderer<'window> { @@ -27,6 +53,11 @@ pub struct WgpuRenderer<'window> { config: wgpu::SurfaceConfiguration, gbuffer: Option, + verts: wgpu::Buffer, + uvs: wgpu::Buffer, + deforms: wgpu::Buffer, + indices: wgpu::Buffer, + part_shader_vert: basic_vert::Shader, part_shader_frag: basic_frag::Shader, part_shader_mask_frag: basic_mask_frag::Shader, @@ -99,7 +130,45 @@ impl<'window> WgpuRenderer<'window> { let composite_mask_pipeline = pipeline::Pipeline::new(&device, &composite_shader_vert, &composite_shader_mask_frag); - //TODO: Upload model textures, verts, uvs, deforms, indicies + let inox_buffers = model + .puppet + .render_ctx + .as_ref() + .ok_or(WgpuRendererError::ModelRenderingNotInitialized)?; + //TODO: Change inox2d upstream to use a bytemuck-able array + let verts = device.create_buffer_init(&BufferInitDescriptor { + label: Some(&format!( + "Inox2D {}::Verts", + model.puppet.meta.name.as_deref().unwrap_or("") + )), + contents: cast_vec2(inox_buffers.vertex_buffers.verts.as_slice()), + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + }); + let uvs = device.create_buffer_init(&BufferInitDescriptor { + label: Some(&format!( + "Inox2D {}::Verts", + model.puppet.meta.name.as_deref().unwrap_or("") + )), + contents: cast_vec2(inox_buffers.vertex_buffers.uvs.as_slice()), + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + }); + let deforms = device.create_buffer_init(&BufferInitDescriptor { + label: Some(&format!( + "Inox2D {}::Verts", + model.puppet.meta.name.as_deref().unwrap_or("") + )), + contents: cast_vec2(inox_buffers.vertex_buffers.deforms.as_slice()), + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + }); + let indices = device.create_buffer_init(&BufferInitDescriptor { + label: Some(&format!( + "Inox2D {}::Verts", + model.puppet.meta.name.as_deref().unwrap_or("") + )), + contents: cast_index(inox_buffers.vertex_buffers.indices.as_slice()), + usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST, + }); + let decoded_textures = decode_model_textures(model.textures.iter()); let mut texture_handles = vec![]; for (index, texture) in decoded_textures.iter().enumerate() { @@ -123,6 +192,10 @@ impl<'window> WgpuRenderer<'window> { surface, config, gbuffer: None, + verts, + uvs, + deforms, + indices, part_shader_vert, part_shader_frag, part_shader_mask_frag, From 2ef98e891b3674ecfa0f4497a2282aa74d1cfd79 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Sat, 21 Feb 2026 00:32:39 +0000 Subject: [PATCH 27/49] Publish location constants --- inox2d-wgpu/build.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/inox2d-wgpu/build.rs b/inox2d-wgpu/build.rs index b7dc57f..acc9030 100644 --- a/inox2d-wgpu/build.rs +++ b/inox2d-wgpu/build.rs @@ -558,7 +558,7 @@ fn introspect_spirv( writeln!(out, "/// END members:")?; writeln!( out, - "const INPUT_LOCATION_{}: u32 = {};", + "pub const INPUT_LOCATION_{}: u32 = {};", var.name.to_uppercase(), var.location )?; @@ -582,12 +582,17 @@ fn introspect_spirv( writeln!(out, " /// Format: {:?}", var.format)?; } writeln!(out, "/// END members:")?; - writeln!( - out, - "const OUTPUT_LOCATION_{}: u32 = {};", - var.name.to_uppercase(), - var.location - )?; + + if var.name != "" { + writeln!( + out, + "pub const OUTPUT_LOCATION_{}: u32 = {};", + var.name.to_uppercase(), + var.location + )?; + } else { + writeln!(out, "/// Declaration elided")?; + } } for descriptor_set in &entrypoint.descriptor_sets { From 729ad922efbd2dd63c771ce31f903167dd5d595b Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Sat, 21 Feb 2026 00:33:28 +0000 Subject: [PATCH 28/49] Finish the normal rendering pass --- inox2d-wgpu/src/lib.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/inox2d-wgpu/src/lib.rs b/inox2d-wgpu/src/lib.rs index c916e6c..a3b6b19 100644 --- a/inox2d-wgpu/src/lib.rs +++ b/inox2d-wgpu/src/lib.rs @@ -297,6 +297,11 @@ impl<'window> InoxRenderer for WgpuRenderer<'window> { } .into_buffer(&self.device); + render_pass.set_vertex_buffer(basic_vert::INPUT_LOCATION_VERTS, self.verts.slice(..)); + render_pass.set_vertex_buffer(basic_vert::INPUT_LOCATION_UVS, self.uvs.slice(..)); + render_pass.set_vertex_buffer(basic_vert::INPUT_LOCATION_DEFORM, self.deforms.slice(..)); + render_pass.set_index_buffer(self.indices.slice(..), wgpu::IndexFormat::Uint16); + if as_mask { let uni_in_frag = basic_mask_frag::Input { threshold: self.last_mask_threshold, @@ -347,7 +352,7 @@ impl<'window> InoxRenderer for WgpuRenderer<'window> { render_pass.set_pipeline(self.part_pipeline.pipeline()); } - //TODO: Actual draw elements call + render_pass.draw_indexed(0..render_ctx.index_len as u32, render_ctx.index_offset as i32, 0..1); drop(render_pass); //NOTE: borrowck also needs us to do this self.encoder = Some(encoder); From 5dbabae3e274bb4f3b10ac86c8b9db1d0108eb87 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Sat, 21 Feb 2026 16:49:32 +0000 Subject: [PATCH 29/49] Implement use of stencil buffer for masking --- inox2d-wgpu/src/lib.rs | 123 +++++++++++++++++++++++++++++++----- inox2d-wgpu/src/pipeline.rs | 4 +- inox2d-wgpu/src/texture.rs | 63 +++++++++++------- 3 files changed, 151 insertions(+), 39 deletions(-) diff --git a/inox2d-wgpu/src/lib.rs b/inox2d-wgpu/src/lib.rs index a3b6b19..db43123 100644 --- a/inox2d-wgpu/src/lib.rs +++ b/inox2d-wgpu/src/lib.rs @@ -63,6 +63,7 @@ pub struct WgpuRenderer<'window> { part_shader_mask_frag: basic_mask_frag::Shader, part_pipeline: pipeline::Pipeline, + part_pipeline_masked: pipeline::Pipeline, part_mask_pipeline: pipeline::Pipeline, composite_shader_vert: composite_vert::Shader, @@ -70,6 +71,7 @@ pub struct WgpuRenderer<'window> { composite_shader_mask_frag: composite_mask_frag::Shader, composite_pipeline: pipeline::Pipeline, + composite_pipeline_masked: pipeline::Pipeline, composite_mask_pipeline: pipeline::Pipeline, encoder: Option, @@ -78,6 +80,8 @@ pub struct WgpuRenderer<'window> { model_sampler: wgpu::Sampler, last_mask_threshold: f32, + is_in_mask: bool, + stencil_reference_value: u32, device: wgpu::Device, queue: wgpu::Queue, @@ -119,16 +123,73 @@ impl<'window> WgpuRenderer<'window> { let part_shader_frag = basic_frag::Shader::new(&device); let part_shader_mask_frag = basic_mask_frag::Shader::new(&device); - let part_pipeline = pipeline::Pipeline::new(&device, &part_shader_vert, &part_shader_frag); - let part_mask_pipeline = pipeline::Pipeline::new(&device, &part_shader_vert, &part_shader_mask_frag); + let masked_depthstencil = wgpu::DepthStencilState { + format: wgpu::TextureFormat::Depth24PlusStencil8, + depth_write_enabled: false, + depth_compare: wgpu::CompareFunction::Always, + stencil: wgpu::StencilState { + front: wgpu::StencilFaceState::IGNORE, + back: wgpu::StencilFaceState::IGNORE, + read_mask: 0xFF, + write_mask: 0x00, + }, + bias: wgpu::DepthBiasState::default(), + }; + + let mask_depthstencil = wgpu::DepthStencilState { + format: wgpu::TextureFormat::Depth24PlusStencil8, + depth_write_enabled: false, + depth_compare: wgpu::CompareFunction::Always, + stencil: wgpu::StencilState { + front: wgpu::StencilFaceState { + compare: wgpu::CompareFunction::Always, + fail_op: wgpu::StencilOperation::Keep, + depth_fail_op: wgpu::StencilOperation::Keep, + pass_op: wgpu::StencilOperation::Replace, + }, + back: wgpu::StencilFaceState { + compare: wgpu::CompareFunction::Always, + fail_op: wgpu::StencilOperation::Keep, + depth_fail_op: wgpu::StencilOperation::Keep, + pass_op: wgpu::StencilOperation::Replace, + }, + read_mask: 0xFF, + write_mask: 0xFF, + }, + bias: wgpu::DepthBiasState::default(), + }; + + let part_pipeline = pipeline::Pipeline::new(&device, &part_shader_vert, &part_shader_frag, None); + let part_pipeline_masked = pipeline::Pipeline::new( + &device, + &part_shader_vert, + &part_shader_frag, + Some(masked_depthstencil.clone()), + ); + let part_mask_pipeline = pipeline::Pipeline::new( + &device, + &part_shader_vert, + &part_shader_mask_frag, + Some(mask_depthstencil.clone()), + ); let composite_shader_vert = composite_vert::Shader::new(&device); let composite_shader_frag = composite_frag::Shader::new(&device); let composite_shader_mask_frag = composite_mask_frag::Shader::new(&device); - let composite_pipeline = pipeline::Pipeline::new(&device, &composite_shader_vert, &composite_shader_frag); - let composite_mask_pipeline = - pipeline::Pipeline::new(&device, &composite_shader_vert, &composite_shader_mask_frag); + let composite_pipeline = pipeline::Pipeline::new(&device, &composite_shader_vert, &composite_shader_frag, None); + let composite_pipeline_masked = pipeline::Pipeline::new( + &device, + &composite_shader_vert, + &composite_shader_frag, + Some(masked_depthstencil), + ); + let composite_mask_pipeline = pipeline::Pipeline::new( + &device, + &composite_shader_vert, + &composite_shader_mask_frag, + Some(mask_depthstencil), + ); let inox_buffers = model .puppet @@ -200,16 +261,20 @@ impl<'window> WgpuRenderer<'window> { part_shader_frag, part_shader_mask_frag, part_pipeline, + part_pipeline_masked, part_mask_pipeline, composite_shader_vert, composite_shader_frag, composite_shader_mask_frag, composite_pipeline, + composite_pipeline_masked, composite_mask_pipeline, encoder: None, model_textures: texture_handles, model_sampler, last_mask_threshold: 0.0, + is_in_mask: false, + stencil_reference_value: 1, device, queue, }) @@ -220,7 +285,14 @@ impl<'window> WgpuRenderer<'window> { self.config.width = width; self.config.height = height; self.surface.configure(&self.device, &self.config); - self.gbuffer = Some(GBuffer::new(&self.device, &self.queue, width, height)); + self.gbuffer = Some(GBuffer::new( + &self.device, + &self.queue, + width, + height, + wgpu::TextureFormat::Rgba32Float, + wgpu::TextureFormat::Depth24PlusStencil8, + )); } } @@ -246,24 +318,29 @@ impl<'window> InoxRenderer for WgpuRenderer<'window> { self.encoder = Some(self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("Inox2DWGPU"), })); + + //TODO: read & translate OpenGLRenderer's `on_begin_draw` / `on_end_draw` } fn on_begin_masks(&mut self, masks: &components::Masks) { self.last_mask_threshold = masks.threshold.clamp(0.0, 1.0); + //TODO: Erase the stencil buffer. //TODO: Enable stencilling on the render target. + + self.is_in_mask = true; } - fn on_begin_mask(&self, mask: &components::Mask) { - unimplemented!() + fn on_begin_mask(&mut self, mask: &components::Mask) { + self.stencil_reference_value = (mask.mode == components::MaskMode::Mask) as u32; } fn on_begin_masked_content(&self) { unimplemented!() } - fn on_end_mask(&self) { - unimplemented!() + fn on_end_mask(&mut self) { + self.is_in_mask = false; } fn draw_textured_mesh_content( @@ -271,15 +348,23 @@ impl<'window> InoxRenderer for WgpuRenderer<'window> { as_mask: bool, components: &drawables::TexturedMeshComponents, render_ctx: &render::TexturedMeshRenderCtx, - id: InoxNodeUuid, + _id: InoxNodeUuid, ) { if let Some(gbuffer) = self.gbuffer.as_ref() { + let depth_stencil_attachment = if as_mask { + Some(gbuffer.stencil().as_depth_stencil_attachment_rw()) + } else if self.is_in_mask { + Some(gbuffer.stencil().as_depth_stencil_attachment_ro()) + } else { + None + }; + //NOTE: borrowck doesn't want us borrowing the encoder, so we .take() it instead. let mut encoder = self.encoder.take().expect("encoder"); let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("WgpuRenderer::draw_textured_mesh_content"), color_attachments: &gbuffer.as_color_attachments(), - depth_stencil_attachment: gbuffer.as_depth_stencil_attachment(), + depth_stencil_attachment, occlusion_query_set: None, timestamp_writes: None, multiview_mask: None, @@ -322,8 +407,15 @@ impl<'window> InoxRenderer for WgpuRenderer<'window> { Some(&self.part_shader_vert.bind(&self.device, &uni_in_vert)), ); + render_pass.set_stencil_reference(self.stencil_reference_value); render_pass.set_pipeline(self.part_mask_pipeline.pipeline()); } else { + let pipeline = if self.is_in_mask { + &self.part_pipeline_masked + } else { + &self.part_pipeline + }; + //Regular parts let uni_in_frag = basic_frag::Input { opacity: components.drawable.blending.opacity, @@ -333,7 +425,7 @@ impl<'window> InoxRenderer for WgpuRenderer<'window> { } .into_buffer(&self.device); - self.part_pipeline.bind_frag( + pipeline.bind_frag( &mut render_pass, Some(&self.part_shader_frag.bind( &self.device, @@ -344,12 +436,13 @@ impl<'window> InoxRenderer for WgpuRenderer<'window> { &uni_in_frag, )), ); - self.part_pipeline.bind_vertex( + pipeline.bind_vertex( &mut render_pass, Some(&self.part_shader_vert.bind(&self.device, &uni_in_vert)), ); - render_pass.set_pipeline(self.part_pipeline.pipeline()); + render_pass.set_stencil_reference(1); + render_pass.set_pipeline(pipeline.pipeline()); } render_pass.draw_indexed(0..render_ctx.index_len as u32, render_ctx.index_offset as i32, 0..1); diff --git a/inox2d-wgpu/src/pipeline.rs b/inox2d-wgpu/src/pipeline.rs index 2cdae92..99cc91b 100644 --- a/inox2d-wgpu/src/pipeline.rs +++ b/inox2d-wgpu/src/pipeline.rs @@ -18,7 +18,7 @@ where V: VertexShader, F: FragmentShader, { - pub fn new(device: &wgpu::Device, vert: &V, frag: &F) -> Self { + pub fn new(device: &wgpu::Device, vert: &V, frag: &F, depth_stencil: Option) -> Self { let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("Pipeline"), @@ -40,7 +40,7 @@ where cull_mode: Some(wgpu::Face::Back), //TODO: I'm pretty sure the GL renderer doesn't do this ..Default::default() }, - depth_stencil: None, + depth_stencil, multisample: wgpu::MultisampleState::default(), multiview_mask: None, cache: None, diff --git a/inox2d-wgpu/src/texture.rs b/inox2d-wgpu/src/texture.rs index 0473cb2..7c7d9c0 100644 --- a/inox2d-wgpu/src/texture.rs +++ b/inox2d-wgpu/src/texture.rs @@ -148,6 +148,7 @@ impl DeviceTexture { pub struct DepthStencilBuffer { device_texture: wgpu::Texture, view: wgpu::TextureView, + format: wgpu::TextureFormat, } impl DepthStencilBuffer { @@ -177,7 +178,11 @@ impl DepthStencilBuffer { }); let view = device_texture.create_view(&wgpu::TextureViewDescriptor::default()); - let empty = Self { device_texture, view }; + let empty = Self { + device_texture, + view, + format, + }; empty.clear(device, queue); empty @@ -220,15 +225,38 @@ impl DepthStencilBuffer { &self.view } - pub fn as_depth_stencil_attachment(&self) -> wgpu::RenderPassDepthStencilAttachment<'_> { + pub fn format(&self) -> wgpu::TextureFormat { + self.format + } + + pub fn as_depth_stencil_attachment_rw(&self) -> wgpu::RenderPassDepthStencilAttachment<'_> { wgpu::RenderPassDepthStencilAttachment { view: &self.view, - depth_ops: Some(wgpu::Operations { + depth_ops: None, + stencil_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store, }), + } + } + + pub fn as_depth_stencil_attachment_ro(&self) -> wgpu::RenderPassDepthStencilAttachment<'_> { + wgpu::RenderPassDepthStencilAttachment { + view: &self.view, + depth_ops: None, stencil_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Discard, + }), + } + } + + pub fn as_depth_stencil_attachment_clear(&self, clear_value: u32) -> wgpu::RenderPassDepthStencilAttachment<'_> { + wgpu::RenderPassDepthStencilAttachment { + view: &self.view, + depth_ops: None, + stencil_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Clear(clear_value), store: wgpu::StoreOp::Store, }), } @@ -244,24 +272,19 @@ pub struct GBuffer { } impl GBuffer { - pub fn new(device: &wgpu::Device, queue: &wgpu::Queue, width: u32, height: u32) -> Self { + pub fn new( + device: &wgpu::Device, + queue: &wgpu::Queue, + width: u32, + height: u32, + format: wgpu::TextureFormat, + depth_format: wgpu::TextureFormat, + ) -> Self { Self { albedo: DeviceTexture::empty_render_target(device, queue, width, height, wgpu::TextureFormat::Rgba8Uint), - emissive: DeviceTexture::empty_render_target( - device, - queue, - width, - height, - wgpu::TextureFormat::Rgba32Float, - ), + emissive: DeviceTexture::empty_render_target(device, queue, width, height, format), bump: DeviceTexture::empty_render_target(device, queue, width, height, wgpu::TextureFormat::Rgba8Uint), - stencil: DepthStencilBuffer::empty_render_target( - device, - queue, - width, - height, - wgpu::TextureFormat::Depth24PlusStencil8, - ), + stencil: DepthStencilBuffer::empty_render_target(device, queue, width, height, depth_format), } } @@ -288,8 +311,4 @@ impl GBuffer { Some(self.bump.as_color_attachment()), ] } - - pub fn as_depth_stencil_attachment(&self) -> Option> { - Some(self.stencil.as_depth_stencil_attachment()) - } } From 4d6ffcdb023f6a5c0a3427ca87ecc244ddbdf286 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Sun, 22 Feb 2026 00:48:35 +0000 Subject: [PATCH 30/49] Silence some Rust warnings --- inox2d-wgpu/build.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/inox2d-wgpu/build.rs b/inox2d-wgpu/build.rs index acc9030..d5bd18f 100644 --- a/inox2d-wgpu/build.rs +++ b/inox2d-wgpu/build.rs @@ -11,7 +11,7 @@ use std::ffi::OsString; use std::fmt::Write; use std::{fs, path}; -fn spirv_to_rust_type(typemember: &ReflectTypeDescription) -> Result, Box> { +fn spirv_to_rust_type<'a>(typemember: &'a ReflectTypeDescription) -> Result, Box> { let base_type = if typemember.type_flags.contains(ReflectTypeFlags::FLOAT) { match typemember.traits.numeric.scalar.width { 32 => "f32", @@ -49,7 +49,7 @@ fn spirv_to_rust_type(typemember: &ReflectTypeDescription) -> Result, B } } -fn spirv_to_wgpu_vertex_format(typemember: &ReflectTypeDescription) -> Result, Box> { +fn spirv_to_wgpu_vertex_format<'a>(typemember: &'a ReflectTypeDescription) -> Result, Box> { let base_type = if typemember.type_flags.contains(ReflectTypeFlags::FLOAT) { match typemember.traits.numeric.scalar.width { 32 => "Float32", @@ -203,7 +203,7 @@ fn gen_shader_new( if binding.block.size > 0 { writeln!( out, - " min_binding_size: Some(NonZero::new({}).expect(\"nonzero type\")),", + " min_binding_size: Some(std::num::NonZero::new({}).expect(\"nonzero type\")),", binding.block.size )?; } else { @@ -531,8 +531,6 @@ fn introspect_spirv( writeln!(out, "use wgpu;")?; writeln!(out, "use wgpu::include_spirv;")?; writeln!(out)?; - writeln!(out, "use std::num::NonZero;")?; - writeln!(out)?; writeln!(out, "use crate::shader;")?; for entrypoint in module.enumerate_entry_points()? { From b8490af251f0cd9e44a03c4543b857424cefe437 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Sun, 22 Feb 2026 00:50:03 +0000 Subject: [PATCH 31/49] Fragment shaders now expose a type for their blend states. I originally considered making this a normal array, but the const generics threatened to spill over to the entire program. Instead, we have an associated type which acts like an array and expected to be one. --- inox2d-wgpu/build.rs | 11 ++++++++++- inox2d-wgpu/src/shader.rs | 7 ++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/inox2d-wgpu/build.rs b/inox2d-wgpu/build.rs index d5bd18f..4952cbf 100644 --- a/inox2d-wgpu/build.rs +++ b/inox2d-wgpu/build.rs @@ -439,6 +439,12 @@ fn gen_fragmentshader_trait_methods( struct_name: &str, ) -> Result<(), Box> { writeln!(out, "impl shader::FragmentShader for {} {{", struct_name)?; + writeln!( + out, + " type BlendStates = [Option; {}];", + entrypoint.output_variables.len() + )?; + writeln!(out)?; writeln!(out, " fn as_fragment_state(&self) -> wgpu::FragmentState {{")?; writeln!(out, " wgpu::FragmentState {{")?; writeln!(out, " module: &self.{},", entrypoint.name)?; @@ -491,7 +497,9 @@ fn gen_fragmentshader_trait_methods( } } - //TODO: This method should have a blendstate param? + // NOTE: This method CANNOT have a blendstate param as self-borrowed + // values are one of Rust's inconceivable types. + // See: https://blog.polybdenum.com/2024/06/07/the-inconceivable-types-of-rust-how-to-make-self-borrows-safe.html writeln!(out, " blend: None,")?; writeln!(out, " write_mask: wgpu::ColorWrites::ALL,")?; @@ -666,6 +674,7 @@ fn introspect_spirv( writeln!(out, "/// Entry point {}", entrypoint.name)?; writeln!(out, "/// Execution model {:?}", entrypoint.spirv_execution_model)?; writeln!(out, "/// Shader stage {:?}", entrypoint.shader_stage)?; + writeln!(out, "#[derive(Clone)]")?; writeln!(out, "pub struct {} {{", struct_name)?; writeln!(out, " {}: wgpu::ShaderModule,", entrypoint.name)?; writeln!(out, " bindgroup_layout: wgpu::BindGroupLayout")?; diff --git a/inox2d-wgpu/src/shader.rs b/inox2d-wgpu/src/shader.rs index 26b56a0..49fd34a 100644 --- a/inox2d-wgpu/src/shader.rs +++ b/inox2d-wgpu/src/shader.rs @@ -9,7 +9,12 @@ pub trait VertexShader: Shader { fn as_vertex_state(&self) -> wgpu::VertexState; } -pub trait FragmentShader: Shader { +pub trait FragmentShader: Shader +where + Self::BlendStates: IntoIterator> + Eq + Hash + Clone, +{ + type BlendStates; + fn as_fragment_state(&self) -> wgpu::FragmentState; } From 22b6556ff1141746fa12928834e7d488781077d7 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Sun, 22 Feb 2026 00:51:45 +0000 Subject: [PATCH 32/49] Add `PipelineGroup` to cache different blend/stencil variants of a given pipeline. The OpenGL renderer just assumes we can change these options on the fly, which isn't how modern GPUs work. I suspect it might have been easier to ubershader this, but I didn't want to change the shader code too much. Even with all 24 possible pipeline combinations compiling this shouldn't be too much of a drag on performance. --- inox2d-wgpu/src/lib.rs | 127 ++++++++++++++++++++++++------------ inox2d-wgpu/src/pipeline.rs | 61 ++++++++++++++++- inox2d-wgpu/src/shader.rs | 4 +- inox2d-wgpu/src/texture.rs | 1 + 4 files changed, 149 insertions(+), 44 deletions(-) diff --git a/inox2d-wgpu/src/lib.rs b/inox2d-wgpu/src/lib.rs index db43123..ec964e9 100644 --- a/inox2d-wgpu/src/lib.rs +++ b/inox2d-wgpu/src/lib.rs @@ -1,5 +1,5 @@ use glam::Mat4; -use inox2d::model::{Model, ModelTexture}; +use inox2d::model::Model; use inox2d::node::{InoxNodeUuid, components, drawables}; //hey wait a second that's just a u32 newtype! UUIDs are four of those! use inox2d::render::{self, InoxRenderer}; use inox2d::texture::decode_model_textures; @@ -62,17 +62,18 @@ pub struct WgpuRenderer<'window> { part_shader_frag: basic_frag::Shader, part_shader_mask_frag: basic_mask_frag::Shader, - part_pipeline: pipeline::Pipeline, - part_pipeline_masked: pipeline::Pipeline, - part_mask_pipeline: pipeline::Pipeline, + masked_depthstencil: wgpu::DepthStencilState, + mask_depthstencil: wgpu::DepthStencilState, + + part_pipeline: pipeline::PipelineGroup, + part_mask_pipeline: pipeline::PipelineGroup, composite_shader_vert: composite_vert::Shader, composite_shader_frag: composite_frag::Shader, composite_shader_mask_frag: composite_mask_frag::Shader, - composite_pipeline: pipeline::Pipeline, - composite_pipeline_masked: pipeline::Pipeline, - composite_mask_pipeline: pipeline::Pipeline, + composite_pipeline: pipeline::PipelineGroup, + composite_mask_pipeline: pipeline::PipelineGroup, encoder: Option, @@ -159,37 +160,20 @@ impl<'window> WgpuRenderer<'window> { bias: wgpu::DepthBiasState::default(), }; - let part_pipeline = pipeline::Pipeline::new(&device, &part_shader_vert, &part_shader_frag, None); - let part_pipeline_masked = pipeline::Pipeline::new( - &device, - &part_shader_vert, - &part_shader_frag, - Some(masked_depthstencil.clone()), - ); - let part_mask_pipeline = pipeline::Pipeline::new( - &device, - &part_shader_vert, - &part_shader_mask_frag, - Some(mask_depthstencil.clone()), - ); + //TODO: We need a pipeline per Inochi blending mode + //(or some kind of ubershader blending) + + let part_pipeline = pipeline::PipelineGroup::new(part_shader_vert.clone(), part_shader_frag.clone()); + let part_mask_pipeline = pipeline::PipelineGroup::new(part_shader_vert.clone(), part_shader_mask_frag.clone()); let composite_shader_vert = composite_vert::Shader::new(&device); let composite_shader_frag = composite_frag::Shader::new(&device); let composite_shader_mask_frag = composite_mask_frag::Shader::new(&device); - let composite_pipeline = pipeline::Pipeline::new(&device, &composite_shader_vert, &composite_shader_frag, None); - let composite_pipeline_masked = pipeline::Pipeline::new( - &device, - &composite_shader_vert, - &composite_shader_frag, - Some(masked_depthstencil), - ); - let composite_mask_pipeline = pipeline::Pipeline::new( - &device, - &composite_shader_vert, - &composite_shader_mask_frag, - Some(mask_depthstencil), - ); + let composite_pipeline = + pipeline::PipelineGroup::new(composite_shader_vert.clone(), composite_shader_frag.clone()); + let composite_mask_pipeline = + pipeline::PipelineGroup::new(composite_shader_vert.clone(), composite_shader_mask_frag.clone()); let inox_buffers = model .puppet @@ -260,14 +244,14 @@ impl<'window> WgpuRenderer<'window> { part_shader_vert, part_shader_frag, part_shader_mask_frag, + mask_depthstencil, + masked_depthstencil, part_pipeline, - part_pipeline_masked, part_mask_pipeline, composite_shader_vert, composite_shader_frag, composite_shader_mask_frag, composite_pipeline, - composite_pipeline_masked, composite_mask_pipeline, encoder: None, model_textures: texture_handles, @@ -303,6 +287,51 @@ impl<'window> WgpuRenderer<'window> { &self.model_textures[part.tex_emissive.raw()], ) } + + fn blend_mode_to_state(state: components::BlendMode) -> wgpu::BlendState { + let component = match state { + components::BlendMode::Normal => wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::One, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + components::BlendMode::Multiply => wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::Dst, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + components::BlendMode::ColorDodge => wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::Dst, + dst_factor: wgpu::BlendFactor::One, + operation: wgpu::BlendOperation::Add, + }, + components::BlendMode::LinearDodge => wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::One, + dst_factor: wgpu::BlendFactor::One, + operation: wgpu::BlendOperation::Add, + }, + components::BlendMode::Screen => wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::One, + dst_factor: wgpu::BlendFactor::OneMinusSrc, + operation: wgpu::BlendOperation::Add, + }, + components::BlendMode::ClipToLower => wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::DstAlpha, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + components::BlendMode::SliceFromLower => wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::OneMinusDstAlpha, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Subtract, + }, + }; + + wgpu::BlendState { + color: component, + alpha: component, + } + } } impl<'window> InoxRenderer for WgpuRenderer<'window> { @@ -359,6 +388,9 @@ impl<'window> InoxRenderer for WgpuRenderer<'window> { None }; + //TODO: Do we even want blending on in Normal mode? + let blend = Some(Self::blend_mode_to_state(components.drawable.blending.mode)); + //NOTE: borrowck doesn't want us borrowing the encoder, so we .take() it instead. let mut encoder = self.encoder.take().expect("encoder"); let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { @@ -371,6 +403,7 @@ impl<'window> InoxRenderer for WgpuRenderer<'window> { }); let (albedo, bumpmap, emissive) = self.textures_for_part(components.texture); + let (albedo, bumpmap, emissive) = (albedo.clone(), bumpmap.clone(), emissive.clone()); //TODO: set blend mode let uni_in_vert = basic_vert::Input { @@ -388,12 +421,19 @@ impl<'window> InoxRenderer for WgpuRenderer<'window> { render_pass.set_index_buffer(self.indices.slice(..), wgpu::IndexFormat::Uint16); if as_mask { + //TODO: What happens if a mask is also masked? + let pipeline = self.part_mask_pipeline.with_configuration( + &self.device, + [blend], + Some(self.mask_depthstencil.clone()), + ); let uni_in_frag = basic_mask_frag::Input { threshold: self.last_mask_threshold, } .into_buffer(&self.device); - self.part_mask_pipeline.bind_frag( + render_pass.set_pipeline(pipeline.pipeline()); + pipeline.bind_frag( &mut render_pass, Some(&self.part_shader_mask_frag.bind( &self.device, @@ -402,21 +442,25 @@ impl<'window> InoxRenderer for WgpuRenderer<'window> { &uni_in_frag, )), ); - self.part_mask_pipeline.bind_vertex( + pipeline.bind_vertex( &mut render_pass, Some(&self.part_shader_vert.bind(&self.device, &uni_in_vert)), ); render_pass.set_stencil_reference(self.stencil_reference_value); - render_pass.set_pipeline(self.part_mask_pipeline.pipeline()); } else { + //Regular parts let pipeline = if self.is_in_mask { - &self.part_pipeline_masked + self.part_pipeline.with_configuration( + &self.device, + [blend, blend, blend], + Some(self.masked_depthstencil.clone()), + ) } else { - &self.part_pipeline + self.part_pipeline + .with_configuration(&self.device, [blend, blend, blend], None) }; - //Regular parts let uni_in_frag = basic_frag::Input { opacity: components.drawable.blending.opacity, multColor: components.drawable.blending.tint.into(), @@ -425,6 +469,7 @@ impl<'window> InoxRenderer for WgpuRenderer<'window> { } .into_buffer(&self.device); + render_pass.set_pipeline(pipeline.pipeline()); pipeline.bind_frag( &mut render_pass, Some(&self.part_shader_frag.bind( diff --git a/inox2d-wgpu/src/pipeline.rs b/inox2d-wgpu/src/pipeline.rs index 99cc91b..ae228bb 100644 --- a/inox2d-wgpu/src/pipeline.rs +++ b/inox2d-wgpu/src/pipeline.rs @@ -1,6 +1,7 @@ use wgpu; use crate::shader::{FragmentShader, VertexShader}; +use std::collections::HashMap; use std::marker::PhantomData; pub struct Pipeline @@ -18,7 +19,13 @@ where V: VertexShader, F: FragmentShader, { - pub fn new(device: &wgpu::Device, vert: &V, frag: &F, depth_stencil: Option) -> Self { + pub fn new( + device: &wgpu::Device, + vert: &V, + frag: &F, + blend: F::BlendStates, + depth_stencil: Option, + ) -> Self { let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("Pipeline"), @@ -27,12 +34,22 @@ where immediate_size: 0, }); + let mut fragment = frag.as_fragment_state(); + let mut fragment_targets = fragment.targets.to_owned(); + for (index, blend) in blend.into_iter().enumerate() { + fragment_targets[index] + .as_mut() + .expect("FragmentShader should require as many blend states as it creates targets") + .blend = blend; + } + fragment.targets = &fragment_targets; + Self { pipeline: device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some("Pipeline"), layout: Some(&layout), vertex: vert.as_vertex_state(), - fragment: Some(frag.as_fragment_state()), + fragment: Some(fragment), primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, strip_index_format: None, @@ -68,3 +85,43 @@ where &self.pipeline } } + +/// Cache for different pipelines with the same shader program. +/// +/// Necessary as certain configurations cannot be changed dynamically in WGPU. +pub struct PipelineGroup +where + V: VertexShader, + F: FragmentShader, +{ + vert: V, + frag: F, + cache: HashMap<(F::BlendStates, Option), Pipeline>, +} + +impl PipelineGroup +where + V: VertexShader, + F: FragmentShader, +{ + pub fn new(vert: V, frag: F) -> Self { + Self { + vert, + frag, + cache: HashMap::new(), + } + } + + pub fn with_configuration( + &mut self, + device: &wgpu::Device, + blend: F::BlendStates, + depth_stencil: Option, + ) -> &Pipeline { + self.cache + .entry((blend, depth_stencil)) + .or_insert_with_key(|(blend, depth_stencil)| { + Pipeline::new(device, &self.vert, &self.frag, blend.clone(), depth_stencil.clone()) + }) + } +} diff --git a/inox2d-wgpu/src/shader.rs b/inox2d-wgpu/src/shader.rs index 49fd34a..378a1f5 100644 --- a/inox2d-wgpu/src/shader.rs +++ b/inox2d-wgpu/src/shader.rs @@ -1,7 +1,9 @@ use wgpu; use wgpu::util::DeviceExt; -pub trait Shader { +use std::hash::Hash; + +pub trait Shader: Clone { fn bindgroup_layout(&self) -> &wgpu::BindGroupLayout; } diff --git a/inox2d-wgpu/src/texture.rs b/inox2d-wgpu/src/texture.rs index 7c7d9c0..196f1e5 100644 --- a/inox2d-wgpu/src/texture.rs +++ b/inox2d-wgpu/src/texture.rs @@ -3,6 +3,7 @@ use wgpu; use inox2d::model::Model; use inox2d::texture::ShallowTexture; +#[derive(Clone)] pub struct DeviceTexture { device_texture: wgpu::Texture, view: wgpu::TextureView, From ac72d72f6f83d242c2925db5ab2bfe2bbf329f73 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Sun, 22 Feb 2026 21:43:25 +0000 Subject: [PATCH 33/49] `self.is_in_mask` should be flagged with `InoxRenderer.on_begin_masked_content` --- inox2d-wgpu/src/lib.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/inox2d-wgpu/src/lib.rs b/inox2d-wgpu/src/lib.rs index ec964e9..e66771a 100644 --- a/inox2d-wgpu/src/lib.rs +++ b/inox2d-wgpu/src/lib.rs @@ -356,16 +356,14 @@ impl<'window> InoxRenderer for WgpuRenderer<'window> { //TODO: Erase the stencil buffer. //TODO: Enable stencilling on the render target. - - self.is_in_mask = true; } fn on_begin_mask(&mut self, mask: &components::Mask) { self.stencil_reference_value = (mask.mode == components::MaskMode::Mask) as u32; } - fn on_begin_masked_content(&self) { - unimplemented!() + fn on_begin_masked_content(&mut self) { + self.is_in_mask = true; } fn on_end_mask(&mut self) { From 2a9456dea6e714af9cbc99fa716dffcc2f6708ec Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Sun, 22 Feb 2026 21:46:01 +0000 Subject: [PATCH 34/49] Add option to configure write mask as our mask shaders are supposed to execute with color writes disabled. --- inox2d-wgpu/build.rs | 2 +- inox2d-wgpu/src/lib.rs | 5 ++++- inox2d-wgpu/src/pipeline.rs | 34 +++++++++++++++++++++++++++------- inox2d-wgpu/src/shader.rs | 7 ++----- 4 files changed, 34 insertions(+), 14 deletions(-) diff --git a/inox2d-wgpu/build.rs b/inox2d-wgpu/build.rs index 4952cbf..3463a7f 100644 --- a/inox2d-wgpu/build.rs +++ b/inox2d-wgpu/build.rs @@ -441,7 +441,7 @@ fn gen_fragmentshader_trait_methods( writeln!(out, "impl shader::FragmentShader for {} {{", struct_name)?; writeln!( out, - " type BlendStates = [Option; {}];", + " type TargetArray = [T; {}];", entrypoint.output_variables.len() )?; writeln!(out)?; diff --git a/inox2d-wgpu/src/lib.rs b/inox2d-wgpu/src/lib.rs index e66771a..2619276 100644 --- a/inox2d-wgpu/src/lib.rs +++ b/inox2d-wgpu/src/lib.rs @@ -423,6 +423,7 @@ impl<'window> InoxRenderer for WgpuRenderer<'window> { let pipeline = self.part_mask_pipeline.with_configuration( &self.device, [blend], + [wgpu::ColorWrites::empty()], Some(self.mask_depthstencil.clone()), ); let uni_in_frag = basic_mask_frag::Input { @@ -447,16 +448,18 @@ impl<'window> InoxRenderer for WgpuRenderer<'window> { render_pass.set_stencil_reference(self.stencil_reference_value); } else { + let all = wgpu::ColorWrites::ALL; //Regular parts let pipeline = if self.is_in_mask { self.part_pipeline.with_configuration( &self.device, [blend, blend, blend], + [all, all, all], Some(self.masked_depthstencil.clone()), ) } else { self.part_pipeline - .with_configuration(&self.device, [blend, blend, blend], None) + .with_configuration(&self.device, [blend, blend, blend], [all, all, all], None) }; let uni_in_frag = basic_frag::Input { diff --git a/inox2d-wgpu/src/pipeline.rs b/inox2d-wgpu/src/pipeline.rs index ae228bb..53a455e 100644 --- a/inox2d-wgpu/src/pipeline.rs +++ b/inox2d-wgpu/src/pipeline.rs @@ -23,7 +23,8 @@ where device: &wgpu::Device, vert: &V, frag: &F, - blend: F::BlendStates, + blend: F::TargetArray>, + write_mask: F::TargetArray, depth_stencil: Option, ) -> Self { let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { @@ -36,11 +37,15 @@ where let mut fragment = frag.as_fragment_state(); let mut fragment_targets = fragment.targets.to_owned(); - for (index, blend) in blend.into_iter().enumerate() { + for (index, (blend, write_mask)) in blend.into_iter().zip(write_mask.into_iter()).enumerate() { fragment_targets[index] .as_mut() .expect("FragmentShader should require as many blend states as it creates targets") .blend = blend; + fragment_targets[index] + .as_mut() + .expect("FragmentShader should require as many blend states as it creates targets") + .write_mask = write_mask; } fragment.targets = &fragment_targets; @@ -96,7 +101,14 @@ where { vert: V, frag: F, - cache: HashMap<(F::BlendStates, Option), Pipeline>, + cache: HashMap< + ( + F::TargetArray>, + F::TargetArray, + Option, + ), + Pipeline, + >, } impl PipelineGroup @@ -115,13 +127,21 @@ where pub fn with_configuration( &mut self, device: &wgpu::Device, - blend: F::BlendStates, + blend: F::TargetArray>, + write_mask: F::TargetArray, depth_stencil: Option, ) -> &Pipeline { self.cache - .entry((blend, depth_stencil)) - .or_insert_with_key(|(blend, depth_stencil)| { - Pipeline::new(device, &self.vert, &self.frag, blend.clone(), depth_stencil.clone()) + .entry((blend, write_mask, depth_stencil)) + .or_insert_with_key(|(blend, write_mask, depth_stencil)| { + Pipeline::new( + device, + &self.vert, + &self.frag, + blend.clone(), + write_mask.clone(), + depth_stencil.clone(), + ) }) } } diff --git a/inox2d-wgpu/src/shader.rs b/inox2d-wgpu/src/shader.rs index 378a1f5..8f9d5a5 100644 --- a/inox2d-wgpu/src/shader.rs +++ b/inox2d-wgpu/src/shader.rs @@ -11,11 +11,8 @@ pub trait VertexShader: Shader { fn as_vertex_state(&self) -> wgpu::VertexState; } -pub trait FragmentShader: Shader -where - Self::BlendStates: IntoIterator> + Eq + Hash + Clone, -{ - type BlendStates; +pub trait FragmentShader: Shader { + type TargetArray: IntoIterator + Eq + Hash + Clone; fn as_fragment_state(&self) -> wgpu::FragmentState; } From bee8ce1f50cb49d0138abcb95e217790943d4241 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Sun, 22 Feb 2026 22:23:03 +0000 Subject: [PATCH 35/49] Use the actual texture clear command --- inox2d-wgpu/src/lib.rs | 8 ++- inox2d-wgpu/src/texture.rs | 102 ++++++++++++++----------------------- 2 files changed, 45 insertions(+), 65 deletions(-) diff --git a/inox2d-wgpu/src/lib.rs b/inox2d-wgpu/src/lib.rs index 2619276..a706f58 100644 --- a/inox2d-wgpu/src/lib.rs +++ b/inox2d-wgpu/src/lib.rs @@ -266,17 +266,23 @@ impl<'window> WgpuRenderer<'window> { pub fn resize(&mut self, width: u32, height: u32) { if width > 0 && height > 0 { + let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("Inox2D texture resizes"), + }); + self.config.width = width; self.config.height = height; self.surface.configure(&self.device, &self.config); self.gbuffer = Some(GBuffer::new( &self.device, - &self.queue, + &mut encoder, width, height, wgpu::TextureFormat::Rgba32Float, wgpu::TextureFormat::Depth24PlusStencil8, )); + + self.queue.submit(std::iter::once(encoder.finish())); } } diff --git a/inox2d-wgpu/src/texture.rs b/inox2d-wgpu/src/texture.rs index 196f1e5..dc28849 100644 --- a/inox2d-wgpu/src/texture.rs +++ b/inox2d-wgpu/src/texture.rs @@ -63,7 +63,7 @@ impl DeviceTexture { pub fn empty_render_target( device: &wgpu::Device, - queue: &wgpu::Queue, + encoder: &mut wgpu::CommandEncoder, width: u32, height: u32, format: wgpu::TextureFormat, @@ -89,40 +89,22 @@ impl DeviceTexture { let view = device_texture.create_view(&wgpu::TextureViewDescriptor::default()); let empty = Self { device_texture, view }; - empty.clear(device, queue); + empty.clear(encoder); empty } // Clear the texture. - pub fn clear(&self, device: &wgpu::Device, queue: &wgpu::Queue) { - let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("Clear command encoder"), - }); - - let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("Clear RenderPass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: self.view(), - resolve_target: None, - depth_slice: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color { - r: 0.0, - g: 0.0, - b: 0.0, - a: 0.0, - }), - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: None, - occlusion_query_set: None, - timestamp_writes: None, - multiview_mask: None, - }); - - drop(render_pass); - queue.submit(std::iter::once(encoder.finish())); + pub fn clear(&self, encoder: &mut wgpu::CommandEncoder) { + encoder.clear_texture( + self.texture(), + &wgpu::ImageSubresourceRange { + aspect: wgpu::TextureAspect::All, + base_mip_level: 0, + mip_level_count: None, + base_array_layer: 0, + array_layer_count: None, + }, + ); } pub fn texture(&self) -> &wgpu::Texture { @@ -155,7 +137,7 @@ pub struct DepthStencilBuffer { impl DepthStencilBuffer { pub fn empty_render_target( device: &wgpu::Device, - queue: &wgpu::Queue, + encoder: &mut wgpu::CommandEncoder, width: u32, height: u32, format: wgpu::TextureFormat, @@ -185,37 +167,22 @@ impl DepthStencilBuffer { format, }; - empty.clear(device, queue); + empty.clear(encoder); empty } // Clear the texture. - pub fn clear(&self, device: &wgpu::Device, queue: &wgpu::Queue) { - let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("Clear command encoder"), - }); - - let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("Clear RenderPass"), - color_attachments: &[], - depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { - view: &self.view, - depth_ops: Some(wgpu::Operations { - load: wgpu::LoadOp::Clear(0.0), - store: wgpu::StoreOp::Store, - }), - stencil_ops: Some(wgpu::Operations { - load: wgpu::LoadOp::Clear(0), - store: wgpu::StoreOp::Store, - }), - }), - occlusion_query_set: None, - timestamp_writes: None, - multiview_mask: None, - }); - - drop(render_pass); - queue.submit(std::iter::once(encoder.finish())); + pub fn clear(&self, encoder: &mut wgpu::CommandEncoder) { + encoder.clear_texture( + self.texture(), + &wgpu::ImageSubresourceRange { + aspect: wgpu::TextureAspect::All, + base_mip_level: 0, + mip_level_count: None, + base_array_layer: 0, + array_layer_count: None, + }, + ); } pub fn texture(&self) -> &wgpu::Texture { @@ -275,17 +242,17 @@ pub struct GBuffer { impl GBuffer { pub fn new( device: &wgpu::Device, - queue: &wgpu::Queue, + encoder: &mut wgpu::CommandEncoder, width: u32, height: u32, format: wgpu::TextureFormat, depth_format: wgpu::TextureFormat, ) -> Self { Self { - albedo: DeviceTexture::empty_render_target(device, queue, width, height, wgpu::TextureFormat::Rgba8Uint), - emissive: DeviceTexture::empty_render_target(device, queue, width, height, format), - bump: DeviceTexture::empty_render_target(device, queue, width, height, wgpu::TextureFormat::Rgba8Uint), - stencil: DepthStencilBuffer::empty_render_target(device, queue, width, height, depth_format), + albedo: DeviceTexture::empty_render_target(device, encoder, width, height, wgpu::TextureFormat::Rgba8Uint), + emissive: DeviceTexture::empty_render_target(device, encoder, width, height, format), + bump: DeviceTexture::empty_render_target(device, encoder, width, height, wgpu::TextureFormat::Rgba8Uint), + stencil: DepthStencilBuffer::empty_render_target(device, encoder, width, height, depth_format), } } @@ -305,6 +272,13 @@ impl GBuffer { &self.stencil } + pub fn clear(&self, encoder: &mut wgpu::CommandEncoder) { + self.albedo().clear(encoder); + self.emissive().clear(encoder); + self.bump().clear(encoder); + self.stencil().clear(encoder); + } + pub fn as_color_attachments(&self) -> [Option>; 3] { [ Some(self.albedo.as_color_attachment()), From 94ef214962050004a955235aaa942aaa2ea5039f Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Sun, 22 Feb 2026 23:11:03 +0000 Subject: [PATCH 36/49] Pedantry: these are textures, not buffers. OpenGL muddied the naming a bit but WGPU / vulkan etc are pretty clear --- inox2d-wgpu/src/texture.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/inox2d-wgpu/src/texture.rs b/inox2d-wgpu/src/texture.rs index dc28849..c326ff5 100644 --- a/inox2d-wgpu/src/texture.rs +++ b/inox2d-wgpu/src/texture.rs @@ -128,13 +128,13 @@ impl DeviceTexture { } } -pub struct DepthStencilBuffer { +pub struct DepthStencilTexture { device_texture: wgpu::Texture, view: wgpu::TextureView, format: wgpu::TextureFormat, } -impl DepthStencilBuffer { +impl DepthStencilTexture { pub fn empty_render_target( device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder, @@ -236,7 +236,7 @@ pub struct GBuffer { albedo: DeviceTexture, emissive: DeviceTexture, bump: DeviceTexture, - stencil: DepthStencilBuffer, + stencil: DepthStencilTexture, } impl GBuffer { @@ -252,7 +252,7 @@ impl GBuffer { albedo: DeviceTexture::empty_render_target(device, encoder, width, height, wgpu::TextureFormat::Rgba8Uint), emissive: DeviceTexture::empty_render_target(device, encoder, width, height, format), bump: DeviceTexture::empty_render_target(device, encoder, width, height, wgpu::TextureFormat::Rgba8Uint), - stencil: DepthStencilBuffer::empty_render_target(device, encoder, width, height, depth_format), + stencil: DepthStencilTexture::empty_render_target(device, encoder, width, height, depth_format), } } @@ -268,7 +268,7 @@ impl GBuffer { &self.bump } - pub fn stencil(&self) -> &DepthStencilBuffer { + pub fn stencil(&self) -> &DepthStencilTexture { &self.stencil } From 7b309b00fb56ab1b5cd2971f8b70732db38b8280 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Sun, 22 Feb 2026 23:13:08 +0000 Subject: [PATCH 37/49] Add support for rendering to the surface texture directly. The GBuffer type is only intended to be rendered to for Composite nodes. I'm still not sure how emissive/bump works outside of Composite nodes - the OpenGL renderer renders straight to the window surface without binding any other textures. I may have to experiment... --- inox2d-wgpu/src/lib.rs | 127 +++++++++++++++++++++++++++++++++-------- inox2d/src/render.rs | 1 + 2 files changed, 104 insertions(+), 24 deletions(-) diff --git a/inox2d-wgpu/src/lib.rs b/inox2d-wgpu/src/lib.rs index a706f58..3b2cb3d 100644 --- a/inox2d-wgpu/src/lib.rs +++ b/inox2d-wgpu/src/lib.rs @@ -3,6 +3,7 @@ use inox2d::model::Model; use inox2d::node::{InoxNodeUuid, components, drawables}; //hey wait a second that's just a u32 newtype! UUIDs are four of those! use inox2d::render::{self, InoxRenderer}; use inox2d::texture::decode_model_textures; +use std::error::Error; use wgpu; use wgpu::util::{BufferInitDescriptor, DeviceExt}; @@ -11,7 +12,7 @@ mod shader; mod shaders; mod texture; -use crate::texture::{DeviceTexture, GBuffer}; +use crate::texture::{DepthStencilTexture, DeviceTexture, GBuffer}; use shader::UniformBlock; use shaders::basic::{basic_frag, basic_mask_frag, basic_vert, composite_frag, composite_mask_frag, composite_vert}; @@ -43,15 +44,25 @@ pub enum WgpuRendererError { CreateSurfaceError(#[from] wgpu::CreateSurfaceError), RequestAdapterError(#[from] wgpu::RequestAdapterError), RequestDeviceError(#[from] wgpu::RequestDeviceError), + SurfaceError(#[from] wgpu::SurfaceError), #[error("Model rendering not initialized")] ModelRenderingNotInitialized, + + #[error("Size cannot be zero")] + SizeCannotBeZero, } pub struct WgpuRenderer<'window> { surface: wgpu::Surface<'window>, config: wgpu::SurfaceConfiguration, - gbuffer: Option, + + /// All textures used as render targets, excluding the surface color + /// buffer. + /// + /// GBuffer is used solely for composite rendering, where rendered pixels + /// are used for a deferred shading pass. + render_targets: Option<(GBuffer, DepthStencilTexture)>, verts: wgpu::Buffer, uvs: wgpu::Buffer, @@ -76,12 +87,14 @@ pub struct WgpuRenderer<'window> { composite_mask_pipeline: pipeline::PipelineGroup, encoder: Option, + surface_texture: Option<(wgpu::SurfaceTexture, wgpu::TextureView)>, model_textures: Vec, model_sampler: wgpu::Sampler, last_mask_threshold: f32, is_in_mask: bool, + is_in_composite: bool, stencil_reference_value: u32, device: wgpu::Device, @@ -236,7 +249,7 @@ impl<'window> WgpuRenderer<'window> { Ok(WgpuRenderer { surface, config, - gbuffer: None, + render_targets: None, verts, uvs, deforms, @@ -254,17 +267,19 @@ impl<'window> WgpuRenderer<'window> { composite_pipeline, composite_mask_pipeline, encoder: None, + surface_texture: None, model_textures: texture_handles, model_sampler, last_mask_threshold: 0.0, is_in_mask: false, + is_in_composite: false, stencil_reference_value: 1, device, queue, }) } - pub fn resize(&mut self, width: u32, height: u32) { + pub fn resize(&mut self, width: u32, height: u32) -> Result<(), WgpuRendererError> { if width > 0 && height > 0 { let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("Inox2D texture resizes"), @@ -273,16 +288,28 @@ impl<'window> WgpuRenderer<'window> { self.config.width = width; self.config.height = height; self.surface.configure(&self.device, &self.config); - self.gbuffer = Some(GBuffer::new( - &self.device, - &mut encoder, - width, - height, - wgpu::TextureFormat::Rgba32Float, - wgpu::TextureFormat::Depth24PlusStencil8, + self.render_targets = Some(( + GBuffer::new( + &self.device, + &mut encoder, + width, + height, + wgpu::TextureFormat::Rgba32Float, + wgpu::TextureFormat::Depth24PlusStencil8, + ), + DepthStencilTexture::empty_render_target( + &self.device, + &mut encoder, + width, + height, + wgpu::TextureFormat::Depth24PlusStencil8, + ), )); self.queue.submit(std::iter::once(encoder.finish())); + Ok(()) + } else { + Err(WgpuRendererError::SizeCannotBeZero) } } @@ -341,27 +368,41 @@ impl<'window> WgpuRenderer<'window> { } impl<'window> InoxRenderer for WgpuRenderer<'window> { - fn begin_render(&mut self) { + fn begin_render(&mut self) -> Result<(), Box> { if self.encoder.is_some() { panic!("Recursive rendering is not permitted."); } - if self.gbuffer.is_none() { + if self.render_targets.is_none() { panic!("Buffer is not yet set up."); } self.encoder = Some(self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("Inox2DWGPU"), })); + let surface_texture = self.surface.get_current_texture()?; + let texview = surface_texture + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + self.surface_texture = Some((surface_texture, texview)); //TODO: read & translate OpenGLRenderer's `on_begin_draw` / `on_end_draw` + + Ok(()) } fn on_begin_masks(&mut self, masks: &components::Masks) { self.last_mask_threshold = masks.threshold.clamp(0.0, 1.0); - - //TODO: Erase the stencil buffer. //TODO: Enable stencilling on the render target. + + if let Some((composite, surface_stencil)) = self.render_targets.as_ref() { + let mut encoder = self.encoder.take().expect("encoder should not be held across calls"); + + composite.stencil().clear(&mut encoder); + surface_stencil.clear(&mut encoder); + + self.encoder = Some(encoder); + } } fn on_begin_mask(&mut self, mask: &components::Mask) { @@ -378,16 +419,49 @@ impl<'window> InoxRenderer for WgpuRenderer<'window> { fn draw_textured_mesh_content( &mut self, - as_mask: bool, + render_mask: bool, components: &drawables::TexturedMeshComponents, render_ctx: &render::TexturedMeshRenderCtx, _id: InoxNodeUuid, ) { - if let Some(gbuffer) = self.gbuffer.as_ref() { - let depth_stencil_attachment = if as_mask { - Some(gbuffer.stencil().as_depth_stencil_attachment_rw()) + if let Some((composite, surface_stencil)) = self.render_targets.as_ref() { + let gbuffer_color = composite.as_color_attachments(); + let surface_color_view = &self.surface_texture.as_ref().expect("surface").1; + let surface_color_attach = Some(wgpu::RenderPassColorAttachment { + view: &surface_color_view, + depth_slice: None, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + }); + let masked_attach = [surface_color_attach.clone()]; + let unmasked_attach = [surface_color_attach, None, None]; + + let color_attachments = if self.is_in_composite { + if render_mask { + &[gbuffer_color[0].clone()] + } else { + gbuffer_color.as_slice() + } + } else { + if render_mask { + masked_attach.as_slice() + } else { + unmasked_attach.as_slice() + } + }; + let stencil_texture = if self.is_in_composite { + composite.stencil() + } else { + surface_stencil + }; + + let depth_stencil_attachment = if render_mask { + Some(stencil_texture.as_depth_stencil_attachment_rw()) } else if self.is_in_mask { - Some(gbuffer.stencil().as_depth_stencil_attachment_ro()) + Some(stencil_texture.as_depth_stencil_attachment_ro()) } else { None }; @@ -399,7 +473,7 @@ impl<'window> InoxRenderer for WgpuRenderer<'window> { let mut encoder = self.encoder.take().expect("encoder"); let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("WgpuRenderer::draw_textured_mesh_content"), - color_attachments: &gbuffer.as_color_attachments(), + color_attachments, depth_stencil_attachment, occlusion_query_set: None, timestamp_writes: None, @@ -424,7 +498,7 @@ impl<'window> InoxRenderer for WgpuRenderer<'window> { render_pass.set_vertex_buffer(basic_vert::INPUT_LOCATION_DEFORM, self.deforms.slice(..)); render_pass.set_index_buffer(self.indices.slice(..), wgpu::IndexFormat::Uint16); - if as_mask { + if render_mask { //TODO: What happens if a mask is also masked? let pipeline = self.part_mask_pipeline.with_configuration( &self.device, @@ -505,21 +579,26 @@ impl<'window> InoxRenderer for WgpuRenderer<'window> { } fn begin_composite_content( - &self, + &mut self, as_mask: bool, components: &drawables::CompositeComponents, render_ctx: &render::CompositeRenderCtx, id: InoxNodeUuid, ) { + self.is_in_composite = true; + //TODO: Clear gbuffer } fn finish_composite_content( - &self, + &mut self, as_mask: bool, components: &drawables::CompositeComponents, render_ctx: &render::CompositeRenderCtx, id: InoxNodeUuid, ) { + assert!(self.is_in_composite); + self.is_in_composite = false; + //TODO: Run deferred composite pass } fn end_render_and_flush(&mut self) { diff --git a/inox2d/src/render.rs b/inox2d/src/render.rs index 6b937cc..d2cc90c 100644 --- a/inox2d/src/render.rs +++ b/inox2d/src/render.rs @@ -4,6 +4,7 @@ mod vertex_buffers; use std::collections::HashSet; use std::error::Error; use std::mem::swap; +use std::error::Error; use crate::node::{ components::{DeformStack, Mask, Masks, ZSort}, From 92fda8b503b1e7ae1dcfb1ed5d75ff2decb6d437 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Sun, 22 Feb 2026 23:52:30 +0000 Subject: [PATCH 38/49] Implement composite begin/end --- inox2d-wgpu/src/lib.rs | 98 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 95 insertions(+), 3 deletions(-) diff --git a/inox2d-wgpu/src/lib.rs b/inox2d-wgpu/src/lib.rs index 3b2cb3d..b6c6a2d 100644 --- a/inox2d-wgpu/src/lib.rs +++ b/inox2d-wgpu/src/lib.rs @@ -586,19 +586,111 @@ impl<'window> InoxRenderer for WgpuRenderer<'window> { id: InoxNodeUuid, ) { self.is_in_composite = true; - //TODO: Clear gbuffer + + let mut encoder = self.encoder.take().expect("encoder"); + + if let Some((composite, _surface_stencil)) = self.render_targets.as_ref() { + composite.clear(&mut encoder); + } + + self.encoder = Some(encoder); } fn finish_composite_content( &mut self, - as_mask: bool, + render_mask: bool, components: &drawables::CompositeComponents, render_ctx: &render::CompositeRenderCtx, id: InoxNodeUuid, ) { assert!(self.is_in_composite); self.is_in_composite = false; - //TODO: Run deferred composite pass + + let mut encoder = self.encoder.take().expect("encoder"); + + if let Some((composite, surface_stencil)) = self.render_targets.as_ref() { + let surface_color_view = &self.surface_texture.as_ref().expect("surface").1; + let depth_stencil_attachment = if render_mask { + Some(surface_stencil.as_depth_stencil_attachment_rw()) + } else if self.is_in_mask { + Some(surface_stencil.as_depth_stencil_attachment_ro()) + } else { + None + }; + + //TODO: Do we even want blending on in Normal mode? + let blend = Some(Self::blend_mode_to_state(components.drawable.blending.mode)); + + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("WgpuRenderer Composite deferred pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &surface_color_view, + depth_slice: None, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment, + occlusion_query_set: None, + timestamp_writes: None, + multiview_mask: None, + }); + + if render_mask { + // LOL, the OpenGL renderer didn't handle the "mask by composite" case. + // I may want to see what Inochi2D's D library does. + todo!(); + } else { + let all = wgpu::ColorWrites::ALL; + let depth_stencil = if self.is_in_mask { + Some(self.masked_depthstencil.clone()) + } else { + None + }; + let pipeline = self.composite_pipeline.with_configuration( + &self.device, + [blend, blend, blend], + [all, all, all], + depth_stencil, + ); + + let uni_in_frag = composite_frag::Input { + opacity: components.drawable.blending.opacity.clamp(0.0, 1.0), + multColor: components + .drawable + .blending + .tint + .clamp(glam::Vec3::ZERO, glam::Vec3::ONE) + .into(), + screenColor: components + .drawable + .blending + .screen_tint + .clamp(glam::Vec3::ZERO, glam::Vec3::ONE) + .into(), + } + .into_buffer(&self.device); + + render_pass.set_pipeline(pipeline.pipeline()); + pipeline.bind_frag( + &mut render_pass, + Some(&self.composite_shader_frag.bind( + &self.device, + composite.albedo().view(), + composite.emissive().view(), + composite.bump().view(), + &self.model_sampler, + &uni_in_frag, + )), + ); + pipeline.bind_vertex(&mut render_pass, Some(&self.composite_shader_vert.bind(&self.device))); + render_pass.draw_indexed(0..6, 0, 0..1); //TODO: Where do these vertices come from!?!? + } + } + + self.encoder = Some(encoder); } fn end_render_and_flush(&mut self) { From 4dcbc580a90e09eca39a8657fcbf898d335bc48e Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Mon, 23 Feb 2026 01:11:44 +0000 Subject: [PATCH 39/49] Make `on_begin_draw`/`on_end_draw` official InoxRenderer trait methods. --- inox2d-opengl/src/lib.rs | 1 + inox2d-wgpu/src/lib.rs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/inox2d-opengl/src/lib.rs b/inox2d-opengl/src/lib.rs index 484d00c..eb038d5 100644 --- a/inox2d-opengl/src/lib.rs +++ b/inox2d-opengl/src/lib.rs @@ -7,6 +7,7 @@ use std::cell::RefCell; use std::error::Error; use std::mem; use std::ops::Deref; +use std::error::Error; use glam::{uvec2, UVec2, Vec3}; use glow::HasContext; diff --git a/inox2d-wgpu/src/lib.rs b/inox2d-wgpu/src/lib.rs index b6c6a2d..dfc3ff5 100644 --- a/inox2d-wgpu/src/lib.rs +++ b/inox2d-wgpu/src/lib.rs @@ -368,7 +368,7 @@ impl<'window> WgpuRenderer<'window> { } impl<'window> InoxRenderer for WgpuRenderer<'window> { - fn begin_render(&mut self) -> Result<(), Box> { + fn on_begin_draw(&mut self, puppet: &inox2d::puppet::Puppet) -> Result<(), Box> { if self.encoder.is_some() { panic!("Recursive rendering is not permitted."); } @@ -693,7 +693,7 @@ impl<'window> InoxRenderer for WgpuRenderer<'window> { self.encoder = Some(encoder); } - fn end_render_and_flush(&mut self) { + fn on_end_draw(&mut self, puppet: &inox2d::puppet::Puppet) { let end = self.encoder.take().expect("encoder").finish(); self.queue.submit(std::iter::once(end)); } From d5a9b9c81e0f1850df35a406451b6f03795c5b0a Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Mon, 23 Feb 2026 03:31:52 +0000 Subject: [PATCH 40/49] We should present at the end of drawing --- inox2d-wgpu/src/lib.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/inox2d-wgpu/src/lib.rs b/inox2d-wgpu/src/lib.rs index dfc3ff5..ce1e6ac 100644 --- a/inox2d-wgpu/src/lib.rs +++ b/inox2d-wgpu/src/lib.rs @@ -696,5 +696,9 @@ impl<'window> InoxRenderer for WgpuRenderer<'window> { fn on_end_draw(&mut self, puppet: &inox2d::puppet::Puppet) { let end = self.encoder.take().expect("encoder").finish(); self.queue.submit(std::iter::once(end)); + + if let Some((surface_texture, _)) = self.surface_texture.take() { + surface_texture.present(); + } } } From afa15d649e30952e4ce47b2ae3ff4b43152aad50 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Mon, 23 Feb 2026 03:35:15 +0000 Subject: [PATCH 41/49] Add an example program that uses the WGPU renderer --- examples/render-wgpu/Cargo.toml | 14 ++++++ examples/render-wgpu/src/main.rs | 80 ++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 examples/render-wgpu/Cargo.toml create mode 100644 examples/render-wgpu/src/main.rs diff --git a/examples/render-wgpu/Cargo.toml b/examples/render-wgpu/Cargo.toml new file mode 100644 index 0000000..12f2311 --- /dev/null +++ b/examples/render-wgpu/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "render-wgpu" +version = "0.1.0" +edition = "2024" + +[dependencies] +common = { path = "../common" } +inox2d = { path = "../../inox2d" } +inox2d-wgpu = { path = "../../inox2d-wgpu"} +winit = "0.29" +env_logger = "0.11.9" +log = "0.4.29" +clap = { version = "4.1.8", features = ["derive"] } +pollster = "0.4.0" \ No newline at end of file diff --git a/examples/render-wgpu/src/main.rs b/examples/render-wgpu/src/main.rs new file mode 100644 index 0000000..1ddb662 --- /dev/null +++ b/examples/render-wgpu/src/main.rs @@ -0,0 +1,80 @@ +use env_logger; +use log::*; +use winit::event_loop::{EventLoop, ControlFlow}; +use winit::window::WindowBuilder; +use winit::event::{Event, WindowEvent}; +use clap::Parser; +use inox2d::formats::inp::parse_inp; +use inox2d::math::camera::Camera; +use inox2d::render::InoxRendererExt; +use inox2d_wgpu::WgpuRenderer; +use pollster::block_on; +use common::scene::ExampleSceneController; + +use std::error::Error; +use std::path::PathBuf; +use std::fs; +use std::sync::Arc; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Cli { + #[arg(help = "Path to the .inp or .inx file.")] + inp_path: PathBuf, +} + +fn main() -> Result<(), Box> { + let cli = Cli::parse(); + + env_logger::init(); + + let event_loop = EventLoop::new()?; + + info!("Loading {:?}", cli.inp_path); + + let data = fs::read(cli.inp_path)?; + let mut model = parse_inp(data.as_slice())?; + + info!("Successfully parsed puppet: {}", + (model.puppet.meta.name.as_deref()).unwrap_or("")); + + model.puppet.init_transforms(); + model.puppet.init_rendering(); + model.puppet.init_params(); + model.puppet.init_physics(); + + let window = Arc::new(WindowBuilder::new().build(&event_loop).expect("valid window")); + let mut renderer = block_on(WgpuRenderer::new(window.clone(), &model)).expect("valid renderer"); + let mut scene_controller = ExampleSceneController::new(&Camera::default(), 0.5); + let camera = Camera::default(); + + event_loop.set_control_flow(ControlFlow::Poll); + event_loop.run(|event, event_loop| { + match event { + Event::WindowEvent { event: WindowEvent::Resized(new_size), .. } => { + if let Err(err) = renderer.resize(new_size.width, new_size.height) { + error!("Resize failed: {}", err); + } + } + Event::WindowEvent { event: WindowEvent::CloseRequested, .. } => { + event_loop.exit(); + } + Event::WindowEvent { event: WindowEvent::RedrawRequested, .. } => { + if let Err(err) = renderer.draw(&model.puppet) { + error!("Draw failed: {}", err); + } + } + Event::WindowEvent { event, .. } => { + scene_controller.interact(&event, &camera); + }, + Event::AboutToWait => { + window.request_redraw(); + + //TODO: Swapchain? Swapchain. + } + _ => {} + } + })?; + + Ok(()) +} \ No newline at end of file From b55b6e9b6e4af47c01e97ad7047727cb79213c6c Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Tue, 24 Feb 2026 23:23:05 +0000 Subject: [PATCH 42/49] Break InoxRenderer into a separate renderer and draw-session trait. This ensures that: * The renderer does not have to hold empty space for temporaries that are only used during the draw process * `on_begin_draw` is always called before any drawing operations can occur * Drawing is always cleaned up, either in `on_end_draw` or `Drop` (our impls currently use `on_end_draw`) --- inox2d-opengl/src/lib.rs | 1 - inox2d-wgpu/src/lib.rs | 224 +++++++++++++++++++-------------------- inox2d/src/render.rs | 1 - 3 files changed, 111 insertions(+), 115 deletions(-) diff --git a/inox2d-opengl/src/lib.rs b/inox2d-opengl/src/lib.rs index eb038d5..484d00c 100644 --- a/inox2d-opengl/src/lib.rs +++ b/inox2d-opengl/src/lib.rs @@ -7,7 +7,6 @@ use std::cell::RefCell; use std::error::Error; use std::mem; use std::ops::Deref; -use std::error::Error; use glam::{uvec2, UVec2, Vec3}; use glow::HasContext; diff --git a/inox2d-wgpu/src/lib.rs b/inox2d-wgpu/src/lib.rs index ce1e6ac..844ecd5 100644 --- a/inox2d-wgpu/src/lib.rs +++ b/inox2d-wgpu/src/lib.rs @@ -1,7 +1,7 @@ use glam::Mat4; use inox2d::model::Model; use inox2d::node::{InoxNodeUuid, components, drawables}; //hey wait a second that's just a u32 newtype! UUIDs are four of those! -use inox2d::render::{self, InoxRenderer}; +use inox2d::render::{self, DrawSession, InoxRenderer}; use inox2d::texture::decode_model_textures; use std::error::Error; use wgpu; @@ -34,7 +34,7 @@ pub fn cast_vec2(array: &[glam::Vec2]) -> &[u8] { /// `upload_array_to_gl`. /// /// NOTE: This probably can already be bytemucked -pub fn cast_index(array: &[u16]) -> &[u8] { +pub fn cast_index(array: &[u32]) -> &[u8] { unsafe { std::slice::from_raw_parts(array.as_ptr() as *const u8, std::mem::size_of_val(array)) } } @@ -86,9 +86,6 @@ pub struct WgpuRenderer<'window> { composite_pipeline: pipeline::PipelineGroup, composite_mask_pipeline: pipeline::PipelineGroup, - encoder: Option, - surface_texture: Option<(wgpu::SurfaceTexture, wgpu::TextureView)>, - model_textures: Vec, model_sampler: wgpu::Sampler, @@ -266,8 +263,6 @@ impl<'window> WgpuRenderer<'window> { composite_shader_mask_frag, composite_pipeline, composite_mask_pipeline, - encoder: None, - surface_texture: None, model_textures: texture_handles, model_sampler, last_mask_threshold: 0.0, @@ -320,7 +315,46 @@ impl<'window> WgpuRenderer<'window> { &self.model_textures[part.tex_emissive.raw()], ) } +} + +impl<'window> InoxRenderer for WgpuRenderer<'window> { + type Draw<'a> + = WgpuDrawSession<'a, 'window> + where + Self: 'a; + + fn on_begin_draw<'a>(&'a mut self, puppet: &inox2d::puppet::Puppet) -> Result, Box> { + if self.render_targets.is_none() { + panic!("Buffer is not yet set up."); + } + + let encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("Inox2DWGPU"), + }); + let surface_texture = self.surface.get_current_texture()?; + let view = surface_texture + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + + //TODO: read & translate OpenGLRenderer's `on_begin_draw` / `on_end_draw` + + Ok(WgpuDrawSession { + render: self, + encoder, + surface_texture, + view, + }) + } +} + +pub struct WgpuDrawSession<'a, 'window> { + render: &'a mut WgpuRenderer<'window>, + encoder: wgpu::CommandEncoder, + surface_texture: wgpu::SurfaceTexture, + view: wgpu::TextureView, +} +impl<'a, 'window> WgpuDrawSession<'a, 'window> { fn blend_mode_to_state(state: components::BlendMode) -> wgpu::BlendState { let component = match state { components::BlendMode::Normal => wgpu::BlendComponent { @@ -367,54 +401,27 @@ impl<'window> WgpuRenderer<'window> { } } -impl<'window> InoxRenderer for WgpuRenderer<'window> { - fn on_begin_draw(&mut self, puppet: &inox2d::puppet::Puppet) -> Result<(), Box> { - if self.encoder.is_some() { - panic!("Recursive rendering is not permitted."); - } - - if self.render_targets.is_none() { - panic!("Buffer is not yet set up."); - } - - self.encoder = Some(self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("Inox2DWGPU"), - })); - let surface_texture = self.surface.get_current_texture()?; - let texview = surface_texture - .texture - .create_view(&wgpu::TextureViewDescriptor::default()); - self.surface_texture = Some((surface_texture, texview)); - - //TODO: read & translate OpenGLRenderer's `on_begin_draw` / `on_end_draw` - - Ok(()) - } - +impl<'a, 'window> DrawSession<'a> for WgpuDrawSession<'a, 'window> { fn on_begin_masks(&mut self, masks: &components::Masks) { - self.last_mask_threshold = masks.threshold.clamp(0.0, 1.0); + self.render.last_mask_threshold = masks.threshold.clamp(0.0, 1.0); //TODO: Enable stencilling on the render target. - if let Some((composite, surface_stencil)) = self.render_targets.as_ref() { - let mut encoder = self.encoder.take().expect("encoder should not be held across calls"); - - composite.stencil().clear(&mut encoder); - surface_stencil.clear(&mut encoder); - - self.encoder = Some(encoder); + if let Some((composite, surface_stencil)) = self.render.render_targets.as_ref() { + composite.stencil().clear(&mut self.encoder); + surface_stencil.clear(&mut self.encoder); } } fn on_begin_mask(&mut self, mask: &components::Mask) { - self.stencil_reference_value = (mask.mode == components::MaskMode::Mask) as u32; + self.render.stencil_reference_value = (mask.mode == components::MaskMode::Mask) as u32; } fn on_begin_masked_content(&mut self) { - self.is_in_mask = true; + self.render.is_in_mask = true; } fn on_end_mask(&mut self) { - self.is_in_mask = false; + self.render.is_in_mask = false; } fn draw_textured_mesh_content( @@ -424,9 +431,9 @@ impl<'window> InoxRenderer for WgpuRenderer<'window> { render_ctx: &render::TexturedMeshRenderCtx, _id: InoxNodeUuid, ) { - if let Some((composite, surface_stencil)) = self.render_targets.as_ref() { + if let Some((composite, surface_stencil)) = self.render.render_targets.as_ref() { let gbuffer_color = composite.as_color_attachments(); - let surface_color_view = &self.surface_texture.as_ref().expect("surface").1; + let surface_color_view = &self.view; let surface_color_attach = Some(wgpu::RenderPassColorAttachment { view: &surface_color_view, depth_slice: None, @@ -439,7 +446,7 @@ impl<'window> InoxRenderer for WgpuRenderer<'window> { let masked_attach = [surface_color_attach.clone()]; let unmasked_attach = [surface_color_attach, None, None]; - let color_attachments = if self.is_in_composite { + let color_attachments = if self.render.is_in_composite { if render_mask { &[gbuffer_color[0].clone()] } else { @@ -452,7 +459,7 @@ impl<'window> InoxRenderer for WgpuRenderer<'window> { unmasked_attach.as_slice() } }; - let stencil_texture = if self.is_in_composite { + let stencil_texture = if self.render.is_in_composite { composite.stencil() } else { surface_stencil @@ -460,7 +467,7 @@ impl<'window> InoxRenderer for WgpuRenderer<'window> { let depth_stencil_attachment = if render_mask { Some(stencil_texture.as_depth_stencil_attachment_rw()) - } else if self.is_in_mask { + } else if self.render.is_in_mask { Some(stencil_texture.as_depth_stencil_attachment_ro()) } else { None @@ -469,9 +476,7 @@ impl<'window> InoxRenderer for WgpuRenderer<'window> { //TODO: Do we even want blending on in Normal mode? let blend = Some(Self::blend_mode_to_state(components.drawable.blending.mode)); - //NOTE: borrowck doesn't want us borrowing the encoder, so we .take() it instead. - let mut encoder = self.encoder.take().expect("encoder"); - let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + let mut render_pass = self.encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("WgpuRenderer::draw_textured_mesh_content"), color_attachments, depth_stencil_attachment, @@ -480,7 +485,7 @@ impl<'window> InoxRenderer for WgpuRenderer<'window> { multiview_mask: None, }); - let (albedo, bumpmap, emissive) = self.textures_for_part(components.texture); + let (albedo, bumpmap, emissive) = self.render.textures_for_part(components.texture); let (albedo, bumpmap, emissive) = (albedo.clone(), bumpmap.clone(), emissive.clone()); //TODO: set blend mode @@ -491,55 +496,59 @@ impl<'window> InoxRenderer for WgpuRenderer<'window> { mvp: Mat4::IDENTITY.to_cols_array_2d(), offset: [0.0; 2], } - .into_buffer(&self.device); + .into_buffer(&self.render.device); - render_pass.set_vertex_buffer(basic_vert::INPUT_LOCATION_VERTS, self.verts.slice(..)); - render_pass.set_vertex_buffer(basic_vert::INPUT_LOCATION_UVS, self.uvs.slice(..)); - render_pass.set_vertex_buffer(basic_vert::INPUT_LOCATION_DEFORM, self.deforms.slice(..)); - render_pass.set_index_buffer(self.indices.slice(..), wgpu::IndexFormat::Uint16); + render_pass.set_vertex_buffer(basic_vert::INPUT_LOCATION_VERTS, self.render.verts.slice(..)); + render_pass.set_vertex_buffer(basic_vert::INPUT_LOCATION_UVS, self.render.uvs.slice(..)); + render_pass.set_vertex_buffer(basic_vert::INPUT_LOCATION_DEFORM, self.render.deforms.slice(..)); + render_pass.set_index_buffer(self.render.indices.slice(..), wgpu::IndexFormat::Uint32); if render_mask { //TODO: What happens if a mask is also masked? - let pipeline = self.part_mask_pipeline.with_configuration( - &self.device, + let pipeline = self.render.part_mask_pipeline.with_configuration( + &self.render.device, [blend], [wgpu::ColorWrites::empty()], - Some(self.mask_depthstencil.clone()), + Some(self.render.mask_depthstencil.clone()), ); let uni_in_frag = basic_mask_frag::Input { - threshold: self.last_mask_threshold, + threshold: self.render.last_mask_threshold, } - .into_buffer(&self.device); + .into_buffer(&self.render.device); render_pass.set_pipeline(pipeline.pipeline()); pipeline.bind_frag( &mut render_pass, - Some(&self.part_shader_mask_frag.bind( - &self.device, + Some(&self.render.part_shader_mask_frag.bind( + &self.render.device, albedo.view(), - &self.model_sampler, + &self.render.model_sampler, &uni_in_frag, )), ); pipeline.bind_vertex( &mut render_pass, - Some(&self.part_shader_vert.bind(&self.device, &uni_in_vert)), + Some(&self.render.part_shader_vert.bind(&self.render.device, &uni_in_vert)), ); - render_pass.set_stencil_reference(self.stencil_reference_value); + render_pass.set_stencil_reference(self.render.stencil_reference_value); } else { let all = wgpu::ColorWrites::ALL; //Regular parts - let pipeline = if self.is_in_mask { - self.part_pipeline.with_configuration( - &self.device, + let pipeline = if self.render.is_in_mask { + self.render.part_pipeline.with_configuration( + &self.render.device, [blend, blend, blend], [all, all, all], - Some(self.masked_depthstencil.clone()), + Some(self.render.masked_depthstencil.clone()), ) } else { - self.part_pipeline - .with_configuration(&self.device, [blend, blend, blend], [all, all, all], None) + self.render.part_pipeline.with_configuration( + &self.render.device, + [blend, blend, blend], + [all, all, all], + None, + ) }; let uni_in_frag = basic_frag::Input { @@ -548,23 +557,23 @@ impl<'window> InoxRenderer for WgpuRenderer<'window> { screenColor: components.drawable.blending.screen_tint.into(), emissionStrength: 1.0, //NOTE: OpenGL never sets this. } - .into_buffer(&self.device); + .into_buffer(&self.render.device); render_pass.set_pipeline(pipeline.pipeline()); pipeline.bind_frag( &mut render_pass, - Some(&self.part_shader_frag.bind( - &self.device, + Some(&self.render.part_shader_frag.bind( + &self.render.device, albedo.view(), bumpmap.view(), emissive.view(), - &self.model_sampler, + &self.render.model_sampler, &uni_in_frag, )), ); pipeline.bind_vertex( &mut render_pass, - Some(&self.part_shader_vert.bind(&self.device, &uni_in_vert)), + Some(&self.render.part_shader_vert.bind(&self.render.device, &uni_in_vert)), ); render_pass.set_stencil_reference(1); @@ -572,9 +581,6 @@ impl<'window> InoxRenderer for WgpuRenderer<'window> { } render_pass.draw_indexed(0..render_ctx.index_len as u32, render_ctx.index_offset as i32, 0..1); - - drop(render_pass); //NOTE: borrowck also needs us to do this - self.encoder = Some(encoder); } } @@ -585,15 +591,11 @@ impl<'window> InoxRenderer for WgpuRenderer<'window> { render_ctx: &render::CompositeRenderCtx, id: InoxNodeUuid, ) { - self.is_in_composite = true; - - let mut encoder = self.encoder.take().expect("encoder"); + self.render.is_in_composite = true; - if let Some((composite, _surface_stencil)) = self.render_targets.as_ref() { - composite.clear(&mut encoder); + if let Some((composite, _surface_stencil)) = self.render.render_targets.as_ref() { + composite.clear(&mut self.encoder); } - - self.encoder = Some(encoder); } fn finish_composite_content( @@ -603,16 +605,14 @@ impl<'window> InoxRenderer for WgpuRenderer<'window> { render_ctx: &render::CompositeRenderCtx, id: InoxNodeUuid, ) { - assert!(self.is_in_composite); - self.is_in_composite = false; - - let mut encoder = self.encoder.take().expect("encoder"); + assert!(self.render.is_in_composite); + self.render.is_in_composite = false; - if let Some((composite, surface_stencil)) = self.render_targets.as_ref() { - let surface_color_view = &self.surface_texture.as_ref().expect("surface").1; + if let Some((composite, surface_stencil)) = self.render.render_targets.as_ref() { + let surface_color_view = &self.view; let depth_stencil_attachment = if render_mask { Some(surface_stencil.as_depth_stencil_attachment_rw()) - } else if self.is_in_mask { + } else if self.render.is_in_mask { Some(surface_stencil.as_depth_stencil_attachment_ro()) } else { None @@ -621,7 +621,7 @@ impl<'window> InoxRenderer for WgpuRenderer<'window> { //TODO: Do we even want blending on in Normal mode? let blend = Some(Self::blend_mode_to_state(components.drawable.blending.mode)); - let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + let mut render_pass = self.encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("WgpuRenderer Composite deferred pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { view: &surface_color_view, @@ -644,13 +644,13 @@ impl<'window> InoxRenderer for WgpuRenderer<'window> { todo!(); } else { let all = wgpu::ColorWrites::ALL; - let depth_stencil = if self.is_in_mask { - Some(self.masked_depthstencil.clone()) + let depth_stencil = if self.render.is_in_mask { + Some(self.render.masked_depthstencil.clone()) } else { None }; - let pipeline = self.composite_pipeline.with_configuration( - &self.device, + let pipeline = self.render.composite_pipeline.with_configuration( + &self.render.device, [blend, blend, blend], [all, all, all], depth_stencil, @@ -671,34 +671,32 @@ impl<'window> InoxRenderer for WgpuRenderer<'window> { .clamp(glam::Vec3::ZERO, glam::Vec3::ONE) .into(), } - .into_buffer(&self.device); + .into_buffer(&self.render.device); render_pass.set_pipeline(pipeline.pipeline()); pipeline.bind_frag( &mut render_pass, - Some(&self.composite_shader_frag.bind( - &self.device, + Some(&self.render.composite_shader_frag.bind( + &self.render.device, composite.albedo().view(), composite.emissive().view(), composite.bump().view(), - &self.model_sampler, + &self.render.model_sampler, &uni_in_frag, )), ); - pipeline.bind_vertex(&mut render_pass, Some(&self.composite_shader_vert.bind(&self.device))); + pipeline.bind_vertex( + &mut render_pass, + Some(&self.render.composite_shader_vert.bind(&self.render.device)), + ); render_pass.draw_indexed(0..6, 0, 0..1); //TODO: Where do these vertices come from!?!? } } - - self.encoder = Some(encoder); } - fn on_end_draw(&mut self, puppet: &inox2d::puppet::Puppet) { - let end = self.encoder.take().expect("encoder").finish(); - self.queue.submit(std::iter::once(end)); - - if let Some((surface_texture, _)) = self.surface_texture.take() { - surface_texture.present(); - } + fn on_end_draw(self, puppet: &inox2d::puppet::Puppet) { + let end = self.encoder.finish(); + self.render.queue.submit(std::iter::once(end)); + self.surface_texture.present(); } } diff --git a/inox2d/src/render.rs b/inox2d/src/render.rs index d2cc90c..6b937cc 100644 --- a/inox2d/src/render.rs +++ b/inox2d/src/render.rs @@ -4,7 +4,6 @@ mod vertex_buffers; use std::collections::HashSet; use std::error::Error; use std::mem::swap; -use std::error::Error; use crate::node::{ components::{DeformStack, Mask, Masks, ZSort}, From 1f48dea404f352b30b63b7234f860a07f227085f Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Tue, 24 Feb 2026 23:25:28 +0000 Subject: [PATCH 43/49] Cargo fmt the example wgpu renderer app --- examples/render-wgpu/src/main.rs | 115 +++++++++++++++++-------------- 1 file changed, 63 insertions(+), 52 deletions(-) diff --git a/examples/render-wgpu/src/main.rs b/examples/render-wgpu/src/main.rs index 1ddb662..95a8da1 100644 --- a/examples/render-wgpu/src/main.rs +++ b/examples/render-wgpu/src/main.rs @@ -1,19 +1,19 @@ -use env_logger; -use log::*; -use winit::event_loop::{EventLoop, ControlFlow}; -use winit::window::WindowBuilder; -use winit::event::{Event, WindowEvent}; use clap::Parser; +use common::scene::ExampleSceneController; +use env_logger; use inox2d::formats::inp::parse_inp; use inox2d::math::camera::Camera; use inox2d::render::InoxRendererExt; use inox2d_wgpu::WgpuRenderer; +use log::*; use pollster::block_on; -use common::scene::ExampleSceneController; +use winit::event::{Event, WindowEvent}; +use winit::event_loop::{ControlFlow, EventLoop}; +use winit::window::WindowBuilder; use std::error::Error; -use std::path::PathBuf; use std::fs; +use std::path::PathBuf; use std::sync::Arc; #[derive(Parser, Debug)] @@ -24,57 +24,68 @@ struct Cli { } fn main() -> Result<(), Box> { - let cli = Cli::parse(); + let cli = Cli::parse(); + + env_logger::init(); - env_logger::init(); + let event_loop = EventLoop::new()?; - let event_loop = EventLoop::new()?; + info!("Loading {:?}", cli.inp_path); - info!("Loading {:?}", cli.inp_path); + let data = fs::read(cli.inp_path)?; + let mut model = parse_inp(data.as_slice())?; - let data = fs::read(cli.inp_path)?; - let mut model = parse_inp(data.as_slice())?; + info!( + "Successfully parsed puppet: {}", + (model.puppet.meta.name.as_deref()).unwrap_or("") + ); - info!("Successfully parsed puppet: {}", - (model.puppet.meta.name.as_deref()).unwrap_or("")); - - model.puppet.init_transforms(); - model.puppet.init_rendering(); - model.puppet.init_params(); - model.puppet.init_physics(); + model.puppet.init_transforms(); + model.puppet.init_rendering(); + model.puppet.init_params(); + model.puppet.init_physics(); - let window = Arc::new(WindowBuilder::new().build(&event_loop).expect("valid window")); - let mut renderer = block_on(WgpuRenderer::new(window.clone(), &model)).expect("valid renderer"); - let mut scene_controller = ExampleSceneController::new(&Camera::default(), 0.5); - let camera = Camera::default(); + let window = Arc::new(WindowBuilder::new().build(&event_loop).expect("valid window")); + let mut renderer = block_on(WgpuRenderer::new(window.clone(), &model)).expect("valid renderer"); + let mut scene_controller = ExampleSceneController::new(&Camera::default(), 0.5); + let camera = Camera::default(); - event_loop.set_control_flow(ControlFlow::Poll); - event_loop.run(|event, event_loop| { - match event { - Event::WindowEvent { event: WindowEvent::Resized(new_size), .. } => { - if let Err(err) = renderer.resize(new_size.width, new_size.height) { - error!("Resize failed: {}", err); - } - } - Event::WindowEvent { event: WindowEvent::CloseRequested, .. } => { - event_loop.exit(); - } - Event::WindowEvent { event: WindowEvent::RedrawRequested, .. } => { - if let Err(err) = renderer.draw(&model.puppet) { - error!("Draw failed: {}", err); - } - } - Event::WindowEvent { event, .. } => { - scene_controller.interact(&event, &camera); - }, - Event::AboutToWait => { - window.request_redraw(); + event_loop.set_control_flow(ControlFlow::Poll); + event_loop.run(|event, event_loop| { + match event { + Event::WindowEvent { + event: WindowEvent::Resized(new_size), + .. + } => { + if let Err(err) = renderer.resize(new_size.width, new_size.height) { + error!("Resize failed: {}", err); + } + } + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => { + event_loop.exit(); + } + Event::WindowEvent { + event: WindowEvent::RedrawRequested, + .. + } => { + if let Err(err) = renderer.draw(&model.puppet) { + error!("Draw failed: {}", err); + } + } + Event::WindowEvent { event, .. } => { + scene_controller.interact(&event, &camera); + } + Event::AboutToWait => { + window.request_redraw(); - //TODO: Swapchain? Swapchain. - } - _ => {} - } - })?; + //TODO: Swapchain? Swapchain. + } + _ => {} + } + })?; - Ok(()) -} \ No newline at end of file + Ok(()) +} From 2501d8cf6233771e720ae9583b3b0546da934cb9 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Sat, 21 Mar 2026 23:12:11 +0000 Subject: [PATCH 44/49] Request clamp-to-border and clear texture features --- inox2d-wgpu/src/lib.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/inox2d-wgpu/src/lib.rs b/inox2d-wgpu/src/lib.rs index 844ecd5..71d411c 100644 --- a/inox2d-wgpu/src/lib.rs +++ b/inox2d-wgpu/src/lib.rs @@ -4,8 +4,8 @@ use inox2d::node::{InoxNodeUuid, components, drawables}; //hey wait a second tha use inox2d::render::{self, DrawSession, InoxRenderer}; use inox2d::texture::decode_model_textures; use std::error::Error; -use wgpu; use wgpu::util::{BufferInitDescriptor, DeviceExt}; +use wgpu::{self, FeaturesWGPU}; mod pipeline; mod shader; @@ -111,7 +111,12 @@ impl<'window> WgpuRenderer<'window> { ..Default::default() }) .await?; - let (device, queue) = adapter.request_device(&wgpu::DeviceDescriptor::default()).await?; + let (device, queue) = adapter + .request_device(&wgpu::DeviceDescriptor { + required_features: wgpu::Features::ADDRESS_MODE_CLAMP_TO_BORDER | wgpu::Features::CLEAR_TEXTURE, + ..Default::default() + }) + .await?; // Find a suitable surface configuration. let surface_caps = surface.get_capabilities(&adapter); From ac67ed8491074695f3dd67efe53a7c5c7e95c708 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Sun, 22 Mar 2026 00:35:29 +0000 Subject: [PATCH 45/49] We apparently need to blend on RGBA32float textures --- inox2d-wgpu/src/lib.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/inox2d-wgpu/src/lib.rs b/inox2d-wgpu/src/lib.rs index 71d411c..2aeabba 100644 --- a/inox2d-wgpu/src/lib.rs +++ b/inox2d-wgpu/src/lib.rs @@ -4,8 +4,8 @@ use inox2d::node::{InoxNodeUuid, components, drawables}; //hey wait a second tha use inox2d::render::{self, DrawSession, InoxRenderer}; use inox2d::texture::decode_model_textures; use std::error::Error; +use wgpu; use wgpu::util::{BufferInitDescriptor, DeviceExt}; -use wgpu::{self, FeaturesWGPU}; mod pipeline; mod shader; @@ -113,7 +113,9 @@ impl<'window> WgpuRenderer<'window> { .await?; let (device, queue) = adapter .request_device(&wgpu::DeviceDescriptor { - required_features: wgpu::Features::ADDRESS_MODE_CLAMP_TO_BORDER | wgpu::Features::CLEAR_TEXTURE, + required_features: wgpu::Features::ADDRESS_MODE_CLAMP_TO_BORDER + | wgpu::Features::CLEAR_TEXTURE + | wgpu::Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES, ..Default::default() }) .await?; From 64a0583a83bd95a64ac4f0a63cd42be62dc07cbb Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Sun, 22 Mar 2026 00:36:50 +0000 Subject: [PATCH 46/49] Run a frame before rendering to ensure all components are setup --- examples/render-wgpu/src/main.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/render-wgpu/src/main.rs b/examples/render-wgpu/src/main.rs index 95a8da1..d7e4ebd 100644 --- a/examples/render-wgpu/src/main.rs +++ b/examples/render-wgpu/src/main.rs @@ -45,6 +45,9 @@ fn main() -> Result<(), Box> { model.puppet.init_params(); model.puppet.init_physics(); + model.puppet.begin_frame(); + model.puppet.end_frame(0.01); + let window = Arc::new(WindowBuilder::new().build(&event_loop).expect("valid window")); let mut renderer = block_on(WgpuRenderer::new(window.clone(), &model)).expect("valid renderer"); let mut scene_controller = ExampleSceneController::new(&Camera::default(), 0.5); From 76beace3dd6e8fe13acf5000023aa20152277aed Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Sun, 22 Mar 2026 19:25:09 +0000 Subject: [PATCH 47/49] Ensure each pipeline has a label including the names of its vertex and fragment pipelines. --- inox2d-wgpu/build.rs | 7 +++++-- inox2d-wgpu/src/pipeline.rs | 5 +++-- inox2d-wgpu/src/shader.rs | 2 ++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/inox2d-wgpu/build.rs b/inox2d-wgpu/build.rs index 3463a7f..fe1be5f 100644 --- a/inox2d-wgpu/build.rs +++ b/inox2d-wgpu/build.rs @@ -517,11 +517,14 @@ fn gen_fragmentshader_trait_methods( Ok(()) } -fn gen_shader_trait_methods(out: &mut String, struct_name: &str) -> Result<(), Box> { +fn gen_shader_trait_methods(out: &mut String, struct_name: &str, label: &str) -> Result<(), Box> { writeln!(out, "impl shader::Shader for {} {{", struct_name)?; writeln!(out, " fn bindgroup_layout(&self) -> &wgpu::BindGroupLayout {{")?; writeln!(out, " &self.bindgroup_layout")?; writeln!(out, " }}")?; + writeln!(out, " fn label(&self) -> &str {{")?; + writeln!(out, " \"{}\"", label)?; + writeln!(out, " }}")?; writeln!(out, "}}")?; Ok(()) @@ -688,7 +691,7 @@ fn introspect_spirv( writeln!(out, "}}")?; writeln!(out)?; - gen_shader_trait_methods(out, &struct_name)?; + gen_shader_trait_methods(out, &struct_name, &format!("{}::{}", filename, entrypoint.name))?; if entrypoint .shader_stage diff --git a/inox2d-wgpu/src/pipeline.rs b/inox2d-wgpu/src/pipeline.rs index 53a455e..dd9942a 100644 --- a/inox2d-wgpu/src/pipeline.rs +++ b/inox2d-wgpu/src/pipeline.rs @@ -27,8 +27,9 @@ where write_mask: F::TargetArray, depth_stencil: Option, ) -> Self { + let name = format!("Pipeline of {} + {}", vert.label(), frag.label()); let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("Pipeline"), + label: Some(&name), // NOTE: This assumes vertex shaders always use set 0 and fragment shaders always use set 1. bind_group_layouts: &[vert.bindgroup_layout(), frag.bindgroup_layout()], @@ -51,7 +52,7 @@ where Self { pipeline: device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("Pipeline"), + label: Some(&name), layout: Some(&layout), vertex: vert.as_vertex_state(), fragment: Some(fragment), diff --git a/inox2d-wgpu/src/shader.rs b/inox2d-wgpu/src/shader.rs index 8f9d5a5..1bd05df 100644 --- a/inox2d-wgpu/src/shader.rs +++ b/inox2d-wgpu/src/shader.rs @@ -5,6 +5,8 @@ use std::hash::Hash; pub trait Shader: Clone { fn bindgroup_layout(&self) -> &wgpu::BindGroupLayout; + + fn label(&self) -> &str; } pub trait VertexShader: Shader { From 1e7751cb624a63c9cf661cc90c91204f9abdc195 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Sun, 22 Mar 2026 19:28:41 +0000 Subject: [PATCH 48/49] Ask for a higher limit of color attachment bytes --- inox2d-wgpu/src/lib.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/inox2d-wgpu/src/lib.rs b/inox2d-wgpu/src/lib.rs index 2aeabba..ac2e332 100644 --- a/inox2d-wgpu/src/lib.rs +++ b/inox2d-wgpu/src/lib.rs @@ -116,6 +116,10 @@ impl<'window> WgpuRenderer<'window> { required_features: wgpu::Features::ADDRESS_MODE_CLAMP_TO_BORDER | wgpu::Features::CLEAR_TEXTURE | wgpu::Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES, + required_limits: wgpu::Limits { + max_color_attachment_bytes_per_sample: 48, + ..Default::default() + }, ..Default::default() }) .await?; From e1ecc299c192b2df36d978d1cdc95fa09369c925 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Sun, 22 Mar 2026 19:30:10 +0000 Subject: [PATCH 49/49] Sampled images should have the correct data type --- inox2d-wgpu/build.rs | 67 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 61 insertions(+), 6 deletions(-) diff --git a/inox2d-wgpu/build.rs b/inox2d-wgpu/build.rs index fe1be5f..60ff386 100644 --- a/inox2d-wgpu/build.rs +++ b/inox2d-wgpu/build.rs @@ -1,8 +1,8 @@ use shaderc; use spirv_reflect; use spirv_reflect::types::{ - ReflectBlockVariable, ReflectDescriptorType, ReflectEntryPoint, ReflectFormat, ReflectTypeDescription, - ReflectTypeFlags, + ReflectBlockVariable, ReflectDescriptorType, ReflectEntryPoint, ReflectFormat, ReflectImageFormat, + ReflectTypeDescription, ReflectTypeFlags, }; use std::borrow::Cow; @@ -233,10 +233,65 @@ fn gen_shader_new( out, " view_dimension: wgpu::TextureViewDimension::D2," )?; //TODO: Support 1D/3D textures - writeln!( - out, - " sample_type: wgpu::TextureSampleType::Float {{ filterable: true }}," - )?; + match binding.image.image_format { + ReflectImageFormat::RGBA32_FLOAT | + ReflectImageFormat::RGBA16_FLOAT | + ReflectImageFormat::R32_FLOAT | + ReflectImageFormat::RG32_FLOAT | + ReflectImageFormat::RG16_FLOAT | + ReflectImageFormat::R11G11B10_FLOAT | + ReflectImageFormat::R16_FLOAT => { + // TODO: filtering on float textures is actually not permitted by WebGPU + // so we need a mode to ask the generated shader code to turn this off + writeln!( + out, + " sample_type: wgpu::TextureSampleType::Float {{ filterable: true }}," + )?; + } + ReflectImageFormat::RGBA8 | //TODO: Any documentation as to what this does? + ReflectImageFormat::RGBA16 | //I asked Al and he said this is UNORM, but Al + ReflectImageFormat::RGB10A2 | //likes to make things up a lot. + ReflectImageFormat::RG16 | + ReflectImageFormat::RG8 | + ReflectImageFormat::R16 | + ReflectImageFormat::R8 | + ReflectImageFormat::RGBA32_UINT | + ReflectImageFormat::RGBA16_UINT | + ReflectImageFormat::RGBA8_UINT | + ReflectImageFormat::R32_UINT | + ReflectImageFormat::RGB10A2_UINT | + ReflectImageFormat::RG32_UINT | + ReflectImageFormat::RG16_UINT | + ReflectImageFormat::RG8_UINT | + ReflectImageFormat::R16_UINT | + ReflectImageFormat::R8_UINT => { + writeln!( + out, + " sample_type: wgpu::TextureSampleType::Uint," + )?; + } + ReflectImageFormat::Undefined | //NOTE: To be clear, "Undefined" makes no sense. + ReflectImageFormat::RGBA8_SNORM | + ReflectImageFormat::RGBA16_SNORM | + ReflectImageFormat::RG16_SNORM | + ReflectImageFormat::RG8_SNORM | + ReflectImageFormat::R16_SNORM | + ReflectImageFormat::R8_SNORM | + ReflectImageFormat::RGBA32_INT | + ReflectImageFormat::RGBA16_INT | + ReflectImageFormat::RGBA8_INT | + ReflectImageFormat::R32_INT | + ReflectImageFormat::RG32_INT | + ReflectImageFormat::RG16_INT | + ReflectImageFormat::RG8_INT | + ReflectImageFormat::R16_INT | + ReflectImageFormat::R8_INT => { + writeln!( + out, + " sample_type: wgpu::TextureSampleType::Sint," + )?; + } + } writeln!(out, " }},")?; }