ajout material et pipeline_cache
This commit is contained in:
+45
-28
@@ -1,45 +1,62 @@
|
|||||||
La nouvelle vision architecturale
|
# ARCHI_MESH_MATERIAL.md (Version corrigée)
|
||||||
Pour comprendre ce changement, imagine que tu veux peindre 10 tableaux différents.
|
|
||||||
|
|
||||||
Avant (Ton code actuel) : Chaque tableau possède sa propre cuisine, son propre chef (le pipeline), et son propre matériel de peinture. C'est inefficace.
|
## La nouvelle vision architecturale : L'Atelier de Rendu
|
||||||
|
|
||||||
Après (La nouvelle structure) : Tu as un Atelier (le Renderer) qui contient des Recettes (le Material / Shader) et tu apportes tes Toiles (le Mesh). Tu peux utiliser la même recette pour 50 toiles différentes sans effort.
|
Pour comprendre notre structure, imagine que tu veux peindre 10 tableaux différents.
|
||||||
|
|
||||||
Voici la nouvelle répartition des responsabilités :
|
**Avant :** Chaque tableau possédait sa propre cuisine, son propre chef (le pipeline), et son propre matériel. C'était inefficace et lourd.
|
||||||
|
|
||||||
1. Le Mesh (La géométrie)
|
**Après :** Tu as un Atelier (Renderer) qui orchestre le dessin. Il utilise des Recettes (Material + PipelineCache) pour définir l'apparence et traite des Toiles (Mesh) pour la géométrie. Tu peux utiliser la même recette pour 50 toiles différentes sans effort.
|
||||||
Il ne sait pas comment il est affiché, il sait seulement ce qu'il est.
|
|
||||||
|
|
||||||
Contenu : Il possède les données brutes (le VertexBuffer et éventuellement un IndexBuffer pour optimiser le dessin).
|
---
|
||||||
|
|
||||||
Rôle : Il est passif. C'est juste un conteneur de données prêtes à être envoyées à la carte graphique.
|
## 1. Le Mesh (La Géométrie)
|
||||||
|
|
||||||
2. Le Material (Le look)
|
Il est purement passif. Il ne sait pas comment il est affiché, il sait seulement ce qu'il est.
|
||||||
Il définit l'apparence de l'objet.
|
|
||||||
|
|
||||||
Contenu : Il possède le RenderPipeline. Le pipeline contient tout ce qui est "lourd" à créer (le shader compilé, les états de fusion, le mode de tracé).
|
- **Contenu :** `vertexBuffer`, optionnellement `indexBuffer`, et les compteurs associés (`num_vertices`, `num_indices`).
|
||||||
|
- **Rôle :** Fournir les données brutes au GPU.
|
||||||
|
|
||||||
Rôle : Il est réutilisable. Si tu as 10 objets en métal, ils partagent tous le même Material (donc le même pipeline).
|
---
|
||||||
|
|
||||||
3. Le Renderer (L'Orchestrateur)
|
## 2. Le Material & PipelineCache (Le Look & La Recette)
|
||||||
Il devient beaucoup plus léger et efficace.
|
|
||||||
|
|
||||||
Contenu : Il ne possède plus de buffers en dur. Il possède une méthode render qui accepte un Mesh ET un Material.
|
Le look est désormais découplé de la géométrie via une gestion centralisée.
|
||||||
|
|
||||||
Rôle : Il fait la liaison au moment de l'appel :
|
**PipelineCache :** C'est la bibliothèque de recettes. Il charge les shaders (WGSL), compile les RenderPipeline, et les met en cache (via `HashMap` + `Arc`) pour éviter de dupliquer les ressources GPU.
|
||||||
|
|
||||||
Rust
|
**Material :** C'est une instance légère qui pointe vers une recette compilée. Il contient un `shader_id` et une référence partagée (`Arc`) vers le RenderPipeline.
|
||||||
|
|
||||||
|
- **Rôle :** Garantir la réutilisation. Si 100 objets partagent le même shader, ils pointent tous vers la même instance compilée en mémoire.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Le Renderer (L'Orchestrateur)
|
||||||
|
|
||||||
|
Il est devenu ultra-léger et généraliste. Il ne possède plus aucun buffer ni pipeline en dur.
|
||||||
|
|
||||||
|
- **Contenu :** Aucun état lourd.
|
||||||
|
- **Rôle :** Il fait la liaison au moment de l'appel :
|
||||||
|
|
||||||
|
```rust
|
||||||
// Schématiquement
|
// Schématiquement
|
||||||
renderer.render_pass.set_pipeline(&material.pipeline); // On change de recette
|
render_pass.set_pipeline(&material.pipeline); // On change de recette
|
||||||
renderer.render_pass.set_vertex_buffer(0, &mesh.buffer); // On pose la toile
|
render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..)); // On pose la toile
|
||||||
renderer.render_pass.draw(...);
|
// Puis dessin indexé ou simple...
|
||||||
Pourquoi cette structure est-elle meilleure ?
|
```
|
||||||
Réutilisation (Performance) : Si tu as 100 objets avec le même shader, tu ne compiles ton shader qu'une seule fois. Tu gagnes énormément en vitesse de chargement et en occupation mémoire.
|
|
||||||
|
|
||||||
Modularité : Tu peux créer un Mesh "Cercle" et un Mesh "Carré". Tu peux créer un Material "Rouge" et un Material "Bleu". Tu peux combiner n'importe quel Mesh avec n'importe quel Material sans changer une ligne de code.
|
---
|
||||||
|
|
||||||
Nettoyage : Ton renderer.rs devient un orchestrateur pur au lieu d'être un fourre-tout.
|
## Pourquoi cette structure est-elle meilleure ?
|
||||||
|
|
||||||
La note technique : "Pipeline Cache"
|
1. **Performance :** Les shaders sont compilés une seule fois. La mémoire GPU est optimisée grâce au partage des RenderPipeline via `Arc`.
|
||||||
Dans cette nouvelle structure, tu devras faire attention à un détail : le couplage entre le Mesh et le Material.
|
2. **Modularité :** Tu peux combiner n'importe quel Mesh avec n'importe quel Material.
|
||||||
Si ton Mesh a une structure de Vertex différente de ce que le Material (Shader) attend, le rendu sera invalide. Le Material doit donc garantir qu'il est capable de traiter le format de données fourni par le Mesh.
|
3. **Propreté :** Ton Renderer est désormais un orchestrateur pur. Il ne connaît plus le détail des shaders ou des layouts de sommets : il se contente d'exécuter la commande de dessin.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quelques notes techniques pour ta doc
|
||||||
|
|
||||||
|
- **Découplage :** Le Material demande au PipelineCache de lui fournir un pipeline au moment de sa création.
|
||||||
|
- **Sécurité :** Le PipelineCache utilise un `HashMap` pour retrouver instantanément un pipeline existant par son `shader_id`, évitant les compilations inutiles.
|
||||||
|
- **Flexibilité :** Le Mesh gère lui-même ses indices, permettant de passer facilement du rendu simple au rendu indexé optimisé.
|
||||||
|
|||||||
+37
-30
@@ -1,42 +1,49 @@
|
|||||||
# Technical Architecture Summary
|
# Three-Layer Structure
|
||||||
|
|
||||||
## Three-Layer Structure
|
## Manager Layer (Context)
|
||||||
|
|
||||||
### Manager Layer (`Context`)
|
**Responsabilité :** Propriétaire du cycle de vie des ressources matérielles (`Device`, `Queue`, `Surface`).
|
||||||
|
|
||||||
- **Responsibility:** Owner of hardware resource lifecycles (`Device`, `Queue`, `Surface`, `SurfaceConfiguration`).
|
**Rôle :** Encapsule la complexité du système de fenêtrage et du swapchain. Orchestre les transactions GPU via `begin_frame()` et `end_frame()`.
|
||||||
- **Role:** Encapsulates windowing system and swapchain complexity. Exposes high-level methods such as `begin_frame()` and `end_frame()` to orchestrate GPU transactions.
|
|
||||||
|
|
||||||
### Specialist Layer (`Renderer`)
|
|
||||||
|
|
||||||
- **Responsibility:** Owner of rendering logic (`RenderPipeline`, `Shaders`, `Buffers`).
|
|
||||||
- **Role:** Executes actual drawing. Does not own hardware resources — uses references (`&Device`, `&TextureView`) provided at call time.
|
|
||||||
|
|
||||||
### Orchestrator Layer (`main.rs`)
|
|
||||||
|
|
||||||
- **Responsibility:** Business logic and execution loop.
|
|
||||||
- **Role:** Calls `Context` methods to obtain the target texture, passes that target to the `Renderer`, then triggers presentation.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Benefits Analysis
|
## Specialist Layer (Renderer & PipelineCache)
|
||||||
|
|
||||||
### 1. Independence & Modularity
|
**PipelineCache (La Bibliothèque) :** Propriétaire de la compilation et du stockage des RenderPipeline. Garantit qu'un shader n'est compilé qu'une seule fois.
|
||||||
|
|
||||||
- **Runtime Agnosticism:** By avoiding any async calls or dependencies on runtimes like Tokio within the library, we ensure the code is portable and can be integrated into any kind of project (game, visualization tool, UI).
|
**Renderer (L'Exécuteur) :** Orchestre l'appel au dessin. Il est désormais agnostique : il ne possède plus les pipelines en dur, mais reçoit dynamiquement les Mesh et les Material à dessiner.
|
||||||
- **Decoupling:** The `Renderer` does not know about the existence of the windowing system (Winit). It could just as well draw onto an off-screen texture for headless rendering.
|
|
||||||
|
|
||||||
### 2. Performance & Efficiency
|
|
||||||
|
|
||||||
- **CPU/GPU Parallelism:** Using the `begin_frame` / `end_frame` pattern with a per-frame `CommandEncoder`, we maximize GPU utilization: the CPU prepares commands for one frame while the GPU executes those from the previous frame.
|
|
||||||
- **Persistent Resource Management:** The `Renderer` retains heavy objects (`RenderPipeline`) compiled only once. Conversely, ephemeral objects (`CommandEncoder`, `TextureView`) are created and freed quickly, minimizing long-term memory footprint.
|
|
||||||
|
|
||||||
### 3. Robustness (Safety & Errors)
|
|
||||||
|
|
||||||
- **Explicit Error Handling:** Use of `Result` types and safe methods such as `.first()` (instead of manual indexing) prevents panics during initialization or resizing, protecting the application against graphics driver instabilities.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Summary
|
## Orchestrator Layer (main.rs)
|
||||||
|
|
||||||
This structure transforms what is often called "spaghetti" graphics code — a mix of window management and shader computation — into a clean, predictable pipeline. The `Context` prepares the ground, the `Renderer` performs the drawing, and the orchestrator maintains the rhythm.
|
**Responsabilité :** Logique métier et boucle d'exécution.
|
||||||
|
|
||||||
|
**Rôle :** Coordonne le Context, le PipelineCache pour créer les Material, et enfin passe le tout au Renderer pour produire l'image.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Updated Benefits Analysis
|
||||||
|
|
||||||
|
## Modularité Totale (Decoupling)
|
||||||
|
|
||||||
|
Le Renderer est totalement découplé du contenu graphique. Il ne connaît pas les shaders, il sait juste "lier" un Material à un Mesh. Cela permet un rendu multi-objets très simple.
|
||||||
|
|
||||||
|
## Efficacité Mémoire (Resource Sharing)
|
||||||
|
|
||||||
|
Grâce au PipelineCache et à l'utilisation de `Arc<wgpu::RenderPipeline>`, plusieurs Material partagent la même recette compilée sur le GPU. On évite la duplication coûteuse de ressources.
|
||||||
|
|
||||||
|
## Performance de Rendu
|
||||||
|
|
||||||
|
En séparant la "préparation des recettes" (PipelineCache) de "l'exécution du dessin" (Renderer), on élimine tout risque de compilation/allocation lourde pendant la boucle de rendu (60 FPS).
|
||||||
|
|
||||||
|
## Flexibilité
|
||||||
|
|
||||||
|
Le passage à un modèle (Mesh + Material) permet de combiner n'importe quelle géométrie avec n'importe quel effet visuel à la volée.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
|
||||||
|
Cette structure transforme une architecture rigide en un atelier de rendu dynamique. Le Context prépare le terrain, le PipelineCache fournit les outils (shaders), le Material définit le style, le Mesh apporte la forme, et le Renderer orchestre l'assemblage final. Le système est désormais prêt à gérer des scènes complexes avec de multiples objets et des effets variés.
|
||||||
|
|||||||
+48
-57
@@ -1,72 +1,63 @@
|
|||||||
# DRAFT — Frame Loop
|
# La Boucle de Rendu (Frame Loop)
|
||||||
|
|
||||||
## The "Frame Loop" (preparing the draw)
|
Pour afficher quelque chose, nous suivons un cycle immuable appelé la Frame Lifetime. Dans ton `main.rs` (l'orchestrateur), le flux est désormais le suivant :
|
||||||
|
|
||||||
To display something, you must follow an immutable cycle called the **Frame Lifetime**. In your Renderer (or a dedicated method of Context), you will need to:
|
- **Context::begin_frame() :** Acquiert la surface texture et crée la TextureView.
|
||||||
|
- **Renderer::render(...)** : Utilise le CommandEncoder pour écrire les ordres de dessin.
|
||||||
|
- **Context::end_frame()** : Soumet les commandes à la file (`queue`) et présente l'image.
|
||||||
|
|
||||||
1. **Acquire a surface texture** — ask the surface for the Back Buffer texture (where you'll draw).
|
---
|
||||||
2. **Create a texture view** — WGPU doesn't draw directly on the texture, but on a "view" into that texture.
|
|
||||||
3. **Create a command encoder** — this is the notebook where you write your orders ("clear screen", "draw triangle", "finalize").
|
|
||||||
4. **Submit commands** — send this notebook to the GPU via the Queue.
|
|
||||||
|
|
||||||
### Why this is the logical next step
|
## Pourquoi cette séparation est vitale
|
||||||
|
|
||||||
Because without this, your `configure` call does nothing. You've reserved space on the GPU; now you need to learn how to "grasp" it to work on it.
|
Le bloc `{ let mut render_pass = ... }` est crucial. Dans Rust, `render_pass` emprunte mutablement `encoder`. Il doit être détruit (via la fin du bloc ou un `drop()`) avant que tu puisses appeler `encoder.finish()`. Si tu oublies cela, le compilateur Rust refusera de compiler, empêchant ainsi des bugs critiques de synchronisation GPU.
|
||||||
|
|
||||||
Here is the skeleton of this cycle that you should implement:
|
---
|
||||||
|
|
||||||
|
## Ressources : Persistantes vs Par-Frame
|
||||||
|
|
||||||
|
Avec notre nouvelle architecture "Atelier", la distinction est devenue encore plus nette :
|
||||||
|
|
||||||
|
| Élément | Durée de vie | Pourquoi ? |
|
||||||
|
|---------|-------------|------------|
|
||||||
|
| SurfaceConfiguration | Persistante | Ne change qu'au redimensionnement. |
|
||||||
|
| RenderPipeline | Persistante | Stocké dans le PipelineCache (`Arc`), compilation unique. |
|
||||||
|
| Material | Persistante | Définit le look ; partage le pipeline via `Arc`. |
|
||||||
|
| Mesh | Persistante | Les données géométriques sont envoyées une fois au GPU. |
|
||||||
|
| CommandEncoder | Par-Frame | Ton "carnet de notes" temporaire pour les ordres du GPU. |
|
||||||
|
| TextureView | Par-Frame | Fenêtre temporaire sur la texture active du swapchain. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ce qui a changé dans l'implémentation
|
||||||
|
|
||||||
|
Le Renderer n'est plus le propriétaire de la Surface. Sa méthode `render` est devenue un orchestrateur généraliste :
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// In your Renderer (or Context)
|
// Le Renderer ne connait plus la surface, il reçoit la vue
|
||||||
pub fn render(&self) -> Result<(), WsgError> {
|
pub fn render(
|
||||||
// 1. Acquire the texture to draw on
|
&self,
|
||||||
let frame = self.surface.get_current_texture()
|
device: &wgpu::Device,
|
||||||
.map_err(|_| WsgError::SurfaceIncompatible)?;
|
queue: &wgpu::Queue,
|
||||||
|
view: &wgpu::TextureView,
|
||||||
// 2. Create the view (the "channel" to the texture)
|
mesh: &Mesh,
|
||||||
let view = frame.texture.create_view(&wgpu::TextureViewDescriptor::default());
|
material: &Material
|
||||||
|
) {
|
||||||
// 3. Create the command encoder
|
let mut encoder = device.create_command_encoder(...);
|
||||||
let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
|
||||||
label: Some("Render Encoder"),
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- This is where we'll draw later ---
|
|
||||||
{
|
{
|
||||||
let _render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
let mut render_pass = encoder.begin_render_pass(...);
|
||||||
label: Some("Render Pass"),
|
render_pass.set_pipeline(&material.pipeline); // Recette via Material
|
||||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
|
||||||
view: &view,
|
// ... dessin ...
|
||||||
resolve_target: None,
|
} // render_pass est automatiquement drop ici
|
||||||
ops: wgpu::Operations {
|
|
||||||
load: wgpu::LoadOp::Clear(wgpu::Color::BLUE), // Blue background for testing
|
|
||||||
store: wgpu::StoreOp::Store,
|
|
||||||
},
|
|
||||||
})],
|
|
||||||
depth_stencil_attachment: None,
|
|
||||||
timestamp_writes: None,
|
|
||||||
occlusion_query_set: None,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Submit and present
|
queue.submit(std::iter::once(encoder.finish()));
|
||||||
self.queue.submit(std::iter::once(encoder.finish()));
|
|
||||||
frame.present();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Why this separation is vital
|
---
|
||||||
|
|
||||||
You'll notice that the `{ let _render_pass ... }` block is delimited by braces. This is very important in Rust: `render_pass` must be dropped before calling `encoder.finish()`. If you forget this, your program will crash because you'd be submitting orders while the "notebook" is still being written.
|
## Pourquoi c'est l'étape logique suivante
|
||||||
|
|
||||||
|
En déléguant la gestion du Pipeline au Material et la possession de la Surface au Context, ton Renderer est devenu un moteur d'exécution pur. Il n'a plus besoin d'être réinitialisé quand la fenêtre change ou quand tu changes de shader : il est prêt à dessiner n'importe quel combo Mesh/Material que tu lui passes en paramètre.
|
||||||
## Persistent vs. Per-Frame Resources
|
|
||||||
|
|
||||||
| Element | Lifetime | Why? |
|
|
||||||
|---------|----------|------|
|
|
||||||
| `SurfaceConfiguration` | Persistent | Only changes on resize |
|
|
||||||
| `RenderPipeline` | Persistent | Very expensive to create (shader compilation) |
|
|
||||||
| Buffers (Vertex/Index) | Persistent | Geometry data doesn't change every frame |
|
|
||||||
| `CommandEncoder` | Frame | Temporary "notebook" for frame commands |
|
|
||||||
| `TextureView` | Frame | View into the active Swapchain texture |
|
|
||||||
|
|||||||
+10
-4
@@ -1,10 +1,16 @@
|
|||||||
//! # Configuration Module
|
//! # Configuration Module
|
||||||
//!
|
//!
|
||||||
//! Holds shared constants for the WSG library, such as shader paths and other compilation-time values.
|
//! Holds shared constants for the WSG library — primarily shader paths and embedded WGSL source code.
|
||||||
//! This module centralizes configuration so that it can be imported by any submodule without duplicating literals.
|
//! 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 (runtime).
|
/// 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";
|
pub const BASIC_SHADER_PATH: &str = "assets/shaders/basic_shader.wgsl";
|
||||||
|
|
||||||
/// The basic WGSL shader source, embedded at compile time as a fallback.
|
/// 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!("../assets/shaders/basic_shader.wgsl");
|
pub const BASIC_SHADER: &str = include_str!("../assets/shaders/basic_shader.wgsl");
|
||||||
|
|||||||
+10
-1
@@ -1,6 +1,13 @@
|
|||||||
//! # Context Module
|
//! # Context Module
|
||||||
//!
|
//!
|
||||||
//! Initializes the GPU, creates the surface, and holds the Device and Queue. It is static (created once at startup).
|
//! The **Manager** layer of the architecture — owns hardware resource lifecycle (Device, Queue, Surface).
|
||||||
|
//! Initializes the GPU at startup; 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.
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use wgpu::{Adapter, Device, Instance, Queue, Surface};
|
use wgpu::{Adapter, Device, Instance, Queue, Surface};
|
||||||
@@ -27,6 +34,7 @@ impl Context {
|
|||||||
/// Initializes the WGPU context. Creates the surface from the window, requests a device from the adapter,
|
/// 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).
|
/// and stores all required objects (instance, surface, adapter, device, queue).
|
||||||
/// Called once at application startup. Returns an error if GPU initialization fails.
|
/// 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> {
|
pub async fn new(window: Arc<Window>) -> Result<Self, WsgError> {
|
||||||
// WGPU instance
|
// WGPU instance
|
||||||
let instance = wgpu::Instance::default();
|
let instance = wgpu::Instance::default();
|
||||||
@@ -64,6 +72,7 @@ impl Context {
|
|||||||
/// Inputs: adapter (GPU capabilities), width/height (surface resolution).
|
/// Inputs: adapter (GPU capabilities), width/height (surface resolution).
|
||||||
/// Returns Ok(()) on success or SurfaceIncompatible if no SRGB format + alpha mode exist.
|
/// Returns Ok(()) on success or SurfaceIncompatible if no SRGB format + alpha mode exist.
|
||||||
/// Typically called by the renderer when window size changes.
|
/// 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(
|
pub fn configure(
|
||||||
&self,
|
&self,
|
||||||
adapter: &wgpu::Adapter,
|
adapter: &wgpu::Adapter,
|
||||||
|
|||||||
+16
-10
@@ -1,7 +1,13 @@
|
|||||||
//! # Error Module
|
//! # Error Module
|
||||||
//!
|
//!
|
||||||
//! Defines `WsgError`, the application-level error type for all WGPU operations.
|
//! 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`.
|
//! 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;
|
use thiserror::Error;
|
||||||
|
|
||||||
@@ -34,8 +40,8 @@ pub enum WsgError {
|
|||||||
ShaderError(String),
|
ShaderError(String),
|
||||||
|
|
||||||
/// An internal WGPU error propagated from a device request failure.
|
/// An internal WGPU error propagated from a device request failure.
|
||||||
/// Automatically converted via `thiserror`'s `#[from]`.
|
/// Automatically converted via `thiserror`'s `#[from]` from `wgpu::RequestDeviceError`.
|
||||||
/// Caller: `Context::new()` — maps `wgpu::RequestDeviceError` into this variant.
|
/// Caller: `Context::new()` — maps the wgpu error into this variant automatically.
|
||||||
#[error("Internal WGPU error: {0}")]
|
#[error("Internal WGPU error: {0}")]
|
||||||
InternalWgpu(#[from] wgpu::RequestDeviceError),
|
InternalWgpu(#[from] wgpu::RequestDeviceError),
|
||||||
|
|
||||||
@@ -51,28 +57,28 @@ pub enum WsgError {
|
|||||||
#[error("No compatible render format or alpha mode found for the surface")]
|
#[error("No compatible render format or alpha mode found for the surface")]
|
||||||
SurfaceIncompatible,
|
SurfaceIncompatible,
|
||||||
|
|
||||||
/// A timeout was encountered while acquiring a surface texture. Skip this frame and retry.
|
/// A timeout was encountered while acquiring a surface texture. Skip this frame and retry. Caller: `Context::begin_frame()`.
|
||||||
#[error("Frame acquisition timed out")]
|
#[error("Frame acquisition timed out")]
|
||||||
FrameTimeout,
|
FrameTimeout,
|
||||||
|
|
||||||
/// The window is occluded (minimized or behind another window). Skip until visible.
|
/// The window is occluded (minimized or behind another window). Skip until visible. Caller: `Context::begin_frame()`.
|
||||||
#[error("Window is occluded")]
|
#[error("Window is occluded")]
|
||||||
Occluded,
|
Occluded,
|
||||||
|
|
||||||
/// The underlying surface changed — call configure() before retrying.
|
/// The underlying surface changed — call configure() before retrying. Caller: `Context::begin_frame()` on surface mismatch.
|
||||||
#[error("Surface configuration outdated; reconfigure required")]
|
#[error("Surface configuration outdated; reconfigure required")]
|
||||||
Outdated,
|
Outdated,
|
||||||
|
|
||||||
/// The surface has been lost and needs to be recreated.
|
/// The surface has been lost and needs to be recreated. Caller: `Context::begin_frame()` on surface loss.
|
||||||
#[error("Surface lost")]
|
#[error("Surface lost")]
|
||||||
Lost,
|
Lost,
|
||||||
|
|
||||||
/// A validation error inside get_current_texture() was raised.
|
/// A validation error inside get_current_texture() was raised. Caller: `Context::begin_frame()`.
|
||||||
#[error("Validation error during frame acquisition")]
|
#[error("Validation error during frame acquisition")]
|
||||||
Validation,
|
Validation,
|
||||||
|
|
||||||
/// Successfully acquired the surface texture but it no longer matches the surface properties.
|
/// Successfully acquired the surface texture but it no longer matches the surface properties.
|
||||||
/// Reconfigure recommended for optimal performance.
|
/// Reconfigure recommended for optimal performance. Caller: `Context::begin_frame()` on suboptimal acquire.
|
||||||
#[error("Acquired suboptimal surface texture; reconfigure recommended")]
|
#[error("Acquired suboptimal surface texture; reconfigure recommended")]
|
||||||
SuboptimalTexture,
|
SuboptimalTexture,
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-1
@@ -1,8 +1,27 @@
|
|||||||
|
//! # WSG Library — Public API Surface
|
||||||
|
//!
|
||||||
|
//! Re-exports all submodules so consumers can access types via `wsg::mesh::Mesh`, etc.
|
||||||
|
//! The library's core responsibility is to abstract five wgpu objects (Instance, Surface, Adapter, Device, Queue)
|
||||||
|
//! into a single Context for simpler user interaction.
|
||||||
|
//!
|
||||||
|
//! ## Module Interactions
|
||||||
|
//! - **context** owns hardware resources (Device, Queue, Surface) across the frame lifecycle.
|
||||||
|
//! - **pipeline_cache** compiles WGSL shaders into RenderPipelines once, caching them via HashMap + Arc.
|
||||||
|
//! - **material** requests pipelines from PipelineCache to define per-object appearance.
|
||||||
|
//! - **mesh** holds vertex/index buffers sent to the GPU once at creation time.
|
||||||
|
//! - **renderer** orchestrates draw calls using Material + Mesh references passed in at render time.
|
||||||
|
//! - **vertex** defines the CPU-side vertex layout matching GPU shader input attributes.
|
||||||
|
//! - **error** provides application-level error types mapping each failure mode to a message.
|
||||||
|
//! - **conf** centralizes shared constants like shader paths and embedded fallback sources.
|
||||||
|
|
||||||
pub mod conf;
|
pub mod conf;
|
||||||
pub mod context;
|
pub mod context;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
pub mod material;
|
||||||
pub mod mesh;
|
pub mod mesh;
|
||||||
|
pub mod pipeline_cache;
|
||||||
pub mod renderer;
|
pub mod renderer;
|
||||||
pub mod vertex;
|
pub mod vertex;
|
||||||
|
|
||||||
pub use error::WsgError; // Pour permettre un import direct du type
|
/// Re-export of the application-level error type for direct use by consumers.
|
||||||
|
pub use error::WsgError; // For convenient import without path prefix
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
//! # Material Module
|
||||||
|
//!
|
||||||
|
//! 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.
|
||||||
|
|
||||||
|
use crate::pipeline_cache::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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
@@ -1,14 +1,28 @@
|
|||||||
|
//! # Mesh Module
|
||||||
|
//!
|
||||||
|
//! 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.
|
||||||
|
|
||||||
use crate::vertex::Vertex;
|
use crate::vertex::Vertex;
|
||||||
use wgpu::util::DeviceExt;
|
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 {
|
pub struct Mesh {
|
||||||
|
/// GPU buffer containing vertex attribute data (position, UV, color).
|
||||||
pub vertex_buffer: wgpu::Buffer,
|
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>,
|
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,
|
pub num_vertices: u32,
|
||||||
|
/// Number of indices in the index buffer. Used as `0..num_indices` for indexed draws.
|
||||||
pub num_indices: u32,
|
pub num_indices: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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).
|
||||||
|
/// 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 {
|
pub fn new(device: &wgpu::Device, vertices: &[Vertex], indices: Option<&[u16]>) -> Self {
|
||||||
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||||
label: Some("Mesh Vertex Buffer"),
|
label: Some("Mesh Vertex Buffer"),
|
||||||
@@ -16,6 +30,7 @@ impl Mesh {
|
|||||||
usage: wgpu::BufferUsages::VERTEX,
|
usage: wgpu::BufferUsages::VERTEX,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Create optional index buffer and count indices if provided
|
||||||
let (index_buffer, num_indices) = if let Some(data) = indices {
|
let (index_buffer, num_indices) = if let Some(data) = indices {
|
||||||
let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||||
label: Some("Mesh Index Buffer"),
|
label: Some("Mesh Index Buffer"),
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
//! # PipelineCache Module
|
||||||
|
//!
|
||||||
|
//! Defines `PipelineCache`, the library's shader compilation cache. It owns WGSL shader loading and RenderPipeline
|
||||||
|
//! creation, storing compiled pipelines 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.
|
||||||
|
|
||||||
|
use crate::conf::BASIC_SHADER;
|
||||||
|
use crate::vertex::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.
|
||||||
|
pipelines: HashMap<String, Arc<wgpu::RenderPipeline>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PipelineCache {
|
||||||
|
/// Creates an empty pipeline cache with no pre-loaded shaders or pipelines.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
pipelines: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
// Note: In production you would load shaders specific to shader_id from files or embedded resources.
|
||||||
|
let shader = self.load_shader(device, shader_id);
|
||||||
|
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::Float32x2,
|
||||||
|
}, // uv
|
||||||
|
wgpu::VertexAttribute {
|
||||||
|
offset: 20,
|
||||||
|
shader_location: 2,
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+54
-184
@@ -1,207 +1,77 @@
|
|||||||
//! # Renderer Module
|
//! # Renderer Module
|
||||||
//!
|
//!
|
||||||
//! The **Specialist** layer of the architecture. Owns rendering logic — RenderPipeline, shaders, and vertex buffers.
|
//! The **Specialist** (Executor) layer of the architecture. Owns rendering logic — 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 hardware resources (Device, Queue); receives references when called by the orchestrator (main.rs).
|
||||||
//! Interacts with Context via `begin_frame()` / `end_frame()`: receives a TextureView as input, writes rendered output to it.
|
//! 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.
|
||||||
|
|
||||||
use crate::conf::BASIC_SHADER;
|
pub struct Renderer {}
|
||||||
use crate::vertex::Vertex;
|
|
||||||
|
|
||||||
// Required for `create_buffer_init` method on Device (wgpu::util::DeviceExt).
|
|
||||||
use wgpu::util::DeviceExt;
|
|
||||||
|
|
||||||
/// Renders geometry using a fixed RenderPipeline. This struct owns heavy objects
|
|
||||||
/// (RenderPipeline) compiled once at creation time, avoiding per-frame allocation.
|
|
||||||
pub struct Renderer {
|
|
||||||
/// Pre-compiled pipeline defining vertex/fragment stages, layout, and blend state.
|
|
||||||
render_pipeline: wgpu::RenderPipeline,
|
|
||||||
/// Default vertex buffer containing a fallback triangle (position + UV + color).
|
|
||||||
default_buffer: wgpu::Buffer,
|
|
||||||
/// Optional user-provided vertex buffer; replaces the default when set.
|
|
||||||
user_buffer: Option<wgpu::Buffer>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Renderer {
|
impl Renderer {
|
||||||
/// Creates the renderer by compiling the shader module and building the render pipeline.
|
/// Creates a new Renderer instance with no internal state — it is pure execution logic only.
|
||||||
/// Inputs:
|
/// Called once at application startup; the same Renderer is reused for all frames.
|
||||||
/// - `device`: GPU device for resource creation
|
pub fn new() -> Self {
|
||||||
/// - `format`: Color attachment format
|
Self {}
|
||||||
/// - `shader_path`: Optional custom WGSL shader file path; falls back to BASIC_SHADER if None.
|
|
||||||
/// Returns the initialized Renderer owning the pipeline. Called once at application startup
|
|
||||||
/// by the orchestrator (main.rs). The pipeline is compiled only here; subsequent calls are cheap.
|
|
||||||
pub fn new(
|
|
||||||
device: &wgpu::Device,
|
|
||||||
format: wgpu::TextureFormat,
|
|
||||||
shader_path: Option<&str>,
|
|
||||||
) -> Self {
|
|
||||||
let shader = Self::load_shader(device, shader_path);
|
|
||||||
// 1. Define the default triangle vertices (position + UV + color).
|
|
||||||
let vertices: &[Vertex] = &[
|
|
||||||
Vertex {
|
|
||||||
position: [0.0, 0.5, 0.0],
|
|
||||||
uv: [0.5, 0.0],
|
|
||||||
color: [1.0, 0.0, 0.0],
|
|
||||||
},
|
|
||||||
Vertex {
|
|
||||||
position: [-0.5, -0.5, 0.0],
|
|
||||||
uv: [0.0, 1.0],
|
|
||||||
color: [0.0, 1.0, 0.0],
|
|
||||||
},
|
|
||||||
Vertex {
|
|
||||||
position: [0.5, -0.5, 0.0],
|
|
||||||
uv: [1.0, 1.0],
|
|
||||||
color: [0.0, 0.0, 1.0],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// 2. Create the GPU vertex buffer from those vertices.
|
|
||||||
let default_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
|
||||||
label: Some("Default Triangle Buffer"),
|
|
||||||
contents: bytemuck::cast_slice(vertices),
|
|
||||||
usage: wgpu::BufferUsages::VERTEX,
|
|
||||||
});
|
|
||||||
|
|
||||||
Self {
|
|
||||||
render_pipeline: Self::build_pipeline(device, format, &shader),
|
|
||||||
default_buffer,
|
|
||||||
user_buffer: None, // User can set this later via update_shader or a setter.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Loads a WGSL shader module from the given path, falling back to BASIC_SHADER if no path is provided.
|
/// Orchestrates a single draw call: creates an encoder, starts a render pass, binds pipeline + buffers, draws, then submits commands.
|
||||||
pub fn load_shader(device: &wgpu::Device, path: Option<&str>) -> wgpu::ShaderModule {
|
/// Inputs: device (GPU command source), queue (command submission target), view (surface texture output), mesh (geometry to draw), material (shader/pipeline).
|
||||||
let source = match path {
|
/// Returns nothing — side-effect: GPU executes the draw and presents to the surface. Called by the orchestrator (main.rs) once per frame.
|
||||||
Some(p) => match std::fs::read_to_string(p) {
|
/// Internal steps: 1) create_command_encoder → 2) begin_render_pass in scoped block → 3) set_pipeline/set_vertex_buffer/draw → drop(render_pass) → 4) submit(encoder.finish()).
|
||||||
Ok(s) => s,
|
pub fn render(
|
||||||
Err(_) => {
|
&self,
|
||||||
println!(
|
|
||||||
"no wsgl file shader found in assets/shaders/, internal basic shader applied instead"
|
|
||||||
);
|
|
||||||
BASIC_SHADER.to_string()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => BASIC_SHADER.to_string(),
|
|
||||||
};
|
|
||||||
device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
|
||||||
label: Some(path.unwrap_or("basic_shader")),
|
|
||||||
source: wgpu::ShaderSource::Wgsl(source.into()),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Builds a render pipeline from a shader module, device, and format.
|
|
||||||
fn build_pipeline(
|
|
||||||
device: &wgpu::Device,
|
device: &wgpu::Device,
|
||||||
format: wgpu::TextureFormat,
|
queue: &wgpu::Queue,
|
||||||
shader: &wgpu::ShaderModule,
|
view: &wgpu::TextureView,
|
||||||
) -> wgpu::RenderPipeline {
|
mesh: &crate::mesh::Mesh,
|
||||||
// Define the vertex layout (the contract between CPU data and GPU shaders).
|
material: &crate::material::Material,
|
||||||
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,
|
|
||||||
}, // Pos
|
|
||||||
wgpu::VertexAttribute {
|
|
||||||
offset: 12,
|
|
||||||
shader_location: 1,
|
|
||||||
format: wgpu::VertexFormat::Float32x2,
|
|
||||||
}, // UV
|
|
||||||
wgpu::VertexAttribute {
|
|
||||||
offset: 20,
|
|
||||||
shader_location: 2,
|
|
||||||
format: wgpu::VertexFormat::Float32x3,
|
|
||||||
}, // Color
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Pipeline layout (wgpu 30: `immediate_size` replaces `push_constant_ranges`).
|
|
||||||
let render_pipeline_layout =
|
|
||||||
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
|
||||||
label: Some("render_pipeline_layout"),
|
|
||||||
bind_group_layouts: &[],
|
|
||||||
immediate_size: 0, // no var<immediate> used
|
|
||||||
});
|
|
||||||
|
|
||||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
|
||||||
label: Some("Render Pipeline"),
|
|
||||||
layout: Some(&render_pipeline_layout),
|
|
||||||
vertex: wgpu::VertexState {
|
|
||||||
module: shader,
|
|
||||||
// entry_point is now Option<&str> — Some to specify the entry point explicitly, None to auto-detect.
|
|
||||||
entry_point: Some("vs_main"),
|
|
||||||
compilation_options: Default::default(), // new required field in wgpu 30
|
|
||||||
buffers: &[Some(vertex_buffer_layout)],
|
|
||||||
},
|
|
||||||
fragment: Some(wgpu::FragmentState {
|
|
||||||
module: shader,
|
|
||||||
entry_point: Some("fs_main"),
|
|
||||||
compilation_options: Default::default(), // new required field in wgpu 30
|
|
||||||
// targets is now &[Option<ColorTargetState>] — each target wrapped in Some.
|
|
||||||
targets: &[Some(wgpu::ColorTargetState {
|
|
||||||
format,
|
|
||||||
blend: Some(wgpu::BlendState::REPLACE),
|
|
||||||
write_mask: wgpu::ColorWrites::ALL,
|
|
||||||
})],
|
|
||||||
}),
|
|
||||||
primitive: wgpu::PrimitiveState::default(),
|
|
||||||
depth_stencil: None,
|
|
||||||
multisample: wgpu::MultisampleState::default(),
|
|
||||||
// multiview → replaced by multiview_mask (NonZeroU32) and cache in wgpu 30.
|
|
||||||
multiview_mask: None,
|
|
||||||
cache: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Hot-reloads a new WGSL shader and rebuilds the render pipeline.
|
|
||||||
pub fn update_shader(
|
|
||||||
&mut self,
|
|
||||||
device: &wgpu::Device,
|
|
||||||
format: wgpu::TextureFormat,
|
|
||||||
shader_path: Option<&str>,
|
|
||||||
) {
|
) {
|
||||||
let new_shader = Self::load_shader(device, shader_path);
|
|
||||||
self.render_pipeline = Self::build_pipeline(device, format, &new_shader);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Renders a single frame by encoding commands into a temporary CommandEncoder and submitting them to the queue.
|
|
||||||
/// Inputs: `device` for command buffer creation, `queue` for submission, `view` as render target (from Context).
|
|
||||||
/// Called by the orchestrator after begin_frame() returns the TextureView. The pipeline is invoked once per call;
|
|
||||||
/// no persistent state is retained between frames.
|
|
||||||
pub fn render(&self, device: &wgpu::Device, queue: &wgpu::Queue, view: &wgpu::TextureView) {
|
|
||||||
// Create command encoder
|
// Create command encoder
|
||||||
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||||
label: Some("render encoder"),
|
label: Some("render encoder"),
|
||||||
});
|
});
|
||||||
|
|
||||||
// RenderPass start — depth_slice is a new required field in wgpu 30 for multisampled textures.
|
{
|
||||||
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
// Block creation so render_pass is dropped before queue submit
|
||||||
label: Some("render pass"),
|
// RenderPass start — depth_slice is a new required field in wgpu 30 for multisampled textures.
|
||||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||||
view,
|
label: Some("render pass"),
|
||||||
resolve_target: None,
|
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||||
depth_slice: None, // new field; None means no multisample resolve needed
|
view,
|
||||||
ops: wgpu::Operations {
|
resolve_target: None,
|
||||||
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
|
depth_slice: None, // new field; None means no multisample resolve needed
|
||||||
store: wgpu::StoreOp::Store,
|
ops: wgpu::Operations {
|
||||||
},
|
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
|
||||||
})],
|
store: wgpu::StoreOp::Store,
|
||||||
..Default::default()
|
},
|
||||||
});
|
})],
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
// Pipeline call — draw with empty vertex buffers (geometry defined in shader)
|
// Pipeline call — draw with empty vertex buffers (geometry defined in shader)
|
||||||
render_pass.set_pipeline(&self.render_pipeline);
|
render_pass.set_pipeline(&material.pipeline);
|
||||||
let active_buffer = self.user_buffer.as_ref().unwrap_or(&self.default_buffer);
|
|
||||||
render_pass.set_vertex_buffer(0, active_buffer.slice(..));
|
|
||||||
render_pass.draw(0..3, 0..1); // Draw the 3 vertices of the selected triangle
|
|
||||||
|
|
||||||
|
// if any indices
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
// Queue submit
|
// Queue submit
|
||||||
drop(render_pass);
|
|
||||||
queue.submit(std::iter::once(encoder.finish()));
|
queue.submit(std::iter::once(encoder.finish()));
|
||||||
}
|
}
|
||||||
/// Utility to hide the "noise" of queue submission.
|
/// Submits a completed command encoder to the GPU queue for execution.
|
||||||
|
/// Inputs: queue (GPU command submission target), encoder (completed command buffer).
|
||||||
|
/// Returns nothing — side-effect: GPU executes all recorded commands. Called by renderer code after render pass completion.
|
||||||
pub fn submit_commands(&self, queue: &wgpu::Queue, encoder: wgpu::CommandEncoder) {
|
pub fn submit_commands(&self, queue: &wgpu::Queue, encoder: wgpu::CommandEncoder) {
|
||||||
queue.submit(std::iter::once(encoder.finish()));
|
queue.submit(std::iter::once(encoder.finish()));
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-1
@@ -1,7 +1,22 @@
|
|||||||
|
//! # Vertex Module
|
||||||
|
//!
|
||||||
|
//! 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.
|
||||||
|
|
||||||
|
/// Per-vertex attribute tuple: position (3D), texture coordinate (2D), color (RGBA).
|
||||||
|
/// Must match the vertex buffer layout in PipelineCache::build_pipeline() byte-for-byte.
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
|
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
pub struct Vertex {
|
pub struct Vertex {
|
||||||
|
/// XYZ coordinates of this vertex in world space. Offset: 0 bytes.
|
||||||
pub position: [f32; 3],
|
pub position: [f32; 3],
|
||||||
|
/// UV texture coordinates. Offset: 12 bytes (after 3 × f32 = 12 bytes).
|
||||||
pub uv: [f32; 2],
|
pub uv: [f32; 2],
|
||||||
pub color: [f32; 3],
|
/// RGBA color values. Offset: 20 bytes (after 5 × f32 = 20 bytes).
|
||||||
|
pub color: [f32; 4],
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user