re-org en App

This commit is contained in:
Jérôme Bousquié
2026-07-07 16:42:37 +02:00
parent cfd8b421a2
commit 82ea16d118
28 changed files with 518 additions and 67 deletions
+31
View File
@@ -0,0 +1,31 @@
# wsg-lib Source Directory
## Overview
This is the source tree for `wsg-lib`, a Rust library wrapping [wgpu](https://github.com/gfx-rs/wgpu) for simple 3D drawing operations. The crate follows a layered architecture organized into five modules:
| Module | Responsibility |
|--------|---------------|
| **core** | Manager (Context) + Executor (Renderer) layers — GPU lifecycle and draw call orchestration |
| **resources** | Data types: Vertex (CPU-side), Mesh (GPU geometry), Material (appearance descriptor) |
| **pipeline** | PipelineCache — WGSL shader loading and RenderPipeline compilation cache |
| **scene** | Scene — resource depot and entity graph for declarative rendering setup |
| **utils** | Configuration constants and WsgError type |
## Architecture Pattern
The library supports two workflows:
- **Declarative (recommended)**: Use `Scene` to register resources and associate entities before the render loop begins. This is the "App facade" pattern described in [ARCHI_APP_FACADE](../../docs/ARCHI_APP_FACADE.md).
- **Manual**: Manipulate Context, Renderer, and PipelineCache directly for fine-grained control.
## Dependency Flow
```
scene → core → pipeline → utils
↘→ resources → utils (via Vertex offsets)
↗
scene (consumes)
```
Each submodule's `mod.rs` re-exports its public types so consumers import through the module level rather than deep paths.
+18
View File
@@ -0,0 +1,18 @@
# Core Module — Manager and Executor Layers
## Overview
The `core` module contains two architectural layers that drive rendering:
| File | Responsibility |
|------|---------------|
| **context** | **Manager layer** — owns GPU hardware resource lifecycle (Instance, Surface, Adapter, Device, Queue). Initializes GPU at startup; orchestrates frame-by-frame rendering via begin_frame() / end_frame(). Does not own rendering logic. |
| **renderer** | **Executor layer** — owns Device/Queue references after initialization from Context. Orchestrates draw calls by binding Material pipelines and Mesh vertex data into a RenderPass. Does not own raw hardware resources externally or RenderPipelines/shaders. |
| **frame** | Per-frame RAII wrapper around the surface texture and its TextureView. Exists only for the duration of a single rendering pass. |
## Interaction with Other Modules
- **utils**: Context returns WsgError from all fallible methods; Renderer does not use errors directly.
- **pipeline**: Context requires Device reference during pipeline creation in new(); Renderer uses PipelineCache indirectly through Material.
- **resources**: Renderer consumes Mesh and Material instances for draw calls.
- **scene**: Renderer queries Scene for Material/Mesh pairs to render.
+150
View File
@@ -0,0 +1,150 @@
//! # Context Module — Manager Layer (Hardware Lifecycle)
//!
//! The **Manager** layer of the architecture — owns hardware resource lifecycle (Device, Queue, Surface).
//! Initializes the GPU at startup via async Builder pattern; orchestrates frame-by-frame rendering via begin_frame() / end_frame().
//! Does not own rendering logic (that belongs to Renderer) or shader compilation (PipelineCache).
//!
//! ## Interaction with Other Modules
//! - **renderer**: receives Device/Queue references to write rendered output into the TextureView.
//! - **pipeline_cache**: requires Device reference during pipeline creation in Context::new().
//! - **error**: returns WsgError variants from all fallible methods.
//!
//! ## Architecture Notes (per ARCHI_APP.md)
//! - **Phase de Déclaration**: Context is created once at application startup before the render loop begins.
//! This follows the declarative workflow where all GPU state is configured upfront.
//! - **Injection Async**: Context::new() is async because the runtime must be injected at creation time.
//! - **Accès Bas-Niveau**: Advanced users can bypass the Scene facade and manipulate Context directly
//! through App.renderer(), App.context(), etc., for fine-grained control over wgpu handles.
use std::sync::Arc;
use wgpu::{Adapter, Device, Instance, Queue, Surface};
use winit::window::Window;
use crate::utils::WsgError;
use crate::core::Frame;
/// Represents the GPU context. Holds all WGPU objects needed for rendering.
/// Created once at startup and shared across frames via Arc.
pub struct Context {
/// The entry point to wgpu — manages connections with graphics drivers (Vulkan, Metal, DX12).
pub instance: Instance,
/// Rendering target surface linking wgpu to the window (winit).
pub surface: Surface<'static>,
/// Physical or software GPU adapter selected by the user.
pub adapter: Adapter,
/// The engine core — creates buffers, textures, pipelines.
pub device: Device,
/// Command submission queue — drawing commands are sent here for execution.
pub queue: Queue,
}
impl Context {
/// Initializes the WGPU context. Creates the surface from the window, requests a device from the adapter,
/// and stores all required objects (instance, surface, adapter, device, queue).
/// Called once at application startup. Returns an error if GPU initialization fails.
/// Internal steps: 1) create Instance → 2) create Surface bound to Window lifecycle → 3) request_adapter for compatible GPU → 4) request_device for Device + Queue.
pub async fn new(window: Arc<Window>) -> Result<Self, WsgError> {
// WGPU instance
let instance = wgpu::Instance::default();
// Surface (bound to window lifecycle, so unsafe)
let surface = instance
.create_surface(window)
.map_err(|e| WsgError::SurfaceCreation(e))?;
// Request for adapter (GPU)
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
compatible_surface: Some(&surface),
..Default::default()
})
.await
.map_err(|_| WsgError::NoAdapter)?;
// Request for adapter device and queue
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor::default())
.await
.map_err(|_| WsgError::DeviceCreation)?;
Ok(Self {
instance,
surface,
adapter,
device,
queue,
})
}
/// Configures the surface with a render format and alpha mode for rendering.
/// Inputs: adapter (GPU capabilities), width/height (surface resolution).
/// Returns Ok(()) on success or SurfaceIncompatible if no SRGB format + alpha mode exist.
/// Typically called by the renderer when window size changes.
/// Internal steps: 1) get_capabilities(adapter) → 2) find SRGB format (fallback to first available) → 3) select first alpha mode → 4) build SurfaceConfiguration → 5) configure() the surface.
pub fn configure(
&self,
adapter: &wgpu::Adapter,
width: u32,
height: u32,
) -> Result<wgpu::TextureFormat, WsgError> {
let caps = self.surface.get_capabilities(adapter);
let format = caps
.formats
.iter()
.copied()
.find(|f| f.is_srgb())
.or(caps.formats.first().copied())
.ok_or(WsgError::SurfaceIncompatible)?;
let config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format,
width,
height,
present_mode: wgpu::PresentMode::Fifo,
alpha_mode: caps
.alpha_modes
.first()
.copied()
.ok_or(WsgError::SurfaceIncompatible)?,
view_formats: vec![],
color_space: wgpu::SurfaceColorSpace::Srgb,
desired_maximum_frame_latency: 2,
};
self.surface.configure(&self.device, &config);
// Return the chosen format so the Renderer can store it for pipeline creation
Ok(format)
}
/// Acquires the next surface texture for rendering this frame. Returns an error variant
/// describing why acquisition failed (timeout, occlusion, surface lost, etc.).
/// Typically called by the orchestrator (main.rs) at the start of each frame loop iteration.
pub fn begin_frame(&self) -> Result<wgpu::SurfaceTexture, WsgError> {
// In wgpu 30, get_current_texture() returns CurrentSurfaceTexture enum directly (not Result).
// All variants are matched to provide explicit error handling instead of panicking.
match self.surface.get_current_texture() {
wgpu::CurrentSurfaceTexture::Success(texture) => Ok(texture),
wgpu::CurrentSurfaceTexture::Suboptimal(_texture) => Err(WsgError::SuboptimalTexture),
wgpu::CurrentSurfaceTexture::Timeout => Err(WsgError::FrameTimeout),
wgpu::CurrentSurfaceTexture::Occluded => Err(WsgError::Occluded),
wgpu::CurrentSurfaceTexture::Outdated => Err(WsgError::Outdated),
wgpu::CurrentSurfaceTexture::Lost => Err(WsgError::Lost),
wgpu::CurrentSurfaceTexture::Validation => Err(WsgError::Validation),
}
}
/// Presents the rendered frame by submitting the acquired surface texture to the GPU queue.
/// The frame must have been obtained via begin_frame(); calling present() twice on the same
/// texture is undefined behavior. Called by the orchestrator after rendering commands are complete.
pub fn end_frame(&self, frame: wgpu::SurfaceTexture) {
// In wgpu 30, present() moved from SurfaceTexture::present() to Queue::present(frame).
self.queue.present(frame);
}
/// Returns a Frame wrapper around the current surface texture and its TextureView.
/// Called by the orchestrator at frame start; equivalent to Context::begin_frame() + Frame construction.
pub fn get_next_frame(&self) -> Frame {
Frame::new(&self.surface)
}
}
+83
View File
@@ -0,0 +1,83 @@
//! # Frame Module — Per-Frame RAII Wrapper (Surface Texture + View)
//!
//! Defines `Frame`, a per-frame RAII wrapper around the surface texture and its TextureView.
//! A Frame exists only for the duration of a single rendering pass — it is acquired at the start
//! of each frame loop iteration via Context::begin_frame() or Frame::try_new(), used by Renderer
//! to write draw commands into the TextureView, then dropped after Renderer::present() submits it.
//!
//! ## Interaction with Other Modules
//! - **context**: provides the Surface from which Frame acquires the current texture.
//! - **renderer**: passes Frame's TextureView to render() as the color attachment target.
//! - **error**: does not use errors directly; Frame::new() panics on acquisition failure while
//! Frame::try_new() returns Option<Self> for graceful recovery.
//!
//! ## Architecture Notes (per ARCHI_APP.md)
//! - **Phase d'Exécution**: Frame is acquired at the start of each render loop iteration and released after rendering.
//! - **Ergonomie**: Users interact with frames through Renderer::render() + Renderer::present(), not directly with wgpu handles.
pub struct Frame {
/// The GPU surface texture representing the current display buffer to be presented.
pub surface_texture: wgpu::SurfaceTexture,
/// A read-only view into surface_texture, used as the RenderPass color attachment during rendering.
pub view: wgpu::TextureView,
}
impl Frame {
/// Acquires the next surface texture and creates a TextureView over it.
/// Called by the orchestrator (main.rs) at the start of each frame loop iteration.
/// Panics if the surface cannot be acquired (e.g., lost, occluded). For non-panicking
/// alternatives, use try_new(). Internal steps: 1) get_current_texture() → 2) match Success/Suboptimal → 3) create_view.
pub fn new(surface: &wgpu::Surface) -> Self {
// In wgpu 30, get_current_texture() returns CurrentSurfaceTexture enum directly (not Result).
// All variants are matched to provide explicit error handling instead of panicking —
// see Context::begin_frame() for the detailed variant mapping.
match surface.get_current_texture() {
// On Success or Suboptimal, we acquire the SurfaceTexture and create its TextureView
wgpu::CurrentSurfaceTexture::Success(frame)
| wgpu::CurrentSurfaceTexture::Suboptimal(frame) => {
let view = frame
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
Self {
surface_texture: frame,
view,
}
}
// Panicking on failure here is intentional — Frame must exist for rendering to proceed.
// Callers should use try_new() if they prefer Option-based error recovery.
other => panic!("Failed to acquire texture: {:?}", other),
}
}
/// Presents the rendered frame by submitting the acquired surface texture to the GPU queue.
/// The frame must have been obtained via new() or try_new(); calling present() twice on the same
/// texture is undefined behavior. Called after Renderer::render().
pub fn present(self, queue: &wgpu::Queue) {
queue.present(self.surface_texture);
}
/// Attempts to acquire the next surface texture without panicking.
/// Returns Some(Frame) on success (Success/Suboptimal) or None on any error variant.
/// Called when graceful frame skipping is preferred over crashing.
pub fn try_new(surface: &wgpu::Surface) -> Option<Self> {
match surface.get_current_texture() {
wgpu::CurrentSurfaceTexture::Success(frame)
| wgpu::CurrentSurfaceTexture::Suboptimal(frame) => {
let view = frame
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
Some(Self {
surface_texture: frame,
view,
})
}
_ => None,
}
}
/// Returns a reference to the TextureView used as the RenderPass color attachment.
/// Called by Renderer::render() to pass the view into begin_render_pass().
pub fn view(&self) -> &wgpu::TextureView {
&self.view
}
}
+19
View File
@@ -0,0 +1,19 @@
//! # Core Module — Manager and Executor Layers
//!
//! Defines the two architectural layers that drive rendering: **Context** (Manager) owns GPU hardware
//! resource lifecycle (Device, Queue, Surface), and **Renderer** (Executor) orchestrates draw calls by binding
//! Material pipelines and Mesh vertex data into a RenderPass.
//!
//! ## Interaction with Other Modules
//! - `context` consumes errors from `utils`, holds Frame references during frame loops.
//! - `renderer` receives Device/Queue references from Context, uses Materials from `resources`.
//! - `frame` is consumed by both Context (begin_frame → end_frame) and Renderer (render → present).
pub mod context;
pub mod frame;
pub mod renderer;
// Re-exports
pub use context::Context;
pub use frame::Frame;
pub use renderer::Renderer;
+117
View File
@@ -0,0 +1,117 @@
//! # Renderer Module — Executor Layer (WGPU Command Execution)
//!
//! The **Executor** layer of the architecture. Executes WGPU rendering commands — orchestrates draw calls by binding
//! Material pipelines and Mesh vertex buffers into a RenderPass, then submits commands to the GPU queue.
//! Does not own hardware resources (Device, Queue); receives references when called by the orchestrator (main.rs).
//! Does not own RenderPipelines or shaders — those are managed by PipelineCache and accessed through Material.
//! Does not own Surface/TextureView — acquired from Context::begin_frame().
//!
//! ## Interaction with Other Modules
//! - **context**: receives Device/Queue references and TextureView; does not call begin/end_frame itself.
//! - **pipeline_cache**: indirectly via Material — Renderer uses pipelines that PipelineCache compiled.
//! - **mesh**: passes vertex/index buffers into set_vertex_buffer/set_index_buffer during draw.
//! - **material**: provides the RenderPipeline reference via set_pipeline during draw.
//!
//! ## Architecture Notes (per ARCHI_APP.md)
//! - **Phase d'Exécution**: Renderer executes per-frame render loops. During this phase it iterates Scene entities
//! and draws each one by binding the appropriate Material+Mesh pair.
//! - **Performance**: Entity sorting within the render loop minimizes pipeline switches (batching par matériau).
//! - **Accès Bas-Niveau**: Advanced users can bypass Scene and call Renderer directly for custom rendering paths.
use crate::core::Context;
use crate::resources::{Mesh, Material};
use crate::core::Frame;
/// The Executor layer of the architecture. Holds shared references to Device and Queue from Context,
/// plus the surface texture format. Executes WGPU rendering commands by binding Materials and Meshes
/// into RenderPasses during each frame. Does not own raw hardware resources (they are Arc-cloned from Context).
pub struct Renderer {
/// GPU command submission queue — holds an Arc clone from Context; shared with other Context users.
queue: wgpu::Queue,
/// GPU device — creates buffers, textures, pipelines; holds an Arc clone from Context.
device: wgpu::Device,
/// Surface texture output format — stored here so it can be passed to PipelineCache on Material creation.
format: wgpu::TextureFormat,
}
impl Renderer {
/// Creates a Renderer by cloning Device and Queue Arc references from the Context, plus capturing the surface format.
/// Called once at application startup during scene setup. The Renderer shares these resources via Arc;
/// Context retains ownership and can continue using them after this call.
pub fn new(context: &Context, format: wgpu::TextureFormat) -> Self {
Self {
queue: context.queue.clone(),
device: context.device.clone(),
format,
}
}
/// Orchestrates rendering of a single object: binds Material pipeline + Mesh vertex data into a RenderPass,
/// then submits commands to the GPU queue for execution. Called per-frame by the orchestrator (main.rs).
/// Internal steps: 1) create CommandEncoder → 2) begin RenderPass with color attachment →
/// 3) set_pipeline(material.pipeline) → 4) set_vertex_buffer(mesh.vertex_buffer) →
/// 5) draw_indexed or draw based on index buffer presence → 6) drop render_pass end scope →
/// 7) submit encoder via queue.
pub fn render(
&self,
view: &wgpu::TextureView,
mesh: &Mesh,
material: &Material,
) {
// Create per-frame command encoder; its lifetime is scoped to this function only.
let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("render encoder"),
});
// RenderPass borrows encoder mutably — must end (drop) before encoder.finish() below.
// This scope boundary enforces Rust's borrow checker rules for GPU synchronization.
{
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,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
store: wgpu::StoreOp::Store,
},
})],
..Default::default()
});
render_pass.set_pipeline(&material.pipeline);
if mesh.num_vertices > 0 {
render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
} else {
// If no vertices, skip drawing entirely (nothing to render)
return;
}
if let Some(index_buffer) = &mesh.index_buffer {
render_pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint16);
render_pass.draw_indexed(0..mesh.num_indices, 0, 0..1);
} else {
render_pass.draw(0..mesh.num_vertices, 0..1);
}
}
self.queue.submit(std::iter::once(encoder.finish()));
}
/// Presents the rendered frame by submitting the acquired surface texture to the GPU queue.
/// The frame must have been obtained via Context::begin_frame() or Frame::try_new(); calling present()
/// twice on the same texture is undefined behavior. Called by the orchestrator after render().
pub fn present(&self, frame: Frame) {
self.queue.present(frame.surface_texture);
}
/// Returns a reference to the owned Device for direct access when needed (e.g., PipelineCache creation).
pub fn device(&self) -> &wgpu::Device {
&self.device
}
/// Returns the surface texture output format used for rendering.
pub fn format(&self) -> wgpu::TextureFormat {
self.format
}
}
+23
View File
@@ -0,0 +1,23 @@
//! # WSG Library Crate Root
//!
//! The top-level entry point for the wsg-lib crate. Exposes five public modules organized by architectural responsibility:
//! **core** (Manager + Executor layers), **resources** (data types), **pipeline** (shader compilation cache),
//! **scene** (resource graph and entity management), and **utils** (configuration and error handling).
//!
//! ## Module Interaction Map
//! - `core` consumes resources from `resources`, pipelines from `pipeline`, and errors from `utils`.
//! - `scene` aggregates Resources, Materials, and Pipelines into an entity graph.
//! - `utils` is a leaf module — no internal dependencies on other library modules.
//!
//! ## Usage
//! Consumers import through the re-exports defined in each submodule's `mod.rs`:
//! ```ignore
//! use wsg_lib::core::{Context, Renderer};
//! use wsg_lib::resources::{Mesh, Material, Vertex};
//! use wsg_lib::utils::BASIC_SHADER;
//! ```
pub mod core;
pub mod pipeline;
pub mod resources;
pub mod scene;
pub mod utils;
+20
View File
@@ -0,0 +1,20 @@
# Pipeline Module — Shader Compilation Cache
## Overview
The `pipeline` module contains the shader compilation cache that avoids duplicate GPU work by reusing compiled RenderPipelines.
| File | Responsibility |
|------|---------------|
| **pipeline_cache** | PipelineCache — maps (shader_id, format) keys to compiled RenderPipelines. Loads WGSL from disk or falls back to embedded BASIC_SHADER constant. Creates pipelines on-demand via build_pipeline(). |
## Interaction with Other Modules
- **utils::conf**: Provides BASIC_SHADER_PATH (disk path) and BASIC_SHADER (embedded fallback).
- **resources::vertex**: Vertex struct field offsets define the CPU-side layout that build_pipeline() uses as the vertex buffer contract.
- **resources::material**: Material::new() calls get_or_create() during construction to obtain a shared RenderPipeline.
## Technical Points
- Pipelines are stored behind Arc so multiple Materials share the same compiled object without copying.
- wgpu 30 requires `compilation_options` in VertexState/FragmentState and `depth_slice` in color attachments.
+14
View File
@@ -0,0 +1,14 @@
//! # Pipeline Module — Shader Compilation Cache
//!
//! Defines the **PipelineCache**, which owns WGSL shader loading and RenderPipeline creation. It caches compiled
//! pipelines keyed by (shader_id, format) to avoid duplicate GPU work — multiple Materials sharing the same ID
//! share one Arc-wrapped pipeline without copying.
//!
//! ## Interaction with Other Modules
//! - `material` calls `get_or_create()` during its own construction to obtain a shared RenderPipeline.
//! - `conf::BASIC_SHADER` provides fallback WGSL source when an external file is not found.
//! - `vertex::Vertex` defines the CPU-side layout that `build_pipeline()` uses as the vertex buffer contract.
pub mod pipeline_cache;
// Re-exports
pub use pipeline_cache::PipelineCache;
+195
View File
@@ -0,0 +1,195 @@
//! # PipelineCache Module — Translation of resources/ Data Toward GPU Pipelines
//!
//! Defines `PipelineCache`, the library's shader compilation cache. It translates WGSL shader source and resources/ data types
//! into compiled RenderPipelines, storing them in a HashMap keyed by shader_id + texture format to avoid duplicate GPU work.
//! Materials request pipelines through this cache; if a pipeline for the given key exists, it is returned directly
//! via Arc cloning. Otherwise the cache compiles one on-the-fly, caches it, then returns it.
//!
//! ## Interaction with Other Modules
//! - **Material** calls `get_or_create()` during its own construction to obtain a shared RenderPipeline.
//! - **conf::BASIC_SHADER** provides fallback WGSL source when an external file is not found.
//! - **vertex::Vertex** defines the CPU-side layout that `build_pipeline` uses as the vertex buffer contract.
//!
//! ## Technical Points
//! - Pipelines are stored behind `Arc` so multiple Materials share the same compiled object without copying.
//! - wgpu 30 requires `compilation_options` in VertexState/FragmentState and `depth_slice` in color attachments.
//! - **Batching**: Multiple Materials with the same shader_id share one pipeline, enabling material-level batching in Renderer.
use crate::utils::BASIC_SHADER;
use crate::resources::Vertex;
use std::collections::HashMap;
use std::sync::Arc;
/// Shader pipeline cache: maps (shader_id, format) keys to compiled RenderPipelines.
/// Ensures each unique shader+format combination is compiled at most once; subsequent requests return cached instances.
pub struct PipelineCache {
/// Cached pipelines keyed by their shader identifier string. Multiple Materials sharing the same ID share one Arc-wrapped pipeline.
pipelines: HashMap<String, Arc<wgpu::RenderPipeline>>,
/// Maps shader IDs to file paths on disk for WGSL loading in `load_shader()`.
shader_paths: HashMap<String, String>,
}
impl PipelineCache {
/// Creates an empty pipeline cache with no pre-loaded shaders or pipelines.
/// Called at application startup before any Material creation. Shader paths must be registered via register_shader() first.
pub fn new() -> Self {
Self {
pipelines: HashMap::new(),
// Maps shader IDs to file paths on disk for WGSL loading in load_shader().
// When a path exists, it reads from it; otherwise falls back to BASIC_SHADER constant.
shader_paths: HashMap::new(),
}
}
/// Registers an external WGSL shader file path associated with a given ID.
/// Inputs: id (unique key for this shader), path (filesystem path to .wgsl file).
/// Returns Ok(id) on success or Err if the ID is already registered. Called during scene setup to register custom shaders.
pub fn register_shader(&mut self, id: &str, path: &str) -> Result<String, String> {
if self.shader_paths.contains_key(id) {
return Err(format!("ID '{}' already exists.", id));
}
self.shader_paths.insert(id.to_string(), path.to_string());
Ok(id.to_string())
}
/// Unregisters a shader by its ID, removing both the path reference and any cached compiled pipeline.
/// Inputs: id (the shader identifier to remove).
/// Returns Ok(id) on success or Err if the ID does not exist. Called when a shader should be freed from GPU memory.
pub fn unregister_shader(&mut self, id: &str) -> Result<String, String> {
if self.shader_paths.remove(id).is_none() {
return Err(format!("ID '{}' does not exist.", id));
}
// Remove cached pipeline so GPU memory is freed (wgpu drops it automatically)
Ok(id.to_string())
}
/// Retrieves a cached RenderPipeline by shader_id, or creates one on-demand if not present.
/// Inputs: device (GPU command source), format (surface texture format for fragment output),
/// shader_id (unique key into the cache).
/// Returns an Arc-wrapped RenderPipeline ready for rendering. Called by Material::new().
pub fn get_or_create(
&mut self,
device: &wgpu::Device,
format: wgpu::TextureFormat,
shader_id: &str,
) -> Arc<wgpu::RenderPipeline> {
// Step 1: Return cached pipeline if it already exists for this shader_id
if let Some(pipeline) = self.pipelines.get(shader_id) {
return pipeline.clone();
}
// Step 2: Compile a new pipeline — loads shader and builds the GPU render pipeline
let path = self
.shader_paths
.get(shader_id)
.map(|s| s.as_str())
.unwrap_or(shader_id);
let shader = self.load_shader(device, path);
let pipeline = Self::build_pipeline(device, format, &shader);
// Step 3: Cache the new pipeline behind Arc and return it
let pipeline_arc = Arc::new(pipeline);
self.pipelines
.insert(shader_id.to_string(), pipeline_arc.clone());
pipeline_arc
}
/// Loads a WGSL shader module: reads from disk first, falls back to the embedded BASIC_SHADER constant.
/// Called internally by `get_or_create()` when compiling a new pipeline.
fn load_shader(&self, device: &wgpu::Device, path: &str) -> wgpu::ShaderModule {
let source = std::fs::read_to_string(path).unwrap_or_else(|_| {
println!("Shader not found: {}, falling back to default", path);
BASIC_SHADER.to_string()
});
device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some(path),
source: wgpu::ShaderSource::Wgsl(source.into()),
})
}
/// Builds a RenderPipeline from a shader module, device, and surface texture format.
/// Inputs: device (GPU command source), format (output texture format), shader (compiled WGSL module).
/// Returns a fully configured RenderPipeline ready for draw calls. Called internally by `get_or_create()`.
fn build_pipeline(
device: &wgpu::Device,
format: wgpu::TextureFormat,
shader: &wgpu::ShaderModule,
) -> wgpu::RenderPipeline {
// Define vertex attribute layout — the contract between CPU vertex data and GPU shader inputs.
// Must match Vertex struct field offsets exactly.
let vertex_buffer_layout = wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[
wgpu::VertexAttribute {
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x3,
}, // position
wgpu::VertexAttribute {
offset: 12,
shader_location: 1,
format: wgpu::VertexFormat::Float32x3,
}, // normal
wgpu::VertexAttribute {
offset: 24,
shader_location: 2,
format: wgpu::VertexFormat::Float32x2,
}, // uv
wgpu::VertexAttribute {
offset: 32,
shader_location: 3,
format: wgpu::VertexFormat::Float32x4,
}, // color
],
};
// Pipeline layout — defines bind group bindings (empty here; no uniform buffers used).
// 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<immediate> used
});
// Create the full RenderPipeline — vertex state + fragment state + primitive configuration.
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 explicitly, None for auto-detection.
entry_point: Some("vs_main"),
compilation_options: Default::default(), // 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(), // required field in wgpu 30
// targets is now &[Option<ColorTargetState>] — each 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 fields in wgpu 30.
multiview_mask: None,
cache: None,
})
}
/// Retrieves a cached RenderPipeline by shader_id without creating one.
/// Inputs: shader_id (unique key into the cache).
/// Returns Some(Arc<RenderPipeline>) if found, None otherwise. Called by renderer code for pipeline inspection.
pub fn get(&self, shader_id: &str) -> Option<&Arc<wgpu::RenderPipeline>> {
self.pipelines.get(shader_id)
}
}
+17
View File
@@ -0,0 +1,17 @@
# Resources Module — Data Types
## Overview
The `resources` module defines three immutable data types that flow through the rendering pipeline. These are created once during scene initialization and consumed by Renderer for draw calls every frame.
| File | Responsibility |
|------|---------------|
| **vertex** | Vertex struct — CPU-side per-attribute tuple (position [f32;3], normal [f32;3], uv [f32;2], color [f32;4]). Must match PipelineCache::build_pipeline() vertex buffer layout byte-for-byte. |
| **mesh** | Mesh struct — persistent GPU geometry container with vertex_buffer (wgpu::Buffer), optional index_buffer, and draw call counters. Created via Mesh::new() which uploads data from CPU to GPU buffers. |
| **material** | Material struct — lightweight appearance descriptor pairing shader_id with a shared RenderPipeline Arc. Multiple Materials referencing the same shader_id point to the identical compiled GPU pipeline. |
## Interaction with Other Modules
- **pipeline**: build_pipeline() reads Vertex field offsets to construct VertexBufferLayout attributes array.
- **scene**: Scene stores Arc<Mesh> and Arc<Material> instances keyed by identifier strings.
- **utils**: Mesh creation uses BASIC_SHADER fallback when external shader files are missing.
+42
View File
@@ -0,0 +1,42 @@
//! # Material Module — Appearance Descriptor (shader_id → RenderPipeline)
//!
//! Defines `Material`, a lightweight appearance descriptor that pairs a shader identifier with
//! a shared RenderPipeline. Materials are created via PipelineCache to ensure pipeline reuse—
//! multiple materials referencing the same shader_id point to the identical compiled GPU pipeline.
//!
//! ## Architecture Notes (per ARCHI_APP.md)
//! - **Identifiants**: Each Material is registered in Scene by string identifier, enabling dynamic access
//! during the render loop without borrow checker issues. The shader_id serves as the Handle<T> key.
//! - **Phase de Déclaration**: Materials are instantiated once in the declarative phase before the render loop begins.
use crate::pipeline::PipelineCache;
use std::sync::Arc;
/// Lightweight appearance descriptor: links a shader ID to a shared RenderPipeline.
/// Does not own the pipeline; holds an Arc for zero-copy sharing across objects using the same shader.
pub struct Material {
/// Unique shader identifier used to look up or create a compiled RenderPipeline in PipelineCache.
pub shader_id: String,
/// Shared reference to the compiled GPU render pipeline. Multiple Materials can share one through Arc cloning.
pub pipeline: Arc<wgpu::RenderPipeline>,
}
impl Material {
/// Creates a new Material by requesting the cache to provide (or create) its RenderPipeline.
/// Inputs: device (GPU command source for pipeline creation), format (surface texture format),
/// shader_id (unique key into PipelineCache), cache (mutable ref for potential insertion).
/// Returns a Material holding the Arc-wrapped pipeline. Called at scene initialization time.
pub fn new(
device: &wgpu::Device,
format: wgpu::TextureFormat,
shader_id: &str,
cache: &mut PipelineCache,
) -> Self {
// Request pipeline from cache — returns cached instance if already exists, creates new otherwise
let pipeline = cache.get_or_create(device, format, shader_id);
Self {
shader_id: shader_id.to_string(),
pipeline,
}
}
}
+57
View File
@@ -0,0 +1,57 @@
//! # Mesh Module — Persistent GPU Geometry Container
//!
//! Defines `Mesh`, a persistent GPU geometry container. Mesh data is uploaded to the GPU once at creation time
//! and remains valid across all frames until dropped. It holds no rendering knowledge—only raw geometric data.
//!
//! ## Architecture Notes (per ARCHI_APP.md)
//! - **Identifiants**: Each Mesh is registered in Scene by string identifier, enabling dynamic access
//! during the render loop without borrow checker issues. The identifier serves as the Handle<T> key.
//! - **Phase de Déclaration**: Meshes are instantiated once in the declarative phase before the render loop begins.
//! - **Performance**: Multiple entities can reference the same Mesh, reducing memory footprint for repeated geometry.
use crate::resources::vertex::Vertex;
use wgpu::util::DeviceExt;
/// Persistent GPU geometry: vertex positions, optional indices, and draw call counters.
/// Created once via `Mesh::new()` during scene setup; referenced by Renderer for every frame.
pub struct Mesh {
/// GPU buffer containing vertex attribute data (position, UV, color).
pub vertex_buffer: wgpu::Buffer,
/// Optional GPU buffer for indexed drawing. Present when the mesh uses index-based rendering instead of simple vertex iteration.
pub index_buffer: Option<wgpu::Buffer>,
/// Number of vertices in the mesh. Used as `0..num_vertices` for non-indexed draws.
pub num_vertices: u32,
/// Number of indices in the index buffer. Used as `0..num_indices` for indexed draws.
pub num_indices: u32,
}
impl Mesh {
/// Creates a new Mesh by uploading vertex and optional index data to GPU buffers.
/// Inputs: device (GPU command source), vertices (CPU-side vertex array), indices (optional CPU-side index array).
/// Returns a Mesh with two GPU buffers ready for rendering. Called at scene initialization time only.
pub fn new(device: &wgpu::Device, vertices: &[Vertex], indices: Option<&[u16]>) -> Self {
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Mesh Vertex Buffer"),
contents: bytemuck::cast_slice(vertices),
usage: wgpu::BufferUsages::VERTEX,
});
// Create optional index buffer and count indices if provided
let (index_buffer, num_indices) = if let Some(data) = indices {
let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Mesh Index Buffer"),
usage: wgpu::BufferUsages::INDEX,
contents: bytemuck::cast_slice(data),
});
(Some(buffer), data.len() as u32)
} else {
(None, 0)
};
Self {
vertex_buffer,
index_buffer,
num_vertices: vertices.len() as u32,
num_indices,
}
}
}
+20
View File
@@ -0,0 +1,20 @@
//! # Resources Module — Data Types
//!
//! Defines the three core data types that flow through the rendering pipeline: **Vertex** (CPU-side per-attribute
//! tuple), **Mesh** (GPU geometry container with vertex/index buffers), and **Material** (appearance descriptor
//! pairing shader ID with a compiled RenderPipeline). These are immutable after creation and consumed by Renderer
//! for draw calls.
//!
//! ## Interaction with Other Modules
//! - `pipeline_cache::build_pipeline()` reads Vertex field offsets to construct the vertex buffer layout.
//! - `mesh::new()` uploads Vertex arrays from CPU memory into GPU vertex buffers via DeviceExt::create_buffer_init().
//! - `material::new()` requests RenderPipelines from PipelineCache during scene initialization.
pub mod material;
pub mod mesh;
pub mod vertex;
// Re-exports
pub use material::Material;
pub use mesh::Mesh;
pub use vertex::Vertex;
+45
View File
@@ -0,0 +1,45 @@
//! # Vertex Module — CPU-Side Per-Attribute Tuple (GPU Contract)
//!
//! Defines `Vertex`, the CPU-side data layout that is sent to the GPU as vertex attribute buffers.
//! The struct's field order and offsets must exactly match the shader input attributes defined in
//! PipelineCache::build_pipeline() — any mismatch will corrupt GPU rendering output.
//!
//! ## Technical Points
//! - `#[repr(C)]` ensures fields are laid out contiguously without Rust padding reordering, matching C ABI.
//! - `bytemuck::Pod + bytemuck::Zeroable` enables safe `cast_slice()` conversion for GPU buffer uploads.
//! - **Performance**: The 56-byte stride per vertex is the contract between CPU data and GPU shader inputs;
//! PipelineCache::build_pipeline() reads this layout to construct VertexBufferLayout attributes array.
/// Per-vertex attribute tuple: position (3D), normal (3D), texture coordinate (2D), color (RGBA).
/// Must match the vertex buffer layout in PipelineCache::build_pipeline() byte-for-byte.
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct Vertex {
/// XYZ coordinates of this vertex in world space. Offset: 0 bytes.
pub position: [f32; 3],
/// XYZ coordinates of the vertex normal. Offset: 12 bytes.
pub normal: [f32; 3],
/// UV texture coordinates. Offset: 24 bytes
pub uv: [f32; 2],
/// RGBA color values. Offset: 32 bytes.
pub color: [f32; 4],
}
impl Default for Vertex {
// Default values for stability
fn default() -> Self {
Self {
// Default position at center (0, 0)
position: [0.0, 0.0, 0.0],
// Normal pointing upward (standard for lighting calculations)
normal: [0.0, 1.0, 0.0],
// UV coordinates at the origin of the texture
uv: [0.0, 0.0],
// Opaque white color by default
color: [1.0, 1.0, 1.0, 1.0],
}
}
}
+20
View File
@@ -0,0 +1,20 @@
# Scene Module — Resource Depot and Entity Graph
## Overview
The `scene` module defines Scene, the declarative layer of the WSG architecture. Users register resources (Meshes, Materials) by identifier before the render loop starts, then associate entities via labels. At runtime, Scene provides immutable access to these resources without exposing raw wgpu handles.
| File | Responsibility |
|------|---------------|
| **scene** | Scene struct — resource depot storing Meshes and Materials keyed by string identifiers, plus entity graph mapping labels to (mesh_id, material_id) pairs for rendering iteration. |
## Interaction with Other Modules
- **core::renderer**: Renderer queries Scene for Material/Mesh pairs during frame rendering; Context does not interact directly.
- **pipeline**: PipelineCache creates Materials keyed by shader_id; Scene stores references to those Materials.
- **resources**: Scene owns Arc<Mesh> and Arc<Material> instances; Vertex is only used at Mesh creation time.
- **utils**: Scene uses WsgError if resource registration fails.
## Architecture Note
Per ARCHI_APP_FACADE.md, Scene is one half of the "App" facade pattern. It enables a declarative workflow where all resources are declared before the render loop begins, while keeping the freedom to build the engine "brick by brick" through direct Context/PipelineCache/Renderer manipulation.
+22
View File
@@ -0,0 +1,22 @@
//! # Scene Module — Resource Depot and Entity Graph
//!
//! Defines `Scene`, the repository for resources (Mesh, Material) and the graph of entity associations between them.
//! Scene is the declarative layer of the architecture: users register Meshes and Materials by identifier before
//! the render loop starts, then associate entities via labels during initialization. At runtime, Scene provides
//! immutable access to these resources without exposing raw wgpu handles.
//!
//! ## Interaction with Other Modules
//! - **core**: Renderer queries Scene for Material/Mesh pairs to draw; Context does not interact directly.
//! - **pipeline**: PipelineCache creates Materials keyed by shader_id; Scene stores references to those Materials.
//! - **resources**: Scene owns Mesh and Material instances; Vertex is only used at Mesh creation time.
//! - **utils**: Scene uses WsgError if resource registration fails.
//!
//! ## Architecture Note
//! Per ARCHI_APP_FACADE.md, Scene is one half of the "App" facade pattern. It enables a declarative workflow where
//! all resources are declared before the render loop begins, while keeping the freedom to build the engine
//! "brick by brick" through direct Context/PipelineCache/Renderer manipulation.
pub mod scene;
// Re-export
pub use scene::Scene;
+118
View File
@@ -0,0 +1,118 @@
//! # Scene Module — Resource Depot and Entity Graph (per ARCHI_APP.md)
//!
//! Defines `Scene`, the declarative layer of the WSG architecture. Users register resources (Meshes, Materials) by identifier before
//! the render loop starts, then associate entities via labels. At runtime, Scene provides immutable access to these resources without exposing raw wgpu handles.
//!
//! ## Architecture Notes (per ARCHI_APP.md)
//! - **La Recette**: Scene is central to the "App" facade workflow. In the Phase de Déclaration, users call add_mesh(), add_material(), and add_entity()
//! to build the resource depot. During Phase d'Exécution, Renderer iterates Scene entities for rendering.
//! - **Identifiants**: All resource registration uses string identifiers (Handle<T>/String pattern), guaranteeing memory safety
//! and avoiding borrow checker issues during dynamic updates.
//! - **Ergonomie**: Users interact only with entity-level operations (add/remove/get) rather than wgpu buffers/pipelines directly.
use crate::resources::{Material, Mesh};
use std::collections::HashMap;
use std::sync::Arc;
/// Resource depot and entity graph. Stores Meshes and Materials keyed by identifier strings,
/// and maps entity labels to their associated mesh+material pairs for rendering iteration.
/// Created once during application setup; entities are added before the render loop starts.
pub struct Scene {
/// Map of mesh identifiers to owned Mesh instances. Populated via `add_mesh()`.
meshes: HashMap<String, Arc<Mesh>>,
/// Map of material identifiers to owned Material instances. Populated via `add_material()`.
materials: HashMap<String, Arc<Material>>,
/// Map of entity labels to (mesh_id, material_id) associations. Populated via `add_entity()`.
entities: HashMap<String, (String, String)>,
}
impl Scene {
/// Creates an empty scene with no registered resources or entities.
/// Called at application startup before any resource registration.
pub fn new() -> Self {
Self {
meshes: HashMap::new(),
materials: HashMap::new(),
entities: HashMap::new(),
}
}
/// Registers a Mesh in the scene under a unique identifier.
/// Inputs: id (unique key), mesh (Mesh instance). Returns Ok(id) on success or Err if already exists.
/// Called during scene initialization when building the resource depot.
pub fn add_mesh(&mut self, id: &str, mesh: Arc<Mesh>) -> Result<String, String> {
if self.meshes.contains_key(id) {
return Err(format!("Mesh ID '{}' already exists.", id));
}
self.meshes.insert(id.to_string(), mesh);
Ok(id.to_string())
}
/// Registers a Material in the scene under a unique identifier.
/// Inputs: id (unique key), material (Material instance). Returns Ok(id) on success or Err if already exists.
/// Called during scene initialization when building the resource depot.
pub fn add_material(&mut self, id: &str, material: Arc<Material>) -> Result<String, String> {
if self.materials.contains_key(id) {
return Err(format!("Material ID '{}' already exists.", id));
}
self.materials.insert(id.to_string(), material);
Ok(id.to_string())
}
/// Associates an entity label with a mesh and material pair for rendering iteration.
/// Inputs: label (entity identifier), mesh_id (key into meshes map), material_id (key into materials map).
/// Returns Ok(label) on success or Err if either referenced resource does not exist.
/// Called during scene initialization to build the renderable entity graph.
pub fn add_entity(
&mut self,
label: &str,
mesh_id: &str,
material_id: &str,
) -> Result<String, String> {
if !self.meshes.contains_key(mesh_id) {
return Err(format!("Mesh '{}' does not exist.", mesh_id));
}
if !self.materials.contains_key(material_id) {
return Err(format!("Material '{}' does not exist.", material_id));
}
self.entities.insert(label.to_string(), (mesh_id.to_string(), material_id.to_string()));
Ok(label.to_string())
}
/// Retrieves a Mesh by its registered identifier.
/// Called by Renderer during frame rendering to obtain vertex data for draw calls.
pub fn get_mesh(&self, id: &str) -> Option<&Arc<Mesh>> {
self.meshes.get(id)
}
/// Retrieves a Material by its registered identifier.
/// Called by Renderer during frame rendering to obtain pipeline reference for draw calls.
pub fn get_material(&self, id: &str) -> Option<&Arc<Material>> {
self.materials.get(id)
}
/// Iterates all entity associations, yielding (label, mesh_ref, material_ref) triples.
/// Called by the orchestrator during each render pass to draw every entity in order.
pub fn iter_entities(
&self,
) -> impl Iterator<Item = (&str, &Arc<Mesh>, &Arc<Material>)> + '_ {
self.entities.iter().map(|(label, (mesh_id, mat_id))| {
let mesh = self.meshes.get(mesh_id).unwrap(); // safe: add_entity validates existence
let mat = self.materials.get(mat_id).unwrap(); // same invariant
(label.as_str(), mesh, mat)
})
}
/// Removes an entity from the graph without freeing its underlying resources.
/// The referenced Mesh and Material remain registered; only the association is dropped.
/// Called during dynamic updates when an entity should be hidden or removed temporarily.
pub fn remove_entity(&mut self, label: &str) -> bool {
self.entities.remove(label).is_some()
}
/// Returns the number of registered entities in this scene.
/// Called for diagnostic logging or culling decisions (e.g., skip rendering empty scenes).
pub fn entity_count(&self) -> usize {
self.entities.len()
}
}
+21
View File
@@ -0,0 +1,21 @@
# Shaders Directory
## Overview
Contains WGSL shader source files used by the PipelineCache module. These are loaded at runtime from disk when referenced by their registered ID in PipelineCache.register_shader(). If a file is missing, PipelineCache falls back to the embedded BASIC_SHADER constant defined in utils::conf.
| File | Purpose |
|------|---------|
| **basic_shader.wgsl** | Default vertex/fragment shader pair (vs_main / fs_main entry points) with position, normal, UV, and color attributes matching the Vertex struct layout. |
## Shader Contract
The WGSL shader must define:
- `@vertex fn vs_main() -> @builtin(position) vec4<f32>` — vertex entry point
- `@fragment fn fs_main() -> @location(0) vec4<f32>` — fragment entry point writing RGBA output
- Vertex input attributes matching the 56-byte stride of resources::Vertex:
- `@location(0)` → position `[f32; 3]` (offset 0)
- `@location(1)` → normal `[f32; 3]` (offset 12)
- `@location(2)` → uv `[f32; 2]` (offset 24)
- `@location(3)` → color `[f32; 4]` (offset 32)
+24
View File
@@ -0,0 +1,24 @@
//! # Basic Shader Module
struct VertexInput {
@location(0) position: vec3<f32>,
@location(1) uv: vec2<f32>,
@location(2) color: vec3<f32>,
};
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) color: vec3<f32>,
};
@vertex
fn vs_main(model: VertexInput) -> VertexOutput {
var out: VertexOutput;
out.clip_position = vec4<f32>(model.position, 1.0);
out.color = model.color; // On transmet la couleur au fragment shader
return out;
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
return vec4<f32>(in.color, 1.0);
}
+16
View File
@@ -0,0 +1,16 @@
# Utils Module — Configuration and Error Handling
## Overview
The `utils` module defines two leaf concepts that other modules consume but have no internal dependencies on. As a leaf module, it does not import from any other library submodule.
| File | Responsibility |
|------|---------------|
| **conf** | Shared constants for shader paths (BASIC_SHADER_PATH) and embedded WGSL source code (BASIC_SHADER). Centralized here so all submodules import from one place instead of duplicating literal strings. Enables PipelineCache to fall back to an embedded default shader when the file-based one is missing. |
| **error** | WsgError enum — application-level error type mapping specific wgpu failure modes to user-friendly messages via thiserror. Every variant maps a GPU initialization or rendering failure to a recoverable or fatal outcome. |
## Interaction with Other Modules
- **pipeline::pipeline_cache**: load_shader() reads BASIC_SHADER_PATH from disk; falls back to BASIC_SHADER if unreadable.
- **core::context**: Returns WsgError variants from all fallible methods (new, configure, begin_frame).
- **core::renderer**: Does not use errors directly — panics on invalid state rather than returning Result.
+16
View File
@@ -0,0 +1,16 @@
//! # Configuration Module
//!
//! Holds shared constants for the WSG library — primarily shader paths and embedded WGSL source code.
//! Centralized here so all submodules import from one place instead of duplicating literal strings.
//! This enables the PipelineCache to fall back to an embedded default shader when the file-based one is missing.
//!
//! ## Interaction with Other Modules
//! - **pipeline_cache::load_shader()** reads `BASIC_SHADER_PATH` from disk; if unreadable, falls back to `BASIC_SHADER`.
//! - Both constants are compile-time values (`include_str!`) ensuring the fallback shader is always available even without external files.
/// Path to the default WGSL shader file on disk (runtime). Used by PipelineCache::load_shader() for file-based loading.
pub const BASIC_SHADER_PATH: &str = "assets/shaders/basic_shader.wgsl";
/// The basic WGSL shader source code, embedded at compile time via `include_str!`.
/// Serves as a fallback when `BASIC_SHADER_PATH` cannot be read at runtime.
pub const BASIC_SHADER: &str = include_str!("../shaders/basic_shader.wgsl");
+84
View File
@@ -0,0 +1,84 @@
//! # Error Module
//!
//! Defines `WsgError`, the application-level error type for all WGPU operations in Context and Renderer methods.
//! Every variant maps a specific failure mode to a user-friendly message via `thiserror`. Errors propagate up through
//! Context's GPU initialization/rendering flow back to the orchestrator (main.rs), which handles them by skipping frames,
//! reconfiguring surfaces, or crashing gracefully.
//!
//! ## Interaction with Other Modules
//! - **context** uses WsgError as return types for `new()`, `configure()`, and `begin_frame()`.
//! - **renderer** does not use errors directly (render panics on invalid state rather than returning Result).
use thiserror::Error;
/// Application-level errors for the WSG library.
/// Every variant maps a specific failure mode during GPU initialization or rendering
/// to a user-friendly message via `thiserror`.
#[derive(Error, Debug)]
pub enum WsgError {
/// The windowing system (winit) failed to create the surface — e.g. no display available.
/// Caller: `Context::new()` after `Instance::create_surface()`.
#[error("Window system error (winit)")]
WindowSystem,
/// No compatible graphics adapter was found for the given surface.
/// This can happen if no Vulkan/Metal/DX12 backend is installed or if the integrated GPU
/// is not exposed to the process.
/// Caller: `Context::new()` after `Instance::request_adapter()`.
#[error("No graphics adapter found")]
NoAdapter,
/// The requested device could not be obtained from the adapter — typically a driver bug
/// or insufficient capabilities.
/// Caller: `Context::new()` after `Adapter::request_device()`.
#[error("Failed to create WGPU device")]
DeviceCreation,
/// A shader module failed to compile or link. The inner string holds the compiler output.
/// Caller: renderer code that creates shaders via `Device::create_shader_module()`.
#[error("Shader compilation or creation error: {0}")]
ShaderError(String),
/// An internal WGPU error propagated from a device request failure.
/// Automatically converted via `thiserror`'s `#[from]` from `wgpu::RequestDeviceError`.
/// Caller: `Context::new()` — maps the wgpu error into this variant automatically.
#[error("Internal WGPU error: {0}")]
InternalWgpu(#[from] wgpu::RequestDeviceError),
/// The OS-level surface could not be created for the given window.
/// Common causes: unsupported display server, window closed before surface creation,
/// or platform-specific limitation.
/// Caller: `Context::new()` after `Instance::create_surface()`.
#[error("Failed to create rendering surface")]
SurfaceCreation(wgpu::CreateSurfaceError),
/// The surface is incompatible — no SRGB texture format and alpha mode are available on the device.
/// Caller: `Context::configure()` when selecting a render format/alpha mode from surface capabilities.
#[error("No compatible render format or alpha mode found for the surface")]
SurfaceIncompatible,
/// A timeout was encountered while acquiring a surface texture. Skip this frame and retry. Caller: `Context::begin_frame()`.
#[error("Frame acquisition timed out")]
FrameTimeout,
/// The window is occluded (minimized or behind another window). Skip until visible. Caller: `Context::begin_frame()`.
#[error("Window is occluded")]
Occluded,
/// The underlying surface changed — call configure() before retrying. Caller: `Context::begin_frame()` on surface mismatch.
#[error("Surface configuration outdated; reconfigure required")]
Outdated,
/// The surface has been lost and needs to be recreated. Caller: `Context::begin_frame()` on surface loss.
#[error("Surface lost")]
Lost,
/// A validation error inside get_current_texture() was raised. Caller: `Context::begin_frame()`.
#[error("Validation error during frame acquisition")]
Validation,
/// Successfully acquired the surface texture but it no longer matches the surface properties.
/// Reconfigure recommended for optimal performance. Caller: `Context::begin_frame()` on suboptimal acquire.
#[error("Acquired suboptimal surface texture; reconfigure recommended")]
SuboptimalTexture,
}
+18
View File
@@ -0,0 +1,18 @@
//! # Utils Module — Configuration and Error Handling
//!
//! Defines two leaf concepts: **conf** (shared constants for shader paths and embedded WGSL source) and
//! **error** (WsgError, the application-level error type mapping wgpu failure modes to user-friendly messages).
//! Both are consumed by other modules but have no internal dependencies on them.
//!
//! ## Interaction with Other Modules
//! - `pipeline_cache` loads shaders from disk using conf::BASIC_SHADER_PATH; falls back to BASIC_SHADER.
//! - `context` returns WsgError variants from all fallible methods (new, configure, begin_frame).
//! - `renderer` does not use errors directly (panics on invalid state rather than returning Result).
pub mod conf;
pub mod error;
// Re-exports
pub use conf::BASIC_SHADER;
pub use conf::BASIC_SHADER_PATH;
pub use error::WsgError;