From 7cc1fb845fe3f8622e94bb05f882551b339deb83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Bousqui=C3=A9?= Date: Sun, 5 Jul 2026 08:00:58 +0200 Subject: [PATCH] renderer render --- Cargo.lock | 1 + assets/shaders/basic_shader.wgsl | 8 +- lib/Cargo.toml | 3 +- lib/conf.rs | 6 +- lib/lib.rs | 1 + lib/renderer.rs | 145 +++++++++++++++++++++++++------ lib/vertex.rs | 7 ++ 7 files changed, 142 insertions(+), 29 deletions(-) create mode 100644 lib/vertex.rs diff --git a/Cargo.lock b/Cargo.lock index f21d685..3ea82a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2302,6 +2302,7 @@ checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" name = "wsg-lib" version = "0.1.0" dependencies = [ + "bytemuck", "thiserror 2.0.18", "wgpu", "winit", diff --git a/assets/shaders/basic_shader.wgsl b/assets/shaders/basic_shader.wgsl index c6b3dd4..d2b640f 100644 --- a/assets/shaders/basic_shader.wgsl +++ b/assets/shaders/basic_shader.wgsl @@ -1,20 +1,24 @@ //! # Basic Shader Module struct VertexInput { @location(0) position: vec3, + @location(1) uv: vec2, + @location(2) color: vec3, }; struct VertexOutput { @builtin(position) clip_position: vec4, + @location(0) color: vec3, }; @vertex fn vs_main(model: VertexInput) -> VertexOutput { var out: VertexOutput; out.clip_position = vec4(model.position, 1.0); + out.color = model.color; // On transmet la couleur au fragment shader return out; } @fragment -fn fs_main() -> @location(0) vec4 { - return vec4(1.0, 0.5, 0.2, 1.0); // Couleur orange +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + return vec4(in.color, 1.0); } diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 143fec5..581ed0e 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -8,5 +8,6 @@ path = "lib.rs" [dependencies] wgpu = "30.0.0" # Vérifiez la version la plus récente -winit = "=0.29" # For window management — pinned to match examples +winit = "0.29" # For window management — pinned to match examples thiserror = "2" +bytemuck = { version = "1.25.0", features = ["derive"] } diff --git a/lib/conf.rs b/lib/conf.rs index cccd803..749008e 100644 --- a/lib/conf.rs +++ b/lib/conf.rs @@ -3,4 +3,8 @@ //! Holds shared constants for the WSG library, such as shader paths and other compilation-time values. //! This module centralizes configuration so that it can be imported by any submodule without duplicating literals. -pub const BASIC_SHADER: &str = "assets/shaders/basic_shader.wgsl"; +/// Path to the default WGSL shader file (runtime). +pub const BASIC_SHADER_PATH: &str = "assets/shaders/basic_shader.wgsl"; + +/// The basic WGSL shader source, embedded at compile time as a fallback. +pub const BASIC_SHADER: &str = include_str!("../assets/shaders/basic_shader.wgsl"); diff --git a/lib/lib.rs b/lib/lib.rs index 057a537..eeb79be 100644 --- a/lib/lib.rs +++ b/lib/lib.rs @@ -2,5 +2,6 @@ pub mod conf; pub mod context; pub mod error; pub mod renderer; +pub mod vertex; pub use error::WsgError; // Pour permettre un import direct du type diff --git a/lib/renderer.rs b/lib/renderer.rs index c5133b2..e216d3d 100644 --- a/lib/renderer.rs +++ b/lib/renderer.rs @@ -1,39 +1,123 @@ //! # Renderer Module //! -//! The **Specialist** layer of the architecture. Owns rendering logic — RenderPipeline, shaders, and buffers — but does not own hardware resources (Device, Queue). Receives references to those resources when called by the orchestrator (main.rs). +//! The **Specialist** layer of the architecture. Owns rendering logic — RenderPipeline, shaders, and vertex buffers. +//! Does not own hardware resources (Device, Queue); receives references when called by the orchestrator (main.rs). //! Interacts with Context via `begin_frame()` / `end_frame()`: receives a TextureView as input, writes rendered output to it. use crate::conf::BASIC_SHADER; +use crate::vertex::Vertex; -// On demande à Cargo de surveiller ce fichier (si le wsgl est modifié, mais pas la lib) -const _: &[u8] = include_bytes!("../assets/shaders/basic_shader.wgsl"); +// Required for `create_buffer_init` method on Device (wgpu::util::DeviceExt). +use wgpu::util::DeviceExt; /// Renders geometry using a fixed RenderPipeline. This struct owns heavy objects /// (RenderPipeline) compiled once at creation time, avoiding per-frame allocation. pub struct Renderer { /// Pre-compiled pipeline defining vertex/fragment stages, layout, and blend state. render_pipeline: wgpu::RenderPipeline, + /// Default vertex buffer containing a fallback triangle (position + UV + color). + default_buffer: wgpu::Buffer, + /// Optional user-provided vertex buffer; replaces the default when set. + user_buffer: Option, } impl Renderer { /// Creates the renderer by compiling the shader module and building the render pipeline. - /// Inputs: `device` for resource creation, `format` for color attachment format. + /// Inputs: + /// - `device`: GPU device for resource creation + /// - `format`: Color attachment format + /// - `shader_path`: Optional custom WGSL shader file path; falls back to BASIC_SHADER if None. /// Returns the initialized Renderer owning the pipeline. Called once at application startup /// by the orchestrator (main.rs). The pipeline is compiled only here; subsequent calls are cheap. - pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self { - // In wgpu 30, ShaderModuleDescriptor fields changed: label is Option<&str> (not Some(label)), - // and source uses Cow<'_, str> instead of [u8]. - // BASIC_SHADER serves as both the debug label and the file path loaded at runtime - // (include_str!() requires a string literal, not a variable). - // See assets/shaders/basic_shader.wgsl for the WGSL source — defines vs_main + fs_main stages. - let shader_source = - std::fs::read_to_string(BASIC_SHADER).expect("Failed to read shader file"); - let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some(BASIC_SHADER), - source: wgpu::ShaderSource::Wgsl(shader_source.into()), + pub fn new( + device: &wgpu::Device, + format: wgpu::TextureFormat, + shader_path: Option<&str>, + ) -> Self { + let shader = Self::load_shader(device, shader_path); + // 1. Define the default triangle vertices (position + UV + color). + let vertices: &[Vertex] = &[ + Vertex { + position: [0.0, 0.5, 0.0], + uv: [0.5, 0.0], + color: [1.0, 0.0, 0.0], + }, + Vertex { + position: [-0.5, -0.5, 0.0], + uv: [0.0, 1.0], + color: [0.0, 1.0, 0.0], + }, + Vertex { + position: [0.5, -0.5, 0.0], + uv: [1.0, 1.0], + color: [0.0, 0.0, 1.0], + }, + ]; + + // 2. Create the GPU vertex buffer from those vertices. + let default_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Default Triangle Buffer"), + contents: bytemuck::cast_slice(vertices), + usage: wgpu::BufferUsages::VERTEX, }); - // push_constant_ranges → removed in wgpu 30; replaced by immediate_size for var support. + Self { + render_pipeline: Self::build_pipeline(device, format, &shader), + default_buffer, + user_buffer: None, // User can set this later via update_shader or a setter. + } + } + + /// Loads a WGSL shader module from the given path, falling back to BASIC_SHADER if no path is provided. + pub fn load_shader(device: &wgpu::Device, path: Option<&str>) -> wgpu::ShaderModule { + let source = match path { + Some(p) => match std::fs::read_to_string(p) { + Ok(s) => s, + Err(_) => { + println!( + "no wsgl file shader found in assets/shaders/, internal basic shader applied instead" + ); + BASIC_SHADER.to_string() + } + }, + None => BASIC_SHADER.to_string(), + }; + device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some(path.unwrap_or("basic_shader")), + source: wgpu::ShaderSource::Wgsl(source.into()), + }) + } + + /// Builds a render pipeline from a shader module, device, and format. + fn build_pipeline( + device: &wgpu::Device, + format: wgpu::TextureFormat, + shader: &wgpu::ShaderModule, + ) -> wgpu::RenderPipeline { + // Define the vertex layout (the contract between CPU data and GPU shaders). + let vertex_buffer_layout = wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::() as wgpu::BufferAddress, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &[ + wgpu::VertexAttribute { + offset: 0, + shader_location: 0, + format: wgpu::VertexFormat::Float32x3, + }, // Pos + wgpu::VertexAttribute { + offset: 12, + shader_location: 1, + format: wgpu::VertexFormat::Float32x2, + }, // UV + wgpu::VertexAttribute { + offset: 20, + shader_location: 2, + format: wgpu::VertexFormat::Float32x3, + }, // Color + ], + }; + + // Pipeline layout (wgpu 30: `immediate_size` replaces `push_constant_ranges`). let render_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("render_pipeline_layout"), @@ -41,18 +125,18 @@ impl Renderer { immediate_size: 0, // no var used }); - let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some("Render Pipeline"), layout: Some(&render_pipeline_layout), vertex: wgpu::VertexState { - module: &shader, + module: shader, // entry_point is now Option<&str> — Some to specify the entry point explicitly, None to auto-detect. entry_point: Some("vs_main"), compilation_options: Default::default(), // new required field in wgpu 30 - buffers: &[], + buffers: &[Some(vertex_buffer_layout)], }, fragment: Some(wgpu::FragmentState { - module: &shader, + module: shader, entry_point: Some("fs_main"), compilation_options: Default::default(), // new required field in wgpu 30 // targets is now &[Option] — each target wrapped in Some. @@ -68,9 +152,18 @@ impl Renderer { // multiview → replaced by multiview_mask (NonZeroU32) and cache in wgpu 30. multiview_mask: None, cache: None, - }); + }) + } - Self { render_pipeline } + /// Hot-reloads a new WGSL shader and rebuilds the render pipeline. + pub fn update_shader( + &mut self, + device: &wgpu::Device, + format: wgpu::TextureFormat, + shader_path: Option<&str>, + ) { + let new_shader = Self::load_shader(device, shader_path); + self.render_pipeline = Self::build_pipeline(device, format, &new_shader); } /// Renders a single frame by encoding commands into a temporary CommandEncoder and submitting them to the queue. @@ -78,7 +171,7 @@ impl Renderer { /// Called by the orchestrator after begin_frame() returns the TextureView. The pipeline is invoked once per call; /// no persistent state is retained between frames. pub fn render(&self, device: &wgpu::Device, queue: &wgpu::Queue, view: &wgpu::TextureView) { - // create command encoder + // Create command encoder let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("render encoder"), }); @@ -100,13 +193,15 @@ impl Renderer { // Pipeline call — draw with empty vertex buffers (geometry defined in shader) render_pass.set_pipeline(&self.render_pipeline); - render_pass.draw(0..3, 0..1); + let active_buffer = self.user_buffer.as_ref().unwrap_or(&self.default_buffer); + render_pass.set_vertex_buffer(0, active_buffer.slice(..)); + render_pass.draw(0..3, 0..1); // Draw the 3 vertices of the selected triangle // Queue submit drop(render_pass); queue.submit(std::iter::once(encoder.finish())); } - // méthode utilitaire pour cacher le "bruit" + /// Utility to hide the "noise" of queue submission. pub fn submit_commands(&self, queue: &wgpu::Queue, encoder: wgpu::CommandEncoder) { queue.submit(std::iter::once(encoder.finish())); } diff --git a/lib/vertex.rs b/lib/vertex.rs new file mode 100644 index 0000000..0ca5ab2 --- /dev/null +++ b/lib/vertex.rs @@ -0,0 +1,7 @@ +#[repr(C)] +#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] +pub struct Vertex { + pub position: [f32; 3], + pub uv: [f32; 2], + pub color: [f32; 3], +}