This commit is contained in:
Jérôme Bousquié
2026-07-08 15:46:50 +02:00
parent 724b658896
commit 7b25564483
25 changed files with 301 additions and 339 deletions
+1 -1
View File
@@ -13,4 +13,4 @@ thiserror = "2"
bytemuck = { version = "1.25.0", features = ["derive"] }
[dev-dependencies]
pollster = "0.4.0"
pollster = { version="0.4.0", features = ["macro"] }
+3 -14
View File
@@ -1,6 +1,6 @@
use wsg_lib::app::{App, AppHandler};
use wsg_lib::resources::{Material, Mesh, Vertex};
use wsg_lib::utils;
use wsg_lib::{App, AppHandler};
// 1. On définit notre "Jeu" qui implémente le comportement
struct MonQuad {
@@ -9,24 +9,13 @@ struct MonQuad {
}
impl AppHandler for MonQuad {
fn render(&mut self, app: &mut App) {
// Le rendu devient simple : on accède aux outils via &mut app
if let Some(frame) = app.context.get_next_frame() {
app.renderer
.render(frame.view(), &self.mesh, &self.material);
app.renderer.present(frame);
}
}
fn render(&mut self, app: &mut App) {}
}
#[pollster::main]
async fn main() -> Result<(), wsg_lib::utils::WsgError> {
// 2. Initialisation via le Builder
let mut app = App::builder()
.title("Exemple Simple")
.size(800, 600)
.build()
.await?;
let mut app = App::new().await?;
// 3. Setup des ressources (déclaration)
app.cache
+4 -2
View File
@@ -2,7 +2,7 @@
## 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:
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 seven modules:
| Module | Responsibility |
|--------|---------------|
@@ -11,12 +11,14 @@ This is the source tree for `wsg-lib`, a Rust library wrapping [wgpu](https://gi
| **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 |
| **app** | App facade — high-level application orchestration with window lifecycle, event loop, and render automation |
| **handler** | AppHandler trait — user-defined game logic interface injected into the render loop |
## 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).
- **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](../../docs/ARCHI_APP.md).
- **Manual**: Manipulate Context, Renderer, and PipelineCache directly for fine-grained control.
## Dependency Flow
+53 -1
View File
@@ -1,3 +1,18 @@
//! # App Facade Module — High-Level Application Orchestration
//!
//! Defines the `App` facade type and `AppHandler` trait that provide the high-level user-facing API.
//! `App` encapsulates window lifecycle, event loop, frame acquisition, and rendering automation.
//! Users implement `AppHandler` to inject their game logic into the render loop without touching wgpu directly.
//! The `AppBuilder` provides a builder-style constructor for creating configured `App` instances.
//!
//! ## Interaction with Other Modules
//! - **core::context**: Consumes GPU hardware lifecycle via Context; acquires frames for rendering.
//! - **core::renderer**: Delegates draw call execution to Renderer per frame.
//! - **pipeline::pipeline_cache**: Holds PipelineCache instance for shader/pipeline management.
//! - **scene::scene**: Exposes Scene as mutable field so users can register resources and entities.
//! - **utils::conf**: Provides default window title, dimensions, and embedded WGSL source.
//! - **handler**: Defines the AppHandler trait that users implement for custom logic.
use crate::AppHandler;
use crate::core::{Context, Renderer};
use crate::pipeline::PipelineCache;
@@ -8,16 +23,33 @@ use std::sync::Arc;
use winit::event_loop::EventLoop;
use winit::window::Window;
/// High-level application facade that orchestrates window lifecycle, event loop, and rendering automation.
/// Encapsulates all five WGPU objects (Instance, Surface, Adapter, Device, Queue) plus the render loop.
/// Users create an App via AppBuilder, then run it with their implementation of AppHandler.
pub struct App {
/// GPU hardware context — owns Instance, Surface, Adapter, Device, Queue lifecycle.
pub context: Context,
/// Executor layer — binds Materials and Meshes into RenderPasses during draw calls.
pub renderer: Renderer,
/// Winit event loop for window management. Set to None after run() consumes it.
pub event_loop: Option<EventLoop<()>>, // On met en Option pour pouvoir faire .take() facilement
/// Shader compilation cache — manages RenderPipelines keyed by shader_id.
pub cache: PipelineCache,
/// Resource depot and entity graph — users register Meshes/Materials here before the render loop begins.
pub scene: Scene,
/// The OS-level window backing this application. Shared via Arc for multi-owner access.
pub window: Arc<Window>,
}
impl App {
/// Runs the application's main loop: processes events, updates logic per frame, renders, and presents.
/// Inputs: handler — user-provided AppHandler implementation containing game logic.
/// Returns Ok(()) on success or Err(WsgError::WindowSystem) if the event loop exits abnormally.
/// Called once at application entry point; runs until the window is closed or an error occurs.
/// Internal steps: 1) take EventLoop from Option → 2) enter winit event loop →
/// 3a) on AboutToWait: call handler.update() + request_redraw →
/// 3b) on RedrawRequested: acquire frame → call handler.render() → present frame →
/// 3c) on CloseRequested: exit event loop.
pub fn run<H: AppHandler + 'static>(mut self, mut handler: H) -> Result<(), WsgError> {
// On extrait l'event_loop de manière sûre grâce au Option
let event_loop = self.event_loop.take().ok_or(WsgError::WindowSystem)?; // Erreur si déjà pris
@@ -33,8 +65,13 @@ impl App {
event: winit::event::WindowEvent::RedrawRequested,
..
} => {
// render logic
// Rendering logic
let frame = self.context.get_next_frame();
// On appelle le render() de l'utilisateur
handler.render(&mut self);
// On présente automatiquement
self.renderer.present(frame);
}
winit::event::Event::WindowEvent {
event: winit::event::WindowEvent::CloseRequested,
@@ -49,13 +86,20 @@ impl App {
}
}
/// Builder for constructing a configured `App` instance with custom title and dimensions.
/// Provides a fluent API for setting window properties before building the full application context.
pub struct AppBuilder {
/// Window title displayed in the OS taskbar/window decorations.
title: String,
/// Window width in pixels.
width: u32,
/// Window height in pixels.
height: u32,
}
impl AppBuilder {
/// Creates an AppBuilder with default values: "WSG App" title, 800x600 resolution.
/// Called as the entry point of the builder pattern — always start here.
pub fn new() -> Self {
Self {
title: APP_DEFAULT_TITLE.to_string(),
@@ -63,15 +107,23 @@ impl AppBuilder {
height: APP_DEFAULT_HEIGHT,
}
}
/// Sets the window title to display in the OS taskbar/window decorations.
/// Inputs: title (string reference). Returns Self for method chaining.
pub fn title(mut self, title: &str) -> Self {
self.title = title.to_string();
self
}
/// Sets the window dimensions in pixels.
/// Inputs: width (pixel count), height (pixel count). Returns Self for method chaining.
pub fn size(mut self, width: u32, height: u32) -> Self {
self.width = width;
self.height = height;
self
}
/// Builds the configured `App` instance by creating all required components in order:
/// EventLoop → Window → Context → Renderer → PipelineCache → Scene.
/// Returns Ok(App) on success or Err(WsgError) if any component fails during creation.
/// Called after setting desired properties via the builder pattern; triggers async GPU initialization.
pub async fn build(self) -> Result<App, WsgError> {
let event_loop = EventLoop::new().unwrap();
let window = Arc::new(
+12 -8
View File
@@ -20,8 +20,8 @@ use std::sync::Arc;
use wgpu::{Adapter, Device, Instance, Queue, Surface};
use winit::window::Window;
use crate::utils::WsgError;
use crate::core::Frame;
use crate::utils::WsgError;
/// Represents the GPU context. Holds all WGPU objects needed for rendering.
/// Created once at startup and shared across frames via Arc.
@@ -41,8 +41,11 @@ pub struct Context {
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.
/// Inputs: window (owned Arc reference to winit Window, provides display surface binding).
/// Returns Ok(Context) on success or Err(WsgError) describing why initialization failed.
/// Called once at application startup before any rendering occurs.
/// 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();
@@ -77,10 +80,11 @@ impl Context {
}
/// 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.
/// Inputs: adapter (GPU capabilities), width/height (surface resolution in pixels).
/// Returns Ok(format) with the chosen texture format or Err(SurfaceIncompatible) if no valid config exists.
/// Typically called by the renderer when window size changes. Called once during AppBuilder::build().
/// 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,
@@ -119,7 +123,7 @@ impl Context {
/// 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.
/// Called by the orchestrator (App::run) 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.
+6 -3
View File
@@ -24,9 +24,11 @@ pub struct Frame {
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.
/// Inputs: surface (borrowed reference to wgpu Surface providing access to display buffers).
/// Returns a new Frame instance. Panics if the surface cannot be acquired (e.g., lost, occluded).
/// Called by the orchestrator (App::run) at the start of each frame loop iteration.
/// Internal steps: 1) get_current_texture() → 2) match Success/Suboptimal variants →
/// 3) create_view on texture → 4) construct Frame with both fields.
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 —
@@ -57,6 +59,7 @@ impl Frame {
}
/// Attempts to acquire the next surface texture without panicking.
/// Inputs: surface (borrowed reference to wgpu Surface providing access to display buffers).
/// 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> {
+15 -14
View File
@@ -19,9 +19,8 @@
//! - **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;
use crate::resources::{Material, Mesh};
/// 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
@@ -37,6 +36,8 @@ pub struct Renderer {
impl Renderer {
/// Creates a Renderer by cloning Device and Queue Arc references from the Context, plus capturing the surface format.
/// Inputs: context (borrowed reference to Context providing GPU resource handles), format (surface texture format).
/// Returns a new Renderer instance sharing the same underlying GPU resources as Context.
/// 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 {
@@ -49,20 +50,18 @@ impl Renderer {
/// 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).
/// Inputs: view (TextureView color attachment target), mesh (geometry to render), material (shader+pipeline).
/// 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,
) {
/// 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"),
});
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.
@@ -106,11 +105,13 @@ impl Renderer {
}
/// Returns a reference to the owned Device for direct access when needed (e.g., PipelineCache creation).
/// Called internally during scene setup; not typically used by external code.
pub fn device(&self) -> &wgpu::Device {
&self.device
}
/// Returns the surface texture output format used for rendering.
/// Called internally during Material/PipelineCache initialization to ensure pipeline compatibility.
pub fn format(&self) -> wgpu::TextureFormat {
self.format
}
+28
View File
@@ -1,6 +1,34 @@
//! # AppHandler Trait — User-Defined Game Logic Interface
//!
//! Defines the `AppHandler` trait that users implement to inject their game logic into the render loop.
//! Provides two callback points: `update()` for pre-render logic (physics, input processing) and
//! `render()` for draw call execution. Both methods receive mutable access to the `App` facade so
//! users can modify resources, entities, or other state during each frame iteration.
//!
//! ## Interaction with Other Modules
//! - **app**: The orchestrator calls update() before rendering and render() during the RedrawRequested event.
//! AppHandler has no direct knowledge of wgpu internals — it operates only through the App facade.
//! - **scene::Scene**: Users typically manipulate app.scene inside these callbacks to add/remove entities.
//! - **pipeline::PipelineCache**: Users may create new Materials via cache.get_or_create() in update().
//!
//! ## Architecture Note
//! Per ARCHI_APP.md, this trait is one half of the "App" facade pattern. It enables a declarative workflow
//! where users define their game logic without touching WGPU directly, while keeping the freedom to build
//! the engine "brick by brick" through direct Context/PipelineCache/Renderer manipulation if needed.
use crate::app::App;
/// Trait defining user-provided game logic injected into the render loop at two callback points.
/// Users implement this trait to define what happens per-frame: update (pre-render logic) and
/// render (draw call execution). Default implementations provide empty update for convenience.
pub trait AppHandler {
/// Called once per frame before rendering begins. Used for physics updates, input processing,
/// entity management, and any other pre-render logic. Default implementation does nothing.
/// Inputs: _app — mutable reference to the App facade providing access to all subsystems.
fn update(&mut self, _app: &mut App) {}
/// Called during each RedrawRequested event after frame acquisition. Used for executing draw calls
/// by iterating Scene entities and calling app.renderer.render(view, mesh, material) per entity.
/// Must be implemented — called every frame that needs rendering.
/// Inputs: app — mutable reference to the App facade providing access to all subsystems.
fn render(&mut self, app: &mut App);
}
+19 -4
View File
@@ -1,14 +1,22 @@
//! # 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).
//! The top-level entry point for the wsg-lib crate. Exposes seven public modules organized by architectural responsibility:
//! **app** (App facade + AppBuilder), **handler** (AppHandler trait), **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.
//! - `app` orchestrates all subsystems plus the event loop; depends on everything else.
//! - `handler` defines the user-facing interface consumed by `app`.
//! - `utils` is a leaf module — no internal dependencies on other library modules.
//!
//! ## Top-Level Re-Exports
//! These are the two primary types users interact with when building applications:
//! - `App` — high-level application facade wrapping window lifecycle, GPU context, and render automation.
//! - `AppHandler` — trait users implement to inject game logic into the render loop.
//!
//! ## Usage
//! Consumers import through the re-exports defined in each submodule's `mod.rs`:
//! ```ignore
@@ -16,6 +24,7 @@
//! use wsg_lib::resources::{Mesh, Material, Vertex};
//! use wsg_lib::utils::BASIC_SHADER;
//! ```
pub mod app;
pub mod core;
pub mod handler;
@@ -24,4 +33,10 @@ pub mod resources;
pub mod scene;
pub mod utils;
pub use handler::AppHandler;
/// Re-export of the high-level application facade for convenient top-level access.
/// Users create App instances via `AppBuilder`, then call `.run(handler)` to start the application.
pub use crate::app::App;
/// Re-export of the user-defined game logic interface for convenient top-level access.
/// Users implement this trait to define update/render callbacks injected into the render loop.
pub use crate::handler::AppHandler;
+14 -5
View File
@@ -33,6 +33,8 @@ pub struct PipelineCache {
impl PipelineCache {
/// Creates an empty pipeline cache with no pre-loaded shaders or pipelines.
/// Inputs: device (owned Arc reference to wgpu Device, required for creating ShaderModules and RenderPipelines).
/// Returns a new PipelineCache ready for shader registration via register_shader().
/// Called at application startup before any Material creation. Shader paths must be registered via register_shader() first.
pub fn new(device: Arc<wgpu::Device>) -> Self {
Self {
@@ -45,7 +47,7 @@ impl PipelineCache {
}
/// 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.
/// Returns Ok(id) on success or Err(String) 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));
@@ -56,20 +58,23 @@ impl PipelineCache {
/// 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.
/// Returns Ok(id) on success or Err(String) 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)
self.pipelines.remove(id);
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).
/// Inputs: 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().
/// Internal steps: 1) check pipelines HashMap for existing entry →
/// 2a) if found: clone Arc and return →
/// 2b) if not found: load_shader() + build_pipeline() → cache behind Arc → insert and return.
pub fn get_or_create(
&mut self,
format: wgpu::TextureFormat,
@@ -97,7 +102,8 @@ impl PipelineCache {
}
/// 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.
/// Inputs: device (GPU command source for shader compilation), path (file path or shader_id string).
/// Returns a compiled wgpu::ShaderModule. 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);
@@ -113,6 +119,9 @@ impl PipelineCache {
/// 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()`.
/// Internal steps: 1) define VertexBufferLayout from Vertex struct offsets →
/// 2) create PipelineLayout with bind_group_layouts + immediate_size →
/// 3) create RenderPipeline with vertex/fragment states, primitive config, multisample state.
fn build_pipeline(
device: &wgpu::Device,
format: wgpu::TextureFormat,
+3 -3
View File
@@ -23,9 +23,9 @@ pub struct Material {
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.
/// Inputs: format (surface texture format required for fragment output), shader_id (unique key into PipelineCache),
/// cache (mutable reference for potential insertion of new pipelines).
/// Returns a Material holding the Arc-wrapped pipeline. Called at scene initialization time only.
pub fn new(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(format, shader_id);
+5 -1
View File
@@ -27,8 +27,12 @@ pub struct Mesh {
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).
/// Inputs: device (GPU command source for buffer creation), vertices (CPU-side vertex array to upload),
/// indices (optional CPU-side index array for indexed drawing).
/// Returns a Mesh with two GPU buffers ready for rendering. Called at scene initialization time only.
/// Internal steps: 1) create_buffer_init for vertex data →
/// 2) if indices provided: create_buffer_init for index data and set num_indices = len →
/// else: set index_buffer = None and num_indices = 0.
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"),
+4 -4
View File
@@ -15,13 +15,13 @@
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct Vertex {
/// XYZ coordinates of this vertex in world space. Offset: 0 bytes.
/// XYZ coordinates of this vertex in world space. Offset: 0 bytes (12 bytes total as [f32;3]).
pub position: [f32; 3],
/// XYZ coordinates of the vertex normal. Offset: 12 bytes.
/// XYZ coordinates of the vertex normal. Offset: 12 bytes (12 bytes total as [f32;3]).
pub normal: [f32; 3],
/// UV texture coordinates. Offset: 24 bytes
/// UV texture coordinates. Offset: 24 bytes (8 bytes total as [f32;2]).
pub uv: [f32; 2],
/// RGBA color values. Offset: 32 bytes.
/// RGBA color values. Offset: 32 bytes (16 bytes total as [f32;4]).
pub color: [f32; 4],
}
+14 -11
View File
@@ -18,9 +18,9 @@ use std::sync::Arc;
/// 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()`.
/// Map of mesh identifiers to owned Arc<Mesh> instances. Populated via `add_mesh()`.
meshes: HashMap<String, Arc<Mesh>>,
/// Map of material identifiers to owned Material instances. Populated via `add_material()`.
/// Map of material identifiers to owned Arc<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)>,
@@ -38,7 +38,7 @@ impl Scene {
}
/// 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.
/// Inputs: id (unique key), mesh (Arc-wrapped Mesh instance). Returns Ok(id) on success or Err(String) 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) {
@@ -49,7 +49,7 @@ impl Scene {
}
/// 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.
/// Inputs: id (unique key), material (Arc-wrapped Material instance). Returns Ok(id) on success or Err(String) 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) {
@@ -60,9 +60,11 @@ impl Scene {
}
/// 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.
/// Inputs: label (entity identifier string), mesh_id (key into meshes map), material_id (key into materials map).
/// Returns Ok(label) on success or Err(String) if either referenced resource does not exist.
/// Called during scene initialization to build the renderable entity graph.
/// Internal steps: 1) validate mesh_id exists → 2) validate material_id exists →
/// 3) insert association into entities HashMap.
pub fn add_entity(
&mut self,
label: &str,
@@ -75,7 +77,10 @@ impl Scene {
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()));
self.entities.insert(
label.to_string(),
(mesh_id.to_string(), material_id.to_string()),
);
Ok(label.to_string())
}
@@ -93,12 +98,10 @@ impl Scene {
/// 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>)> + '_ {
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
let mat = self.materials.get(mat_id).unwrap(); // same invariant
(label.as_str(), mesh, mat)
})
}
+17 -10
View File
@@ -4,18 +4,25 @@
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.
## Files
| 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. |
| **basic_shader.wgsl** | Default vertex/fragment shader pair (vs_main / fs_main entry points) with position, uv, and color attributes. |
## Shader Contract
## Shader Contract (basic_shader.wgsl)
The WGSL shader must define:
The WGSL shader defines:
- `@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)
- `@vertex fn vs_main(model: VertexInput) -> VertexOutput` — vertex entry point
- `@fragment fn fs_main(in: VertexOutput) -> @location(0) vec4<f32>` — fragment entry point writing RGBA output
### Vertex Input Layout
| Location | Attribute | Type | Offset (bytes) |
|----------|-----------|------|----------------|
| 0 | position | vec3<f32> | 0 |
| 1 | uv | vec2<f32> | 12 |
| 2 | color | vec3<f32> | 24 |
**Note**: This shader uses a 39-byte vertex stride (3+2+3 floats). It does NOT include normal data or alpha channel interpolation — it outputs fully opaque geometry with per-vertex color passthrough. This differs from the full `Vertex` struct layout (56 bytes with normal + alpha) defined in resources::Vertex; if a full shader matching the Vertex struct is needed, extend this shader accordingly.
+18
View File
@@ -1,4 +1,22 @@
//! # Basic Shader Module
//!
//! Default vertex/fragment shader pair used by PipelineCache when no external .wgsl file is found.
//! This shader implements a simple unlit rendering path: passes through position and color attributes
//! from VertexInput to fragment output, producing flat-colored geometry without lighting calculations.
//!
//! ## Shader Contract
//! Must define entry points matching PipelineCache::build_pipeline():
//! - @vertex fn vs_main(model: VertexInput) -> VertexOutput
//! - model.position → @location(0), vec3<f32>, offset 0 bytes in vertex buffer
//! - model.uv → @location(1), vec2<f32>, offset 12 bytes in vertex buffer
//! - model.color → @location(2), vec3<f32>, offset 24 bytes in vertex buffer
//! - @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4<f32>
//! - Writes RGBA output where alpha is hardcoded to 1.0 (fully opaque).
//!
//! ## Technical Notes
//! - No normal or UV interpolation — this is an unlit shader that directly outputs the per-vertex color.
//! - The clip_position is computed as vec4<f32>(position, 1.0), assuming position is already in NDC space.
struct VertexInput {
@location(0) position: vec3<f32>,
@location(1) uv: vec2<f32>,
+7
View File
@@ -3,10 +3,12 @@
//! 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.
//! Also provides application defaults for window title, width, and height used by AppBuilder.
//!
//! ## 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.
//! - **app::AppBuilder** reads APP_DEFAULT_TITLE, APP_DEFAULT_WIDTH, and APP_DEFAULT_HEIGHT for default window configuration.
/// 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";
@@ -15,6 +17,11 @@ pub const BASIC_SHADER_PATH: &str = "assets/shaders/basic_shader.wgsl";
/// Serves as a fallback when `BASIC_SHADER_PATH` cannot be read at runtime.
pub const BASIC_SHADER: &str = include_str!("../shaders/basic_shader.wgsl");
/// Default application title displayed in the OS taskbar/window decorations.
pub const APP_DEFAULT_TITLE: &str = "WSG App";
/// Default window width in pixels.
pub const APP_DEFAULT_WIDTH: u32 = 800;
/// Default window height in pixels.
pub const APP_DEFAULT_HEIGHT: u32 = 600;
+1
View File
@@ -8,6 +8,7 @@
//! ## 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).
//! - **frame** does not use errors — Frame::new() panics while Frame::try_new() returns Option<Self>.
use thiserror::Error;