//! # Renderer Module //! //! 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; // 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`: 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, 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, }); 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"), bind_group_layouts: &[], immediate_size: 0, // no var used }); device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some("Render Pipeline"), layout: Some(&render_pipeline_layout), vertex: wgpu::VertexState { 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: &[Some(vertex_buffer_layout)], }, fragment: Some(wgpu::FragmentState { 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. targets: &[Some(wgpu::ColorTargetState { format, blend: Some(wgpu::BlendState::REPLACE), write_mask: wgpu::ColorWrites::ALL, })], }), primitive: wgpu::PrimitiveState::default(), depth_stencil: None, multisample: wgpu::MultisampleState::default(), // multiview → replaced by multiview_mask (NonZeroU32) and cache in wgpu 30. multiview_mask: None, cache: None, }) } /// 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. /// Inputs: `device` for command buffer creation, `queue` for submission, `view` as render target (from Context). /// 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 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("render encoder"), }); // RenderPass start — depth_slice is a new required field in wgpu 30 for multisampled textures. let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("render pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { view, resolve_target: None, depth_slice: None, // new field; None means no multisample resolve needed ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), store: wgpu::StoreOp::Store, }, })], ..Default::default() }); // Pipeline call — draw with empty vertex buffers (geometry defined in shader) render_pass.set_pipeline(&self.render_pipeline); 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())); } /// 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())); } }