ajout material et pipeline_cache

This commit is contained in:
Jérôme Bousquié
2026-07-06 10:40:00 +02:00
parent 22edac6ad5
commit 6c94fc96d6
12 changed files with 464 additions and 316 deletions
+45 -28
View File
@@ -1,45 +1,62 @@
La nouvelle vision architecturale
Pour comprendre ce changement, imagine que tu veux peindre 10 tableaux différents.
# ARCHI_MESH_MATERIAL.md (Version corrigée)
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)
Il ne sait pas comment il est affiché, il sait seulement ce qu'il est.
**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.
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 définit l'apparence de l'objet.
Il est purement passif. Il ne sait pas comment il est affiché, il sait seulement ce qu'il est.
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)
Il devient beaucoup plus léger et efficace.
## 2. Le Material & PipelineCache (Le Look & La Recette)
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
renderer.render_pass.set_pipeline(&material.pipeline); // On change de recette
renderer.render_pass.set_vertex_buffer(0, &mesh.buffer); // On pose la toile
renderer.render_pass.draw(...);
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.
render_pass.set_pipeline(&material.pipeline); // On change de recette
render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..)); // On pose la toile
// Puis dessin indexé ou simple...
```
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"
Dans cette nouvelle structure, tu devras faire attention à un détail : le couplage entre le Mesh et le 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.
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`.
2. **Modularité :** Tu peux combiner n'importe quel Mesh avec n'importe quel Material.
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
View File
@@ -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`).
- **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.
**Rôle :** Encapsule la complexité du système de fenêtrage et du swapchain. Orchestre les transactions GPU via `begin_frame()` et `end_frame()`.
---
## 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).
- **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.
**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.
---
## 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
View File
@@ -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
// In your Renderer (or Context)
pub fn render(&self) -> Result<(), WsgError> {
// 1. Acquire the texture to draw on
let frame = self.surface.get_current_texture()
.map_err(|_| WsgError::SurfaceIncompatible)?;
// 2. Create the view (the "channel" to the texture)
let view = frame.texture.create_view(&wgpu::TextureViewDescriptor::default());
// 3. Create the command encoder
let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Render Encoder"),
});
// --- This is where we'll draw later ---
// Le Renderer ne connait plus la surface, il reçoit la vue
pub fn render(
&self,
device: &wgpu::Device,
queue: &wgpu::Queue,
view: &wgpu::TextureView,
mesh: &Mesh,
material: &Material
) {
let mut encoder = device.create_command_encoder(...);
{
let _render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Render Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
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,
});
}
let mut render_pass = encoder.begin_render_pass(...);
render_pass.set_pipeline(&material.pipeline); // Recette via Material
render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
// ... dessin ...
} // render_pass est automatiquement drop ici
// 4. Submit and present
self.queue.submit(std::iter::once(encoder.finish()));
frame.present();
Ok(())
queue.submit(std::iter::once(encoder.finish()));
}
```
### 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
## 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 |
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.