diff --git a/Cargo.toml b/Cargo.toml index 49a2811f..b5649119 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,5 +3,6 @@ resolver = "2" members = [ "inox2d", "inox2d-opengl", + "inox2d-wgpu", "examples/*", ] diff --git a/examples/render-opengl/src/main.rs b/examples/render-opengl/src/main.rs index 34091364..c744f036 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/examples/render-webgl/src/main.rs b/examples/render-webgl/src/main.rs index 3bc79cf2..c15475f5 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()); diff --git a/examples/render-wgpu/Cargo.toml b/examples/render-wgpu/Cargo.toml new file mode 100644 index 00000000..12f23119 --- /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 00000000..d7e4ebdc --- /dev/null +++ b/examples/render-wgpu/src/main.rs @@ -0,0 +1,94 @@ +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 winit::event::{Event, WindowEvent}; +use winit::event_loop::{ControlFlow, EventLoop}; +use winit::window::WindowBuilder; + +use std::error::Error; +use std::fs; +use std::path::PathBuf; +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(); + + 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); + 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(()) +} diff --git a/inox2d-opengl/src/lib.rs b/inox2d-opengl/src/lib.rs index 6e12b420..484d00cc 100644 --- a/inox2d-opengl/src/lib.rs +++ b/inox2d-opengl/src/lib.rs @@ -4,6 +4,7 @@ mod shaders; pub mod texture; use std::cell::RefCell; +use std::error::Error; use std::mem; use std::ops::Deref; @@ -18,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; @@ -410,11 +411,55 @@ impl OpenglRenderer { } impl InoxRenderer for OpenglRenderer { - fn on_begin_masks(&self, masks: &Masks) { - self.push_debug_group("inox2d - begin masks"); + type Draw<'a> + = OpenGlSession<'a> + where + Self: 'a; + + /// Update the renderer with latest puppet data. + fn on_begin_draw<'a>(&'a 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(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.render.push_debug_group("inox2d - begin masks"); + + let gl = &self.render.gl; + unsafe { gl.enable(glow::STENCIL_TEST); gl.clear_stencil(!masks.has_masks() as i32); @@ -425,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(&self, mask: &Mask) { - self.push_debug_group("inox2d - begin mask"); + fn on_begin_mask(&mut self, mask: &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(&self) { - self.push_debug_group("inox2d - begin masked content"); + fn on_begin_masked_content(&mut self) { + 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); @@ -452,30 +497,30 @@ impl InoxRenderer for OpenglRenderer { gl.color_mask(true, true, true, true); } - self.pop_debug_group(); + self.render.pop_debug_group(); } - fn on_end_mask(&self) { - let gl = &self.gl; + fn on_end_mask(&mut self) { + 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( - &self, + &mut self, as_mask: bool, components: &TexturedMeshComponents, 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 /* @@ -495,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(): @@ -506,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); @@ -529,23 +574,23 @@ impl InoxRenderer for OpenglRenderer { ); } - self.pop_debug_group(); + self.render.pop_debug_group(); } fn begin_composite_content( - &self, + &mut self, _as_mask: bool, _components: &CompositeComponents, _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, @@ -560,25 +605,25 @@ 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( - &self, + &mut self, as_mask: bool, components: &CompositeComponents, _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); } @@ -595,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); @@ -619,53 +664,20 @@ impl InoxRenderer for OpenglRenderer { gl.draw_elements(glow::TRIANGLES, 6, glow::UNSIGNED_INT, 0); } - 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"); + self.render.pop_debug_group(); } /// Renderer cleaning up after one frame. - pub fn on_end_draw(&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-wgpu/.gitignore b/inox2d-wgpu/.gitignore new file mode 100644 index 00000000..da84dff1 --- /dev/null +++ b/inox2d-wgpu/.gitignore @@ -0,0 +1,5 @@ +build + +#These are autogenerated shader introspection files +src/shaders/**/*.rs +src/shaders.rs \ No newline at end of file diff --git a/inox2d-wgpu/Cargo.toml b/inox2d-wgpu/Cargo.toml new file mode 100644 index 00000000..3995fa8c --- /dev/null +++ b/inox2d-wgpu/Cargo.toml @@ -0,0 +1,20 @@ +[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" } +glam = "0.29.0" +wgpu = { version = "28.0.0", features=["spirv"] } +thiserror = "1.0.39" + +[build-dependencies] +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 new file mode 100644 index 00000000..60ff3864 --- /dev/null +++ b/inox2d-wgpu/build.rs @@ -0,0 +1,892 @@ +use shaderc; +use spirv_reflect; +use spirv_reflect::types::{ + ReflectBlockVariable, ReflectDescriptorType, ReflectEntryPoint, ReflectFormat, ReflectImageFormat, + ReflectTypeDescription, ReflectTypeFlags, +}; + +use std::borrow::Cow; +use std::error::Error; +use std::ffi::OsString; +use std::fmt::Write; +use std::{fs, path}; + +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", + _ => "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<'a>(typemember: &'a 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, + typevar: &ReflectTypeDescription, +) -> Result<(), Box> { + writeln!(out, "#[allow(non_snake_case)]")?; //I'm too lazy to write a to_snake_case fn + writeln!(out, "pub 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 rust_type = spirv_to_rust_type(typemember)?; + writeln!(out, " pub {}: {},", blockmember.name, rust_type)?; + } + + writeln!(out, "}}")?; + writeln!(out)?; + + 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) { + 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, " }}")?; + writeln!(out, "}}")?; + writeln!(out)?; + + 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(std::num::NonZero::new({}).expect(\"nonzero type\")),", + binding.block.size + )?; + } else { + writeln!(out, " min_binding_size: None,")?; + } + writeln!(out, " }},")?; + } + + //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 + 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, " }},")?; + } + + //TODO: generate bindings for all of these + ReflectDescriptorType::Undefined + | 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)?; + } + ReflectDescriptorType::SampledImage => { + write!(&mut bind_params, ", {}: &wgpu::TextureView", binding.name)?; + } + + //TODO: generate bindings for all of these + ReflectDescriptorType::Undefined + | 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, + " pub 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 + )?; + } + ReflectDescriptorType::SampledImage => { + writeln!( + out, + " resource: wgpu::BindingResource::TextureView({})", + binding.name + )?; + } + + //TODO: generate bindings for all of these + ReflectDescriptorType::Undefined + | 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_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)?; + 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, " }}")?; + writeln!(out, "}}")?; + + Ok(()) +} + +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, + " type TargetArray = [T; {}];", + 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)?; + writeln!(out, " entry_point: Some(\"{}\"),", entrypoint.name)?; + 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,")?; + } + } + + // 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,")?; + + writeln!(out, " }}),")?; + } + writeln!(out, " ],")?; + writeln!( + out, + " compilation_options: wgpu::PipelineCompilationOptions::default()" + )?; + writeln!(out, " }}")?; + writeln!(out, " }}")?; + writeln!(out, "}}")?; + + Ok(()) +} + +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(()) +} + +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 crate::shader;")?; + + for entrypoint in module.enumerate_entry_points()? { + // 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:")?; + writeln!( + out, + "pub const INPUT_LOCATION_{}: u32 = {};", + var.name.to_uppercase(), + var.location + )?; + } + + 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:")?; + + 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 { + writeln!(out, "/// descriptor set {}", descriptor_set.set)?; + + 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 => { + 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)?; + writeln!( + out, + "const BINDING_{}: u32 = {};", + binding.name.to_uppercase(), + binding.binding + )?; + } 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)?; + + 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, "#[derive(Clone)]")?; + 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 {} {{", 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_trait_methods(out, &struct_name, &format!("{}::{}", filename, entrypoint.name))?; + + if entrypoint + .shader_stage + .contains(spirv_reflect::types::ReflectShaderStageFlags::VERTEX) + { + writeln!(out)?; + gen_vertexshader_trait_methods(out, &entrypoint, &struct_name)?; + } + + if entrypoint + .shader_stage + .contains(spirv_reflect::types::ReflectShaderStageFlags::FRAGMENT) + { + writeln!(out)?; + gen_fragmentshader_trait_methods(out, &entrypoint, &struct_name)?; + } + } + + 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(); + + 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, + &item_filename.to_string_lossy(), + &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 +/// 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()?; + + 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 new file mode 100644 index 00000000..ac2e3328 --- /dev/null +++ b/inox2d-wgpu/src/lib.rs @@ -0,0 +1,713 @@ +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, DrawSession, InoxRenderer}; +use inox2d::texture::decode_model_textures; +use std::error::Error; +use wgpu; +use wgpu::util::{BufferInitDescriptor, DeviceExt}; + +mod pipeline; +mod shader; +mod shaders; +mod texture; + +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}; + +/// 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: &[u32]) -> &[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), + 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, + + /// 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, + 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, + + 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::PipelineGroup, + composite_mask_pipeline: pipeline::PipelineGroup, + + 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, + queue: wgpu::Queue, +} + +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 { + 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?; + + // 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); + let part_shader_mask_frag = basic_mask_frag::Shader::new(&device); + + 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(), + }; + + //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::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 + .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() { + 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, + config, + render_targets: None, + verts, + uvs, + deforms, + indices, + part_shader_vert, + part_shader_frag, + part_shader_mask_frag, + mask_depthstencil, + masked_depthstencil, + part_pipeline, + part_mask_pipeline, + composite_shader_vert, + composite_shader_frag, + composite_shader_mask_frag, + composite_pipeline, + composite_mask_pipeline, + 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) -> Result<(), WgpuRendererError> { + 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.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) + } + } + + 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> { + 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 { + 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<'a, 'window> DrawSession<'a> for WgpuDrawSession<'a, 'window> { + fn on_begin_masks(&mut self, masks: &components::Masks) { + 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.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.render.stencil_reference_value = (mask.mode == components::MaskMode::Mask) as u32; + } + + fn on_begin_masked_content(&mut self) { + self.render.is_in_mask = true; + } + + fn on_end_mask(&mut self) { + self.render.is_in_mask = false; + } + + fn draw_textured_mesh_content( + &mut self, + render_mask: bool, + components: &drawables::TexturedMeshComponents, + render_ctx: &render::TexturedMeshRenderCtx, + _id: InoxNodeUuid, + ) { + if let Some((composite, surface_stencil)) = self.render.render_targets.as_ref() { + let gbuffer_color = composite.as_color_attachments(); + let surface_color_view = &self.view; + 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.render.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.render.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.render.is_in_mask { + Some(stencil_texture.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 = self.encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("WgpuRenderer::draw_textured_mesh_content"), + color_attachments, + depth_stencil_attachment, + occlusion_query_set: None, + timestamp_writes: None, + multiview_mask: None, + }); + + 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 + 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.render.device); + + 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.render.part_mask_pipeline.with_configuration( + &self.render.device, + [blend], + [wgpu::ColorWrites::empty()], + Some(self.render.mask_depthstencil.clone()), + ); + let uni_in_frag = basic_mask_frag::Input { + threshold: self.render.last_mask_threshold, + } + .into_buffer(&self.render.device); + + render_pass.set_pipeline(pipeline.pipeline()); + pipeline.bind_frag( + &mut render_pass, + Some(&self.render.part_shader_mask_frag.bind( + &self.render.device, + albedo.view(), + &self.render.model_sampler, + &uni_in_frag, + )), + ); + pipeline.bind_vertex( + &mut render_pass, + Some(&self.render.part_shader_vert.bind(&self.render.device, &uni_in_vert)), + ); + + render_pass.set_stencil_reference(self.render.stencil_reference_value); + } else { + let all = wgpu::ColorWrites::ALL; + //Regular parts + 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.render.masked_depthstencil.clone()), + ) + } else { + self.render.part_pipeline.with_configuration( + &self.render.device, + [blend, blend, blend], + [all, all, all], + None, + ) + }; + + 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.render.device); + + render_pass.set_pipeline(pipeline.pipeline()); + pipeline.bind_frag( + &mut render_pass, + Some(&self.render.part_shader_frag.bind( + &self.render.device, + albedo.view(), + bumpmap.view(), + emissive.view(), + &self.render.model_sampler, + &uni_in_frag, + )), + ); + pipeline.bind_vertex( + &mut render_pass, + Some(&self.render.part_shader_vert.bind(&self.render.device, &uni_in_vert)), + ); + + 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); + } + } + + fn begin_composite_content( + &mut self, + as_mask: bool, + components: &drawables::CompositeComponents, + render_ctx: &render::CompositeRenderCtx, + id: InoxNodeUuid, + ) { + self.render.is_in_composite = true; + + if let Some((composite, _surface_stencil)) = self.render.render_targets.as_ref() { + composite.clear(&mut self.encoder); + } + } + + fn finish_composite_content( + &mut self, + render_mask: bool, + components: &drawables::CompositeComponents, + render_ctx: &render::CompositeRenderCtx, + id: InoxNodeUuid, + ) { + assert!(self.render.is_in_composite); + self.render.is_in_composite = false; + + 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.render.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 = self.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.render.is_in_mask { + Some(self.render.masked_depthstencil.clone()) + } else { + None + }; + let pipeline = self.render.composite_pipeline.with_configuration( + &self.render.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.render.device); + + render_pass.set_pipeline(pipeline.pipeline()); + pipeline.bind_frag( + &mut render_pass, + Some(&self.render.composite_shader_frag.bind( + &self.render.device, + composite.albedo().view(), + composite.emissive().view(), + composite.bump().view(), + &self.render.model_sampler, + &uni_in_frag, + )), + ); + 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!?!? + } + } + } + + 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-wgpu/src/pipeline.rs b/inox2d-wgpu/src/pipeline.rs new file mode 100644 index 00000000..dd9942ad --- /dev/null +++ b/inox2d-wgpu/src/pipeline.rs @@ -0,0 +1,148 @@ +use wgpu; + +use crate::shader::{FragmentShader, VertexShader}; +use std::collections::HashMap; +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, + blend: F::TargetArray>, + 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(&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()], + immediate_size: 0, + }); + + let mut fragment = frag.as_fragment_state(); + let mut fragment_targets = fragment.targets.to_owned(); + 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; + + Self { + pipeline: device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some(&name), + layout: Some(&layout), + vertex: vert.as_vertex_state(), + fragment: Some(fragment), + 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, + 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, &[]) + } + + pub fn pipeline(&self) -> &wgpu::RenderPipeline { + &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::TargetArray>, + F::TargetArray, + 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::TargetArray>, + write_mask: F::TargetArray, + depth_stencil: Option, + ) -> &Pipeline { + self.cache + .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 new file mode 100644 index 00000000..1bd05dfc --- /dev/null +++ b/inox2d-wgpu/src/shader.rs @@ -0,0 +1,35 @@ +use wgpu; +use wgpu::util::DeviceExt; + +use std::hash::Hash; + +pub trait Shader: Clone { + fn bindgroup_layout(&self) -> &wgpu::BindGroupLayout; + + fn label(&self) -> &str; +} + +pub trait VertexShader: Shader { + fn as_vertex_state(&self) -> wgpu::VertexState; +} + +pub trait FragmentShader: Shader { + type TargetArray: IntoIterator + Eq + Hash + Clone; + + 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, + }) + } +} diff --git a/inox2d-wgpu/src/shaders/basic/anim.vert b/inox2d-wgpu/src/shaders/basic/anim.vert new file mode 100644 index 00000000..569f0709 --- /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(set = 0, 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 00000000..e4265f49 --- /dev/null +++ b/inox2d-wgpu/src/shaders/basic/basic-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(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(sampler2D(tex, samp), 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 00000000..72bf39b2 --- /dev/null +++ b/inox2d-wgpu/src/shaders/basic/basic.frag @@ -0,0 +1,44 @@ +/* + 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(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 = 4) uniform Input { + uniform float opacity; + uniform vec3 multColor; + uniform vec3 screenColor; + uniform float emissionStrength; +} uni_in; + +void main() { + // Sample texture + vec4 texColor = texture(sampler2D(albedo_tex, samp), 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(sampler2D(emissive_tex, samp), texUVs).xyz * uni_in.emissionStrength, 1) * outAlbedo.a; + + // Bumpmap + 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/basic.vert b/inox2d-wgpu/src/shaders/basic/basic.vert new file mode 100644 index 00000000..a01f4126 --- /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(set = 0, 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 00000000..f95591b4 --- /dev/null +++ b/inox2d-wgpu/src/shaders/basic/composite-mask.frag @@ -0,0 +1,24 @@ +/* + 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(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(sampler2D(tex, samp), 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 00000000..0250ebdb --- /dev/null +++ b/inox2d-wgpu/src/shaders/basic/composite.frag @@ -0,0 +1,42 @@ +/* + 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(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 = 4) uniform Input { + float opacity; + vec3 multColor; + vec3 screenColor; +} uni_in; + +void main() { + // Sample texture + vec4 texColor = texture(sampler2D(albedo_tex, samp), 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(sampler2D(emissive_tex, samp), texUVs) * outAlbedo.a; + + // Bumpmap + outBump = texture(sampler2D(bumpmap_tex, samp), 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 00000000..4903a594 --- /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(set = 0, 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 00000000..3ea4107b --- /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(set = 0, 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 00000000..a6fc8283 --- /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(set = 1, 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 00000000..7a2644a9 --- /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(set = 1, 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 00000000..1d1ba3be --- /dev/null +++ b/inox2d-wgpu/src/shaders/lighting.frag @@ -0,0 +1,57 @@ +/* + 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(set = 1, 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(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) { + return exp(-0.5 * dot(i /= sigma, i)) / (6.28 * sigma * sigma); +} + +// Bloom texture by blurring it +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; + 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(sampler2D(tx, smp), uv + scale * d, uni_in.LOD); + } + + return out_ / out_.a; +} + +void main() { + + // Bloom + 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(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/mask.frag b/inox2d-wgpu/src/shaders/mask.frag new file mode 100644 index 00000000..af09fb1f --- /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 00000000..e0efb221 --- /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(set = 0, 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 00000000..41f77a32 --- /dev/null +++ b/inox2d-wgpu/src/shaders/scene.frag @@ -0,0 +1,20 @@ +/* + 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(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(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 diff --git a/inox2d-wgpu/src/shaders/scene.vert b/inox2d-wgpu/src/shaders/scene.vert new file mode 100644 index 00000000..e2e51297 --- /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(set = 0, 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 diff --git a/inox2d-wgpu/src/texture.rs b/inox2d-wgpu/src/texture.rs new file mode 100644 index 00000000..c326ff53 --- /dev/null +++ b/inox2d-wgpu/src/texture.rs @@ -0,0 +1,289 @@ +use wgpu; + +use inox2d::model::Model; +use inox2d::texture::ShallowTexture; + +#[derive(Clone)] +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 empty_render_target( + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + 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(encoder); + empty + } + + // Clear the texture. + 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 { + &self.device_texture + } + + 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 DepthStencilTexture { + device_texture: wgpu::Texture, + view: wgpu::TextureView, + format: wgpu::TextureFormat, +} + +impl DepthStencilTexture { + pub fn empty_render_target( + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + 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, + format, + }; + + empty.clear(encoder); + empty + } + + // Clear the texture. + 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 { + &self.device_texture + } + + pub fn view(&self) -> &wgpu::TextureView { + &self.view + } + + 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: 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, + }), + } + } +} + +/// Structure that holds render targets for interim rendering results. +pub struct GBuffer { + albedo: DeviceTexture, + emissive: DeviceTexture, + bump: DeviceTexture, + stencil: DepthStencilTexture, +} + +impl GBuffer { + pub fn new( + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + width: u32, + height: u32, + format: wgpu::TextureFormat, + depth_format: wgpu::TextureFormat, + ) -> Self { + Self { + 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: DepthStencilTexture::empty_render_target(device, encoder, width, height, depth_format), + } + } + + 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) -> &DepthStencilTexture { + &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()), + Some(self.emissive.as_color_attachment()), + Some(self.bump.as_color_attachment()), + ] + } +} diff --git a/inox2d/src/render.rs b/inox2d/src/render.rs index c77fe74d..6b937ccf 100644 --- a/inox2d/src/render.rs +++ b/inox2d/src/render.rs @@ -2,6 +2,7 @@ mod deform_stack; mod vertex_buffers; use std::collections::HashSet; +use std::error::Error; use std::mem::swap; use crate::node::{ @@ -215,25 +216,37 @@ 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<'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. - 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 +257,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 +267,43 @@ 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 on_end_draw(self, puppet: &Puppet); } -pub trait InoxRendererExt { +pub trait DrawSessionExt { /// Draw a Drawable, which is potentially masked. - fn draw_drawable(&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(&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 { /// 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) -> Result<(), Box>; } -impl InoxRendererExt for T { - fn draw_drawable(&self, as_mask: bool, comps: &World, id: InoxNodeUuid) { +impl<'a, T: DrawSession<'a>> DrawSessionExt for T { + 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, @@ -291,7 +317,7 @@ impl InoxRendererExt 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(); } @@ -300,7 +326,7 @@ impl InoxRendererExt 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 { @@ -308,13 +334,26 @@ impl InoxRendererExt for T { } } - fn draw_composite(&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 { @@ -330,25 +369,33 @@ 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(&self, puppet: &Puppet) { + fn draw(&mut self, puppet: &Puppet) -> Result<(), Box> { + let mut draw = self.on_begin_draw(puppet)?; + for uuid in &puppet .render_ctx .as_ref() .expect("RenderCtx of puppet must be initialized before calling draw().") .root_drawables_zsorted { - self.draw_drawable(false, &puppet.node_comps, *uuid); + draw.draw_drawable(puppet, false, &puppet.node_comps, *uuid); } + + draw.on_end_draw(puppet); + + Ok(()) } }