Compare commits

...

10 Commits

Author SHA1 Message Date
Jérôme Bousquié c5f8edc4f4 roadmap 2026-07-30 17:54:06 +02:00
Jérôme Bousquié 2887cba121 typo 2026-07-29 21:04:48 +02:00
Jérôme Bousquié b1966cb341 doc arene pour slotmap 2026-07-29 21:00:18 +02:00
Jérôme Bousquié 2710899292 ajout docs ARENE et GPU_CPU 2026-07-28 21:25:14 +02:00
Jérôme Bousquié d782469202 architecture rendu update/render 2026-07-08 16:26:23 +02:00
Jérôme Bousquié 3414d68819 Plan 2026-07-08 15:58:20 +02:00
Jérôme Bousquié 7b25564483 doc 2026-07-08 15:46:50 +02:00
Jérôme Bousquié 724b658896 modif appbuilder 2026-07-08 12:29:18 +02:00
Jérôme Bousquié 82ea16d118 re-org en App 2026-07-07 16:42:37 +02:00
Jérôme Bousquié cfd8b421a2 plan passage à App 2026-07-07 11:57:57 +02:00
48 changed files with 1543 additions and 2731 deletions
+1
View File
@@ -1,2 +1,3 @@
/target
examples/target
lib/examples/target
Generated
+15 -9
View File
@@ -1076,6 +1076,20 @@ name = "pollster"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
dependencies = [
"pollster-macro",
]
[[package]]
name = "pollster-macro"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac5da421106a50887c5b51d20806867db377fbb86bacf478ee0500a912e0c113"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "portable-atomic"
@@ -2304,20 +2318,12 @@ version = "0.57.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
[[package]]
name = "wsg-examples"
version = "0.1.0"
dependencies = [
"pollster",
"winit",
"wsg-lib",
]
[[package]]
name = "wsg-lib"
version = "0.1.0"
dependencies = [
"bytemuck",
"pollster",
"thiserror 2.0.18",
"wgpu",
"winit",
+1 -1
View File
@@ -1,3 +1,3 @@
[workspace]
members = ["lib", "examples"]
members = ["lib"]
resolver = "2"
+78 -11
View File
@@ -1,16 +1,83 @@
# WSG - WGPU Simple Graphics Library
A simple WGPU wrapper to expose basic objects for drawing and manipulation: Meshes, Vertices, Indexes, UVs
WSG is a Rust library that wraps [wgpu](https://github.com/gfx-rs/wgpu) to provide a simple, declarative API for 3D graphics. It abstracts away the complexity of managing GPU resources while exposing low-level primitives for advanced users.
| Component | Ownership | Role |
|-----------|-----------|------|
| Instance | wgpu | The entry point. It manages connections with graphics drivers (Vulkan, Metal, DX12). |
| Surface | wgpu | The link between wgpu and your window (winit). This is where rendering is displayed. |
| Adapter | wgpu | Represents your GPU (physical or software). |
| Device | wgpu | The engine's core. It creates buffers, textures, and pipelines. |
| Queue | wgpu | The queue. You send drawing commands for execution. |
| Context | Our Lib | A logical container. wgpu doesn't have a "Context" object; we create it to group these disparate objects and simplify your user API. |
## What it does
Context (Lib) : Initializes the GPU, creates the surface, and holds the Device and Queue. It is static (created once at startup).
WSG provides two complementary workflows:
Renderer (Lib) : Uses the Device to create pipelines, manages your 500,000 vertices, and uses the Queue to send rendering instructions each frame. It is dynamic (it changes depending on what you want to display).
### Declarative workflow (recommended)
Register your scene's resources and entities before the render loop starts, then iterate them each frame:
```rust
use wsg_lib::{App, AppHandler};
use wsg_lib::resources::{Mesh, Material, Vertex};
struct MyGame { /* ... */ }
impl AppHandler for MyGame {
fn render(&mut self, app: &mut App) {
// Draw every entity registered in app.scene
for (_, mesh, material) in app.scene.iter_entities() {
app.renderer.render(app.context.get_next_frame().view(), mesh, material);
}
}
}
#[pollster::main]
async fn main() -> Result<(), wsg_lib::utils::WsgError> {
let mut app = AppBuilder::new().build().await?;
// Declare resources
app.cache.register_shader("basic", "assets/shaders/basic_shader.wgsl")?;
let vertices: [Vertex; 4] = [/* ... */];
let indices: [u16; 6] = [0, 1, 2, 0, 2, 3];
let mesh = Mesh::new(app.context.device(), &vertices, Some(&indices));
let material = Material::new(app.renderer.format(), "basic", &mut app.cache);
app.scene.add_mesh("quad", Arc::new(mesh))?;
app.scene.add_material("mat", Arc::new(material))?;
app.scene.add_entity("my_quad", "quad", "mat")?;
// Run the render loop
app.run(MyGame {})
}
```
### Manual workflow
For fine-grained control, bypass the Scene facade entirely and manipulate Context, Renderer, and PipelineCache directly through their public APIs.
## Architecture overview
WSG follows a two-layer architecture:
- **Manager layer (Context)** — owns GPU hardware lifecycle (Instance → Surface → Adapter → Device → Queue). Created once at startup.
- **Executor layer (Renderer)** — orchestrates draw calls per frame by binding Materials + Meshes into RenderPasses. Dynamic, changes each frame.
The high-level `App` facade ties everything together, automating window lifecycle, event processing, and frame presentation. Users implement the `AppHandler` trait to inject game logic.
## Quick reference
| Concept | Type | Responsibility |
|---------|------|---------------|
| App | Facade | Window lifecycle + event loop + render automation |
| AppHandler | Trait | User-defined update/render callbacks |
| Scene | Struct | Resource depot + entity graph (declarative) |
| Context | Struct | GPU hardware lifecycle (Manager) |
| Renderer | Struct | Draw call orchestration (Executor) |
| Material | Struct | Shader ID → compiled RenderPipeline |
| Mesh | Struct | Persistent GPU geometry container |
| Vertex | Struct | CPU-side vertex attribute tuple |
| PipelineCache | Struct | Shader compilation cache |
| Frame | Struct | Per-frame RAII wrapper for surface texture + view |
## Getting started
```bash
cargo add wsg-lib # Add the dependency
# Then build your app following the declarative example above
```
For details on the architecture and internal modules, see [ARCHI_APP](docs/ARCHI_APP.md).
-7
View File
@@ -1,7 +0,0 @@
# Shaders Directory
Contains WGSL shader source files compiled at runtime by wgpu. Each file defines vertex and fragment stages for a specific rendering pipeline.
## Files
- **basic_shader.wgsl** — Simple triangle shader used as a minimal example. Renders a solid red triangle using hardcoded positions computed entirely on the GPU side.
-24
View File
@@ -1,24 +0,0 @@
//! # Basic Shader Module
struct VertexInput {
@location(0) position: vec3<f32>,
@location(1) uv: vec2<f32>,
@location(2) color: vec3<f32>,
};
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) color: vec3<f32>,
};
@vertex
fn vs_main(model: VertexInput) -> VertexOutput {
var out: VertexOutput;
out.clip_position = vec4<f32>(model.position, 1.0);
out.color = model.color; // On transmet la couleur au fragment shader
return out;
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
return vec4<f32>(in.color, 1.0);
}
+82
View File
@@ -0,0 +1,82 @@
# Architecture du Moteur wsg_lib
wsg_lib est un moteur de rendu modulaire basé sur wgpu. Il adopte une architecture à deux niveaux : une façade de haut niveau pour la productivité et un accès bas niveau pour un contrôle total.
## 1. Philosophie et Principes
- **Abstraction vs Transparence** : Le moteur masque la complexité (wgpu, winit, gestion des Frame) via `App`, tout en exposant les briques élémentaires pour les utilisateurs avancés.
- **Approche orientée Scène** : Le rendu repose sur la composition d'une `Scene` contenant les entités, matériaux et géométries.
- **Pipeline Data-Driven** : Les ressources (Shaders, Meshes, Materials) sont découplées. Le `PipelineCache` gère automatiquement la compilation et la réutilisation des pipelines GPU.
## 2. Organisation des Modules (`lib/src/`)
- **`core/`** : Plomberie système (`Context`, `Renderer`, `Frame`). Accès bas niveau.
- **`pipeline/`** : `PipelineCache` pour la gestion des états GPU et shaders.
- **`resources/`** : Données (`Mesh`, `Material`, `Vertex`).
- **`scene/`** : Hiérarchie et stockage des objets à visualiser (Entités, Transformations).
- **`shaders/`** : Assets WGSL.
- **`utils/`** : Utilitaires transverses.
## 3. Interfaces de Haut Niveau (`App` & `AppHandler`)
### L'objet `App`
La façade `App` orchestre la boucle de jeu. Elle encapsule :
- Le cycle de vie de la fenêtre.
- La boucle d'événements.
- La gestion automatique des Frame (acquisition et présentation).
### Le trait `AppHandler`
L'utilisateur implémente ce trait pour définir la logique métier :
```rust
pub trait AppHandler {
// Appelé avant la préparation de la frame
fn update(&mut self, _app: &mut App) {}
// Appelé au moment de la présentation
fn render(&mut self, app: &mut App);
}
```
## 4. Workflow et Cycle de Vie
### A. Initialisation (Configuration)
- **Shaders** : Chargés avant la renderloop.
- **PipelineCache** : Enregistre les shaders.
- **Matériaux** : Créés avec un shader associé. Un mesh sans matériau explicite utilise `basic_shader` par défaut.
- **Scene** : Assemblage des objets. L'utilisateur peuple la scène via `app.scene`.
### B. Boucle de Rendu (Automatisée)
Le moteur gère la renderloop interne :
1. **Update** : Appel à `AppHandler::update`.
2. **Acquisition** : Gestion interne de `wgpu::SurfaceTexture`.
3. **Render** : Appel à `AppHandler::render` où l'utilisateur exécute `app.render(scene)`.
4. **Présentation** : Gestion interne de `present()`.
## 5. Accès Avancé
Les utilisateurs souhaitant ignorer l'abstraction `App` peuvent accéder directement à :
- `wsg_lib::core::Context` et `Renderer` pour gérer manuellement les RenderPass.
- `wsg_lib::pipeline::PipelineCache` pour des besoins de shaders personnalisés.
- `winit` pour la gestion précise des événements système.
## 6. Structure des données (pour LLM)
```
App (Facade) -> Scene (Conteneur) -> Entities -> Mesh + Material (Shader)
|
+-> Renderer (WGPU) <-> PipelineCache (Shaders)
```
## Notes pour l'implémentation future
- `app.render(scene)` : Cette méthode doit devenir l'API principale pour le rendu de la scène complète.
- Trait `AppHandler` : Il est recommandé de faire passer la `Scene` ou une référence à celle-ci comme argument ou de permettre à `AppHandler` d'être le lieu où la `Scene` est manipulée (ex : `MyGame { scene: Scene, ... }`).
- `PipelineCache` : Son utilisation doit être invisible pour l'utilisateur standard lors de la création d'un `Material`.
+209
View File
@@ -0,0 +1,209 @@
# Fiche Technique : Gestion des Ressources avec des Arènes Générationalles (`slotmap`)
Cette fiche technique détaille l'implémentation recommandée pour gérer efficacement et en toute sécurité les ressources (maillages, textures, matériaux, lumières, etc.) au sein du moteur graphique WSG. Nous utilisons le concept d'**arène générationalle**, implémenté via la crate `slotmap`, pour bénéficier d'IDs stables, de performances optimales, de sécurité accrue et de fonctionnalités avancées comme les `SecondaryMap`.
## Objectifs
* **Stabilité des IDs :** Garantir que les identifiants (Keys) des ressources restent valides même si d'autres ressources sont supprimées.
* **Performance :** Accéder aux ressources via un ID de manière aussi rapide que possible (accès quasi direct via index).
* **Sécurité :** Empêcher l'utilisation accidentelle d'IDs obsolètes ("Dangling IDs") qui pointeraient vers des objets supprimés ou réaffectés.
* **Flexibilité :** Permettre l'ajout et la suppression de ressources dynamiquement.
* **Extensibilité Future :** Profiter des fonctionnalités avancées de `slotmap` comme les `SecondaryMap` pour attacher des données dynamiques ou transitoires aux ressources existantes sans modifier leur structure principale.
## Concepts Clés
### 1. Arène Typée
Chaque type de ressource nécessite une arène séparée. Par exemple :
* `SlotMap<MeshId, Mesh>` pour stocker les `Mesh`
* `SlotMap<MaterialId, Material>` pour stocker les `Material`
* `SlotMap<TextureId, Texture>` pour stocker les `Texture`
* `SlotMap<LightId, Light>` pour stocker les `Light`
Cela permet d'optimiser l'accès et de garantir la cohérence des types.
### 2. Handles (Identifiants) Typés
Un Handle est un objet spécial généré par l'arène lors de l'insertion d'une ressource. Il sert de référence stable à cette ressource. Nous utilisons des types personnalisés (struct wrappers) pour typer fortement ces Handles, empêchant les erreurs de mélange entre types de ressources.
### 3. Génération (Generation)
Pour renforcer la sécurité, chaque Handle encapsule non seulement un **index** (où l'objet est stocké dans le tableau interne de l'arène), mais aussi un numéro de **génération**. Lorsqu'un objet est supprimé, l'emplacement dans le tableau interne est marqué comme vide, mais le numéro de génération associé à cet emplacement est incrémenté. Lorsque ce même emplacement est réutilisé pour un nouvel objet, le nouvel objet reçoit le même index mais une génération plus récente. Si un ancien Handle (avec un index et une ancienne génération) est utilisé pour tenter d'accéder à l'arène, le système vérifie si la génération du Handle correspond à celle stockée à l'index. Si ce n'est pas le cas, l'accès est refusé, empêchant l'utilisation d'un Handle périmé.
## Implémentation avec `slotmap`
### Dépendance
Ajoutez `slotmap` à votre `Cargo.toml` :
```toml
[dependencies]
slotmap = { version = "1.0", features = ["serde"] } # Inclure 'serde' si nécessaire, sinon omettre la feature
```
(Note : slotmap a une dépendance sur serde par défaut. Si vous n'avez absolument pas besoin de sérialisation/désérialisation des arènes, vous pouvez potentiellement chercher une alternative légère comme thunderdome, mais slotmap est le standard et offre plus de fonctionnalités).
## Structure de Base et Typage Fort
```rust
use slotmap::{SlotMap, new_key_type};
// --- Définition des types de ressources ---
// Ces structs doivent être définies ailleurs dans votre code
#[derive(Debug)]
pub struct Mesh {
// ... champs du mesh ...
}
#[derive(Debug)]
pub struct Material {
// ... champs du material ...
}
#[derive(Debug)]
pub struct Texture {
// ... champs de la texture ...
}
#[derive(Debug)]
pub struct Light {
// ... champs de la lumière ...
}
// --- Définition des Handles typés ---
// Ces lignes créent des types uniques pour chaque Handle
new_key_type! { pub struct MeshId; }
new_key_type! { pub struct MaterialId; }
new_key_type! { pub struct TextureId; }
new_key_type! { pub struct LightId; }
// --- Définition des arènes ---
pub struct ResourceManager {
meshes: SlotMap<MeshId, Mesh>,
materials: SlotMap<MaterialId, Material>,
textures: SlotMap<TextureId, Texture>,
lights: SlotMap<LightId, Light>,
// Ajoutez d'autres arènes pour d'autres types si nécessaire
}
impl ResourceManager {
pub fn new() -> Self {
// Optionnel : spécifier une capacité initiale estimée pour chaque arène
// let estimated_mesh_count = 100;
// let meshes = SlotMap::with_capacity_and_key(estimated_mesh_count);
// ...
Self {
meshes: SlotMap::new(),
materials: SlotMap::new(),
textures: SlotMap::new(),
lights: SlotMap::new(),
}
}
// --- Méthodes pour ajouter des ressources ---
pub fn add_mesh(&mut self, mesh: Mesh) -> MeshId { // Retourne un Handle typé
self.meshes.insert(mesh)
}
pub fn add_material(&mut self, material: Material) -> MaterialId {
self.materials.insert(material)
}
pub fn add_texture(&mut self, texture: Texture) -> TextureId {
self.textures.insert(texture)
}
pub fn add_light(&mut self, light: Light) -> LightId {
self.lights.insert(light)
}
// --- Méthodes pour accéder aux ressources ---
pub fn get_mesh(&self, handle: MeshId) -> Option<&Mesh> {
self.meshes.get(handle)
}
pub fn get_material(&self, handle: MaterialId) -> Option<&Material> {
self.materials.get(handle)
}
pub fn get_texture(&self, handle: TextureId) -> Option<&Texture> {
self.textures.get(handle)
}
pub fn get_light(&self, handle: LightId) -> Option<&Light> {
self.lights.get(handle)
}
// --- Méthodes pour accéder aux ressources mutables (utile dans update(), mais à éviter pendant le rendu) ---
pub fn get_mesh_mut(&mut self, handle: MeshId) -> Option<&mut Mesh> {
self.meshes.get_mut(handle)
}
pub fn get_material_mut(&mut self, handle: MaterialId) -> Option<&mut Material> {
self.materials.get_mut(handle)
}
pub fn get_texture_mut(&mut self, handle: TextureId) -> Option<&mut Texture> {
self.textures.get_mut(handle)
}
pub fn get_light_mut(&mut self, handle: LightId) -> Option<&mut Light> {
self.lights.get_mut(handle)
}
// --- Méthodes pour supprimer des ressources ---
pub fn remove_mesh(&mut self, handle: MeshId) -> Option<Mesh> { // Option<T> retourné est la ressource supprimée
self.meshes.remove(handle)
}
pub fn remove_material(&mut self, handle: MaterialId) -> Option<Material> {
self.materials.remove(handle)
}
pub fn remove_texture(&mut self, handle: TextureId) -> Option<Texture> {
self.textures.remove(handle)
}
pub fn remove_light(&mut self, handle: LightId) -> Option<Light> {
self.lights.remove(handle)
}
// --- Méthode pour vérifier si un Handle est toujours valide ---
pub fn contains_mesh(&self, handle: MeshId) -> bool {
self.meshes.contains_key(handle)
}
pub fn contains_material(&self, handle: MaterialId) -> bool {
self.materials.contains_key(handle)
}
pub fn contains_texture(&self, handle: TextureId) -> bool {
self.textures.contains_key(handle)
}
pub fn contains_light(&self, handle: LightId) -> bool {
self.lights.contains_key(handle)
}
}
```
# Bonnes Pratiques d'Utilisation
1. Initialisation Groupée : Encouragez les utilisateurs de WSG à créer la majorité de leurs ressources statiques (maillages de niveau, matériaux de base, textures fixes, lumières ambiantes, etc.) avant de lancer la boucle de rendu principale. Vous pouvez éventuellement fournir une fonction reserve_initial_capacities(&mut resource_manager, expected_counts...) qui appelle SlotMap::reserve() pour optimiser la mémoire initiale.
2. Stocker les Handles Typés : Les entités de la scène (ou les objets graphiques) doivent stocker les Handles typés (MeshId, MaterialId, etc.) retournés lors de l'ajout des ressources. Par exemple, un objet GameObject pourrait contenir un Option<MeshId> pour son Mesh, un Option<MaterialId> pour son Material, etc. Le typage fort empêche les erreurs de mélange.
3. Accès pendant le rendu : Pendant la phase de rendu (render()), accédez aux ressources via les Handles typés stockés. Utilisez get() (lecture seule) pour éviter les conflits avec les systèmes de mise à jour concurrents.
4. Accès pendant la mise à jour : Pendant la phase de mise à jour (update()), vous pouvez utiliser get_mut() si des modifications sont nécessaires. Soyez vigilant à la gestion des lifetimes et de la mutabilité.
5. Validation : Avant d'utiliser un Handle potentiellement ancien ou incertain, vérifiez sa validité avec contains_* si l'opération n'est pas critique, ou laissez get() renvoyer None si le Handle est invalide.
6. Suppression Dynamique : Bien que possible, la suppression de ressources pendant la boucle de rendu doit être faite avec prudence. Assurez-vous que les entités ou objets qui référençaient cette ressource soient informés ou nettoyés pour éviter d'utiliser des Handles invalides. La suppression est souvent mieux gérée en fin de frame ou via un système de "marquage pour suppression" suivi d'un nettoyage différé.
7. Futur : SecondaryMaps : slotmap permet d'utiliser des SecondaryMap pour associer dynamiquement des données à des ressources existantes sans modifier leur structure principale. Par exemple, SecondaryMap<MeshId, Transform> pourrait stocker les transformations actuelles de chaque maillage. Cela peut être utile pour le rendu ou pour des systèmes de physique/transformation indépendants.
# Avantages de cette Approche
* Simplicité d'utilisation : Les développeurs utilisent des Handles typés stables, sans se soucier des références Rust ou des lifetimes complexes pour les ressources partagées.
* Performance : Les accès sont rapides, proches de l'accès direct via index, grâce à l'implémentation interne de slotmap.
* Sécurité : Le système de génération empêche efficacement l'utilisation de Handles invalides, ce qui peut causer des plantages ou des bugs subtils.
* Conformité avec Rust : Respecte les principes de propriété et de sécurité mémoire de Rust sans recourir à Rc<RefCell<T>> ou d'autres constructions potentiellement coûteuses ou moins sûres pour la gestion partagée des ressources.
* Typage Fort : Les types MeshId, MaterialId, etc., empêchent les erreurs de compilation liées au mélange de Handles de types différents.
* Extensibilité : L'écosystème slotmap (SecondaryMap) offre des perspectives pour des architectures plus complexes à l'avenir.
+53
View File
@@ -0,0 +1,53 @@
Architecture de Rendu 3D GPU-Driven avec wGPU :
Bonnes Pratiques & Guide d'Implémentation
Ce document sert de spécification technique et de trame d'implémentation pour l'architecture de rendu 3D pilotée par le GPU (GPU-Driven Rendering) utilisant wgpu. L'objectif est de déléguer un maximum de charges de calcul au GPU pour soulager le CPU et maximiser les performances de parallélisme.
1. Répartition des Rôles : CPU vs GPU (La Source de Vérité)
Pour éviter les goulets d'étranglement dus aux allers-retours sur le bus PCIe, la règle d'or est la suivante : Le CPU est le cerveau logique, le GPU est l'exécutant visuel.
Côté CPU (Source de Vérité)
- Ce qu'il conserve : Les données logiques et les transformations brutes des objets (ex: Vec<Transform> contenant la position, la rotation, et l'échelle).
- Ce qu'il fait : Il gère la logique de jeu, l'IA, le réseau et les interactions globales.
- Ce qu'il ne fait plus : Il ne calcule plus les matrices de transformation mondiales (World Matrices) en masse, et ne fait plus de tests de visibilité unitaires.
Côté GPU (Exécutant Autonome)
- Ce qu'il calcule : Les World Matrices, le Frustum Culling, et la génération des listes de dessin indirectes.
- Ce qu'il conserve : Les buffers de données persistants en VRAM (Storage Buffers) qui vivent d'une frame à l'autre sans jamais redescendre vers le CPU.
2. Le Pipeline d'Exécution par Frame (Ordre des Passes)
L'exécution des tâches s'appuie sur une structure séquentielle stricte au sein d'un même CommandEncoder. Le driver et wGPU s'occupent des barrières de mémoire implicites entre chaque étape.
```
[ CPU : Envoi des Transforms bruts ]
[ Pass 1 : Compute (Calcul World Matrices + Frustum Culling + Indirect Draw Buffer) ]
↓ (Barrière de mémoire automatique gérée par le driver)
[ Pass 2 : Render (Draw Indexed Indirect basé sur les objets visibles) ]
```
Étape par étape :
- Mise à jour CPU (Minimaliste) : Le CPU écrit les transformations brutes (Transform) modifiées dans un buffer GPU mappé (via un mécanisme de Double Buffering pour éviter les conflits de lecture/écriture).
- Pass de Calcul (Compute Pass) :
- Calcul des World Matrices : Un compute shader lit les transformations brutes et génère la matrice 4x4 finale pour chaque mesh.
- Frustum Culling GPU : Le même compute shader (ou un compute pass dédié) compare la Bounding Box (AABB) de chaque objet avec les plans de la caméra (matrice de projection/vue).
- Remplissage du Buffer Indirect : Si l'objet est visible, son identifiant est injecté dans un buffer de commandes de dessin indirect (Indirect Draw Buffer).
- Pass de Rendu (Render Pass) :
- Le CPU émet une unique commande globale : draw_indexed_indirect.
- Le GPU pioche directement dans le buffer préparé par le compute pass et dessine uniquement les objets visibles, sans intervention du CPU.
3. Stratégie de Synchronisation
- Sécurité de l'ordre : L'ordre d'appel des méthodes sur le CommandEncoder (begin_compute_pass suivi de begin_render_pass) garantit l'ordre d'exécution séquentiel sur le GPU.
- Barrières de mémoire : Le pilote insère automatiquement les barrières nécessaires pour s'assurer que le buffer de la WorldMatrix et le buffer Indirect sont complètement écrits par le compute shader avant d'être lus par le render pipeline.
- Éviter le Readback (map_async) : Sauf cas exceptionnel (débug ou interaction scriptée critique), aucune donnée géométrique ou de position ne doit remonter du GPU vers le CPU. Le CPU fait confiance à sa propre structure de données initiale pour la logique métier.
4. Synthèse des Structures de Données en VRAM
Pour implémenter cette architecture, prévoyez l'utilisation des buffers wGPU suivants :
Nom du Buffer,Rôle,Type wGPU,Direction du flux
Transform Buffer,Stocke les positions/rotations/échelles brutes.,Storage Buffer,CPU → GPU
Matrix Buffer,Stocke les World Matrices finales calculées.,Storage Buffer,GPU (Calculé) → GPU (Lu par le Render)
Bounding Box Buffer,Stocke les AABB de chaque mesh pour le culling.,Storage Buffer,CPU → GPU (Statique)
Indirect Draw Buffer,Contient la liste dynamique des objets à dessiner.,Indirect Buffer + Storage,GPU (Rempli par Compute) → GPU (Lu par Render)
-41
View File
@@ -1,41 +0,0 @@
# ARCHI_MESH_MATERIAL.md
## La nouvelle vision architecturale : L'Atelier de Rendu
Pour comprendre notre structure, imagine que tu veux peindre 10 tableaux différents.
**Avant :** Chaque tableau possédait sa propre cuisine et ses propres outils. C'était inefficace.
**Après :** Tu as un Atelier (`Renderer`) qui orchestre le dessin et possède les outils de base (`Device`, `Queue`, `Format`). Il utilise des Recettes (`Material` + `PipelineCache`) pour définir l'apparence et traite des Toiles (`Mesh`) pour la géométrie.
---
## 1. Le Mesh (La Géométrie)
Il est purement passif. Il ne sait pas comment il est affiché, il sait seulement ce qu'il est.
- **Contenu :** `vertex_buffer`, optionnellement `index_buffer`, et les compteurs (`num_vertices`, `num_indices`).
- **Rôle :** Fournir les données brutes au GPU.
---
## 2. Le Material & PipelineCache (Le Look & La Recette)
Le look est découplé de la géométrie via une gestion centralisée.
- **PipelineCache :** 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.
- **Material :** 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.
---
## 3. Le Renderer (L'Orchestrateur propriétaire)
Le `Renderer` a été promu au rang de propriétaire des ressources matérielles.
- **Contenu :** `device`, `queue`, `format`.
- **Rôle :**
1. **Initialisation :** Reçoit le `Context` au démarrage et s'approprie ses ressources.
2. **Exécution :** Orchestre les appels GPU en utilisant ses ressources internes. Il expose une API simplifiée qui ne demande plus à l'utilisateur de manipuler le `device` ou la `queue`.
3. **Présentation :** Possède la méthode `present(frame)` qui utilise sa `queue` interne pour afficher l'image.
```rust
// Exemple d'orchestration simplifiée dans main.rs
renderer.render(frame.view(), &mesh, &material);
renderer.present(frame); // Plus besoin de passer la queue !
-49
View File
@@ -1,49 +0,0 @@
# Three-Layer Structure
## Manager Layer (Context)
**Responsabilité :** Propriétaire initial du cycle de vie des ressources matérielles (`Device`, `Queue`, `Surface`).
**Rôle :** Encapsule la complexité du système de fenêtrage et du swapchain. Il fournit les capacités brutes au `Renderer` lors de son initialisation.
---
## Specialist Layer (Renderer & PipelineCache)
**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.
**Renderer (L'Exécuteur propriétaire) :** Il est devenu le propriétaire des ressources matérielles (`Device`, `Queue`, `Format`). Il est agnostique du contenu graphique : il orchestre dynamiquement le rendu des `Mesh` via les `Material` et gère seul la présentation des `Frame`.
---
## Orchestrator Layer (main.rs)
**Responsabilité :** Logique métier et boucle d'exécution.
**Rôle :** Coordonne le `Context` (pour l'init matérielle), le `PipelineCache` (pour les shaders), et délègue l'exécution au `Renderer`. Il ne manipule plus directement le `Device` ou la `Queue` après la création du `Renderer`.
---
# Updated Benefits Analysis
## Modularité Totale (Decoupling)
Le `Renderer` est totalement découplé du contenu graphique. Il sait "lier" un `Material` à un `Mesh` en utilisant ses propres ressources internes.
## 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.
## Encapsulation & Robustesse
En transférant la propriété de `Device` et `Queue` au `Renderer`, on élimine les erreurs de transmission de références. Le `main.rs` devient plus léger et le `Renderer` devient un point d'entrée unique et sécurisé pour toutes les commandes GPU.
## Flexibilité
Le passage à un modèle (`Mesh` + `Material`) permet de combiner n'importe quelle géométrie avec n'importe quel effet visuel, tout en laissant le `Renderer` gérer la boucle de présentation de manière autonome (ex: `renderer.present(frame)`).
---
# Summary
Cette structure transforme une architecture rigide en un atelier de rendu dynamique. Le `Context` prépare le terrain, le `Renderer` devient le propriétaire des ressources et l'orchestrateur de dessin, le `PipelineCache` fournit les outils, le `Material` définit le style et le `Mesh` apporte la forme. Le système est désormais prêt à gérer des scènes complexes avec une API propre et sécurisée.
+47
View File
@@ -0,0 +1,47 @@
# Architecture de Rendu : Cycle Update/Render et Gestion des Données
Ce document définit la stratégie de gestion de la mutabilité et des données du moteur wsg_lib, conçue pour maximiser la performance et garantir la sécurité mémoire via Rust.
## 1. La Dichotomie Update / Render
Pour éviter les conflits de données et optimiser le pipeline GPU, le moteur sépare strictement le cycle de vie de la frame en deux phases :
### Phase Update (Mutabilité Totale)
- L'utilisateur peut modifier librement l'état de la Scene (transformations, propriétés des matériaux, ajout/suppression d'entités).
- C'est l'unique zone de mutation autorisée. Le système est en "lecture-écriture".
### Phase Render (Lecture et Orchestration)
- La Scene est considérée comme immuable vis-à-vis du rendu.
- Le moteur itère automatiquement sur les entités pour soumettre les commandes au GPU.
- L'utilisateur dispose d'une "trappe" via `AppHandler::render()` pour injecter du code de rendu personnalisé, mais sans modifier l'état métier des objets.
## 2. Gestion des Données : Indirection par ID (Handle)
Pour contourner les limitations du Borrow Checker de Rust lors de l'accès aux ressources, le moteur utilise une approche par **Indirection (Handles/IDs)**.
- **HashMaps et Vecs indexés** : Les ressources (`Mesh`, `Material`) ne sont pas stockées sous forme de références directes (`&Mesh`) dans les entités. Elles sont stockées dans des conteneurs centralisés dans la Scene.
- **Identifiants (Handles)** : Chaque entité possède un `MeshId` ou `MaterialId`.
- **Avantage** : Cela élimine les problèmes de durées de vie (lifetimes) complexes. Vous pouvez passer des IDs partout sans bloquer la mutabilité des conteneurs parents.
- **Performance** : Cette approche permet au moteur de trier les entités par `MaterialId` avant le rendu, réduisant drastiquement les changements d'état GPU (**State Change Overhead**).
## 3. Points d'Attention du Borrow Checker
Bien que cette architecture facilite la gestion de la mémoire, des règles strictes s'appliquent à `App` :
- **Conflit de Mutabilité** : App contient à la fois la Scene et le Renderer. Il est interdit d'emprunter `&mut scene` et `&mut renderer` simultanément.
- **Solution** : Dans la boucle de rendu interne (`App::run`), le moteur doit être structuré pour séquencer les accès : `let scene = &app.scene;` suivi de `let renderer = &mut app.renderer;` puis `renderer.render_scene(scene);`.
- **Séparation des Responsabilités** : Le RenderLoop doit posséder la main sur l'ordonnancement pour éviter que l'utilisateur ne tente de muter la scène pendant que le renderer est en train de lire les données.
## 4. Synthèse des Avantages
- **Performance (Batching)** : Le rendu automatique par le moteur permet d'implémenter des stratégies de rendu optimales invisibles pour l'utilisateur.
- **Ergonomie** : L'utilisateur n'écrit pas de boucles de rendu complexes. Il se concentre sur sa logique métier dans `update()`.
- **Sécurité** : L'utilisation d'IDs évite les cycles de références et les références pendantes, rendant le code plus sûr et plus facile à maintenir.
## 5. Guide de Développement pour l'Utilisateur
> "Si vous devez changer la position d'un objet ou son matériau, faites-le dans `update()`. Si vous avez besoin d'afficher un élément de debug ou un rendu spécial, faites-le dans `render()`, mais traitez les objets de la scène comme des données en lecture seule."
Cette structure permet au projet d'être extrêmement scalable. L'ajout futur de fonctionnalités (Lumières, Textures, Caméras) ne nécessitera que d'ajouter de nouveaux conteneurs dans la Scene et de mettre à jour le système de tri dans `Renderer::render_scene()`.
-33
View File
@@ -28,36 +28,3 @@ Avec notre nouvelle architecture "Atelier", la distinction est devenue encore pl
| 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
// 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 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
queue.submit(std::iter::once(encoder.finish()));
}
```
---
## 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.
+67
View File
@@ -0,0 +1,67 @@
# Plan d'Implémentation : Consolidation et Finalisation du Moteur wsg_lib
Ce plan définit les étapes prioritaires pour stabiliser l'architecture actuelle. L'objectif est de rendre l'API intuitive pour l'utilisateur standard tout en conservant la puissance de contrôle pour l'utilisateur avancé.
## Phase 1 : Finalisation et Nettoyage de l'Existant (Priorité Absolue)
Cette phase vise à supprimer la dette technique et à unifier les accès.
### Uniformisation des Modules
- Vérifier que tous les traits (`AppHandler`) et structures (`App`, `Context`) sont explicitement marqués `pub` dans leurs fichiers sources.
- Ré-exporter l'API dans `lib.rs` pour permettre des imports simplifiés (ex : `use wsg_lib::{App, AppHandler}`).
- Nettoyer les accès internes pour que l'utilisateur n'ait pas à importer les modules système (`core`, `pipeline`) sauf besoin spécifique.
### Abstraction de la Boucle (`App::run`)
- Déplacer la gestion de `winit::event_loop` et des `Frame` à l'intérieur de la méthode `run()` de `App`.
- Garantir que le trait `AppHandler` reçoit une référence à `App` permettant d'appeler `app.renderer` ou `app.scene`.
- Supprimer toute gestion de `Frame` ou `EventLoop` manuelle des exemples utilisateurs (`simple.rs`).
### Correction du Builder et Initialisation
- Standardiser la création de `App` via un `AppBuilder` robuste.
- Gérer les `dev-dependencies` dans `lib/Cargo.toml` (notamment `pollster` avec la feature `macro`) pour permettre la compilation des exemples sans polluer les dépendances finales de la librairie.
## Phase 2 : Structure de Rendu et Scène
Une fois la plomberie encapsulée, nous devons rendre l'assemblage des objets cohérent.
### Intégration de la Scene
- Formaliser la structure `Scene` : un conteneur qui liste les Entities.
- Associer le `PipelineCache` à la Scene pour que le rendu des matériaux soit automatique.
- Implémenter la logique `app.render(scene)` : cette méthode doit parcourir la scène, récupérer les matériaux, gérer les pipelines via le cache, et soumettre les draw calls.
### Gestion des Matériaux et Shaders
- S'assurer que chaque Mesh possède une référence vers un Material.
- Implémenter le comportement par défaut : si aucun matériau n'est assigné, le moteur injecte automatiquement le `basic_shader`.
## Phase 3 : Documentation et Interface (API "User-Friendly")
### Refonte des Exemples
- `simple.rs` doit devenir le modèle : **15 lignes** de code, pas de manipulation WGPU explicite.
- `manual.rs` doit rester disponible en tant que tutoriel pour ceux qui veulent contourner l'abstraction App.
### Nettoyage du Code Interne
- Vérifier les durées de vie (lifetimes) et les Arc pour s'assurer qu'aucune fuite mémoire ou accès concurrentiel invalide ne survient lors des changements de frame.
## Phase 4 : Nouvelles Fonctionnalités (Planification Future)
Une fois les phases 1 à 3 validées, nous pourrons introduire :
- **Système de Lumières** : Ajout de buffers d'uniformes dans le PipelineCache.
- **Textures** : Intégration d'un module de chargement d'images et de BindGroups.
- **Caméras** : Gestion des matrices de projection/vue dans la Scene.
## Check-list de Vérification pour le LLM d'Assistance
- [ ] Est-ce que `simple.rs` compile sans importer `winit` ou `wgpu` ?
- [ ] Est-ce que `App::run` gère bien le cycle update → render → present ?
- [ ] Les modules sont-ils bien exposés via `lib.rs` ?
- [ ] `pollster` est-il uniquement en dev-dependencies ?
Ce plan garantit que les fondations sont saines. Une fois la Scene rendue automatiquement par `app.render()`, l'ajout de toute nouvelle fonctionnalité (lumières, textures) deviendra une simple question d'ajout de données dans la structure de scène, sans modification de la boucle de rendu.
+137
View File
@@ -0,0 +1,137 @@
# Roadmap WSG — Prototype → Moteur Complet
> Basé sur l'architecture existante (ARCHI_APP, ARCHI_ARENES, ARCHI_CPU_GPU, ARCHI_RENDU).
> Objectif : prototype fonctionnel d'abord, enrichissement progressif ensuite.
---
## Phase 1️⃣ — Prototype MVP : Un Mesh 3D éclairé à l'écran
**Objectif** : Afficher un cube (ou autre mesh) 3D avec un éclairage Phong basique.
### 1.1 Dépendances & Mathématiques
- [ ] Ajouter `glam = "0.29"` en dépendance (`lib/Cargo.toml`)
- [ ] Ajouter `slotmap = "1.0"` en dépendance
- [ ] Créer module `math/` (ou `transform.rs`) :
- [ ] Struct `Transform { translation: Vec3, rotation: Quat, scale: Vec3 }`
- [ ] Méthode `to_matrix() -> Mat4` pour calculer la matrice locale
- [ ] Struct `Camera { position: Vec3, target: Vec3, up: Vec3 }`
- [ ] Fonctions `view_matrix()` et `projection_matrix(fov, aspect, near, far)`
### 1.2 Geometry & Mesh
- [ ] Créer struct `Geometry` :
- [ ] `positions: Vec<[f32; 3]>` (obligatoire)
- [ ] `indices: Option<Vec<u16>>` (optionnel)
- [ ] `normals: Option<Vec<[f32; 3]>>` (pour Phong)
- [ ] Refactorer `Mesh` pour contenir :
- [ ] `geometry: Arc<Geometry>`
- [ ] `vertex_buffer: wgpu::Buffer`
- [ ] `index_buffer: Option<wgpu::Buffer>`
- [ ] `transform: Transform` (état CPU)
- [ ] Ajouter un mesh de test (cube unitaire) en exemple
### 1.3 Shader Phong Minimal
- [ ] Créer `standard_shader.wgsl` :
- [ ] Vertex shader : projection * view * world * position
- [ ] Fragment shader : éclairage hémisphérique + diffuse avec une lumière directionnelle
- [ ] Uniforms : `view_matrix`, `proj_matrix`, `world_matrix`, `light_dir`, `light_color`
- [ ] Mettre à jour `Material` pour supporter les uniforms du shader Phong
### 1.4 Scene avec Arènes (slotmap)
- [ ] Implémenter `Scene` avec arènes générationalles :
- [ ] `SlotMap<MeshId, Mesh>`
- [ ] `SlotMap<MaterialId, Material>` (préparation future)
- [ ] Méthodes : `add_mesh()`, `get_mesh()`, `iter_meshes()`
- [ ] Les entités stockent des `MeshId` (handles typés), pas des références
### 1.5 Rendu du Prototype
- [ ] Uniform buffer pour la frame : `view_matrix`, `proj_matrix`, `light_dir`
- [ ] Uniform buffer par mesh : `world_matrix` (calculée sur CPU pour le MVP)
- [ ] `Renderer::render()` itère sur les meshes de la Scene et dessine chacun
- [ ] Exemple fonctionnel : un cube éclairé tourne à l'écran
---
## Phase 2️⃣ — Système de Ressources complet
**Objectif** : Étoffer la Scene avec tous les types de ressources.
### 2.1 Arènes complètes
- [ ] `SlotMap<MaterialId, Material>`
- [ ] `SlotMap<TextureId, Texture>` (struct de base)
- [ ] `SlotMap<LightId, Light>` (struct de base)
- [ ] `SlotMap<EntityId, Entity>` pour les entités de la scène
### 2.2 Entités & Hiérarchie
- [ ] Struct `Entity { mesh_id: Option<MeshId>, material_id: Option<MaterialId>, transform: Transform }`
- [ ] `Scene::add_entity()` → retourne `EntityId`
- [ ] `Scene::iter_entities()` → pour le render loop
### 2.3 Camera dans la Scene
- [ ] Intégrer `Camera` comme ressource de la Scene
- [ ] Permettre plusieurs caméras (actuelle/inactive)
- [ ] Exposer API : `scene.set_active_camera(camera_id)`
---
## Phase 3️⃣ — GPU-Driven Rendering
**Objectif** : Déléguer les calculs de transformation et culling au GPU (suivre ARCHI_CPU_GPU.md).
### 3.1 Compute Shader
- [ ] Buffer `TransformBuffer` (CPU → GPU) : positions/rotations/échelles brutes
- [ ] Buffer `MatrixBuffer` (GPU calculé) : World Matrices finales
- [ ] Compute shader : calcul des World Matrices pour tous les meshes
### 3.2 Frustum Culling GPU
- [ ] Ajouter `BBox` dans `Geometry` (center + extents)
- [ ] Buffer `BoundingBoxBuffer` (CPU → GPU, statique)
- [ ] Compute shader : culling basé sur la frustum de caméra
- [ ] Buffer `IndirectDrawBuffer` rempli par le GPU
### 3.3 Rendu Indirect
- [ ] `draw_indexed_indirect()` au lieu de draw calls individuels
- [ ] Un seul command draw pour tous les objets visibles
---
## Phase 4️⃣ — Fonctionnalités Avancées
**Objectif** : Qualité visuelle et performances.
### 4.1 Textures
- [ ] Struct `Texture` avec chargement d'image
- [ ] Ajouter `uvs: Option<Vec<[f32; 2]>>` dans `Geometry`
- [ ] BindGroup pour les textures dans le shader
- [ ] `Material` supporte une texture diffuse
### 4.2 Éclairage avancé
- [ ] Support multi-lumières (directionnelles, ponctuelles)
- [ ] Lumières hémisphériques
- [ ] Shadows (optionnel)
### 4.3 Optimisations
- [ ] Batching par Material (réduction des state changes GPU)
- [ ] Level of Detail (LOD)
- [ ] HDR + Tone Mapping (optionnel)
---
## Phase 5️⃣ — Documentation & Polish
- [ ] Exemple complet : mesh texturé, éclairé, avec caméra orbitale
- [ ] Documentation API (`docs/ARCHI_SCENE.md`)
- [ ] Tests unitaires : `Geometry`, `Scene`, `Transform`
- [ ] README mis à jour avec les nouvelles fonctionnalités
---
## Notes de Décision
| Décision | Raison |
|----------|--------|
| **Normals dès Phase 1** | Nécessaires pour le shader Phong ; sans elles, pas d'éclairage |
| **UVs en Phase 4** | Inutiles avant les textures ; on garde `Geometry` simple au départ |
| **BBox en Phase 3** | Utile uniquement pour le frustum culling GPU |
| **World Matrix CPU → MVP, GPU → Phase 3** | Le MVP est plus simple avec un uniform par mesh ; la migration GPU-driven est progressive |
| **slotmap dès Phase 1** | Architecture décidée (`ARCHI_ARENES.md`) ; mieux de l'adopter tôt que de refactorer |
-23
View File
@@ -1,23 +0,0 @@
[ INITIALISATION ]
|
+---> Context (Manager)
| |
| +---> Renderer (Spécialiste)
| |
+---> PipelineCache (Bibliothèque)
|
+---> Material
|
+---> Mesh
[ BOUCLE DE RENDU (Frame Loop) ]
|
+---> Frame (Acquisition)
|
+---> Renderer (Orchestrateur)
| |
| +--[Rendu]--> CommandEncoder
|
+---> Context (Présentation)
|
+--[Submit]--> Queue
-2407
View File
File diff suppressed because it is too large Load Diff
-13
View File
@@ -1,13 +0,0 @@
[package]
name = "wsg-examples" # On change le nom du package pour éviter le conflit "examples"
version = "0.1.0"
edition = "2024"
[dependencies]
wsg-lib = { path = "../lib" }
pollster = "0.4.0"
winit = "0.29" # Pinned — matches lib/ version exactly
[[bin]]
name = "demo" # Un nom de binaire qui ne risque pas de conflits
path = "main.rs" # On pointe directement vers le fichier à la racine du dossier
+4 -1
View File
@@ -4,10 +4,13 @@ version = "0.1.0"
edition = "2024"
[lib]
path = "lib.rs"
path = "src/lib.rs"
[dependencies]
wgpu = "30.0.0" # Vérifiez la version la plus récente
winit = "0.29" # For window management — pinned to match examples
thiserror = "2"
bytemuck = { version = "1.25.0", features = ["derive"] }
[dev-dependencies]
pollster = { version="0.4.0", features = ["macro"] }
+12 -11
View File
@@ -1,14 +1,14 @@
use std::sync::Arc;
use winit::event_loop::EventLoop;
use winit::window::WindowBuilder;
use wsg_lib::conf;
use wsg_lib::context::Context;
use wsg_lib::frame::Frame; // Import nécessaire
use wsg_lib::material::Material;
use wsg_lib::mesh::Mesh;
use wsg_lib::pipeline_cache::PipelineCache;
use wsg_lib::renderer::Renderer;
use wsg_lib::vertex::Vertex;
use wsg_lib::core::Context;
use wsg_lib::core::Frame;
use wsg_lib::core::Renderer;
use wsg_lib::pipeline::PipelineCache;
use wsg_lib::resources::Material;
use wsg_lib::resources::Mesh;
use wsg_lib::resources::Vertex;
use wsg_lib::utils;
fn main() {
println!(
@@ -27,15 +27,16 @@ fn main() {
.expect("Échec configuration");
// 2. Initialisation du Renderer (Il récupère tout ce dont il a besoin)
let mut cache = PipelineCache::new();
let device = Arc::new(context.device.clone());
let mut cache = PipelineCache::new(device);
cache
.register_shader("basic", conf::BASIC_SHADER_PATH)
.register_shader("basic", utils::BASIC_SHADER_PATH)
.unwrap();
let renderer = Renderer::new(&context, format);
// 3. Material : On utilise renderer.device() et renderer.format()
let material = Material::new(renderer.device(), renderer.format(), "basic", &mut cache);
let material = Material::new(renderer.format(), "basic", &mut cache);
// Mesh : On utilise le device du renderer
let vertices = [
+59
View File
@@ -0,0 +1,59 @@
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 {
mesh: Mesh,
material: Material,
}
impl AppHandler for MonQuad {
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::new().await?;
// 3. Setup des ressources (déclaration)
app.cache
.register_shader("basic", utils::BASIC_SHADER_PATH)
.unwrap();
let vertices: [Vertex; 4] = [
Vertex {
position: [-0.5, 0.5, 0.0],
normal: [0.0, 0.0, 1.0],
uv: [0.0, 0.0],
color: [1.0, 0.0, 0.0, 1.0],
},
Vertex {
position: [0.5, 0.5, 0.0],
normal: [0.0, 0.0, 1.0],
uv: [1.0, 0.0],
color: [0.0, 1.0, 0.0, 1.0],
},
Vertex {
position: [0.5, -0.5, 0.0],
normal: [0.0, 0.0, 1.0],
uv: [1.0, 1.0],
color: [0.0, 0.0, 1.0, 1.0],
},
Vertex {
position: [-0.5, -0.5, 0.0],
normal: [0.0, 0.0, 1.0],
uv: [0.0, 1.0],
color: [1.0, 1.0, 0.0, 1.0],
},
];
let indices: [u16; 6] = [0, 1, 2, 0, 2, 3];
let mesh = Mesh::new(app.context.device(), &vertices, Some(&indices));
let material = Material::new(app.renderer.format(), "basic", &mut app.cache);
// 4. Lancement de la boucle (la magie opère ici)
let handler = MonQuad { mesh, material };
app.run(handler)
}
-28
View File
@@ -1,28 +0,0 @@
//! # 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 context;
pub mod error;
pub mod frame;
pub mod material;
pub mod mesh;
pub mod pipeline_cache;
pub mod renderer;
pub mod vertex;
/// Re-export of the application-level error type for direct use by consumers.
pub use error::WsgError; // For convenient import without path prefix
+33
View File
@@ -0,0 +1,33 @@
# wsg-lib Source Directory
## Overview
This is the source tree for `wsg-lib`, a Rust library wrapping [wgpu](https://github.com/gfx-rs/wgpu) for simple 3D drawing operations. The crate follows a layered architecture organized into seven modules:
| Module | Responsibility |
|--------|---------------|
| **core** | Manager (Context) + Executor (Renderer) layers — GPU lifecycle and draw call orchestration |
| **resources** | Data types: Vertex (CPU-side), Mesh (GPU geometry), Material (appearance descriptor) |
| **pipeline** | PipelineCache — WGSL shader loading and RenderPipeline compilation cache |
| **scene** | Scene — resource depot and entity graph for declarative rendering setup |
| **utils** | Configuration constants and WsgError type |
| **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](../../docs/ARCHI_APP.md).
- **Manual**: Manipulate Context, Renderer, and PipelineCache directly for fine-grained control.
## Dependency Flow
```
scene → core → pipeline → utils
↘→ resources → utils (via Vertex offsets)
scene (consumes)
```
Each submodule's `mod.rs` re-exports its public types so consumers import through the module level rather than deep paths.
+153
View File
@@ -0,0 +1,153 @@
//! # 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;
use crate::scene::Scene;
use crate::utils::WsgError;
use crate::utils::conf::{APP_DEFAULT_HEIGHT, APP_DEFAULT_TITLE, APP_DEFAULT_WIDTH};
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
event_loop
.run(move |event, elwt| {
match event {
winit::event::Event::AboutToWait => {
// update logic
handler.update(&mut self);
self.window.request_redraw();
}
winit::event::Event::WindowEvent {
event: winit::event::WindowEvent::RedrawRequested,
..
} => {
// 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,
..
} => {
elwt.exit();
}
_ => {}
}
})
.map_err(|_| WsgError::WindowSystem)
}
}
/// 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(),
width: APP_DEFAULT_WIDTH,
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(
winit::window::WindowBuilder::new()
.with_title(&self.title)
.build(&event_loop)
.map_err(|_| WsgError::WindowSystem)?,
);
let context = Context::new(window.clone()).await?;
let device = Arc::new(context.device.clone());
let format = context
.configure(&context.adapter, self.width, self.height)
.map_err(|_| WsgError::SurfaceIncompatible)?;
let renderer = Renderer::new(&context, format);
let cache = PipelineCache::new(device);
let scene = Scene::new();
Ok(App {
context,
renderer,
cache,
scene,
event_loop: Some(event_loop),
window,
})
}
}
+18
View File
@@ -0,0 +1,18 @@
# Core Module — Manager and Executor Layers
## Overview
The `core` module contains two architectural layers that drive rendering:
| File | Responsibility |
|------|---------------|
| **context** | **Manager layer** — owns GPU hardware resource lifecycle (Instance, Surface, Adapter, Device, Queue). Initializes GPU at startup; orchestrates frame-by-frame rendering via begin_frame() / end_frame(). Does not own rendering logic. |
| **renderer** | **Executor layer** — owns Device/Queue references after initialization from Context. Orchestrates draw calls by binding Material pipelines and Mesh vertex data into a RenderPass. Does not own raw hardware resources externally or RenderPipelines/shaders. |
| **frame** | Per-frame RAII wrapper around the surface texture and its TextureView. Exists only for the duration of a single rendering pass. |
## Interaction with Other Modules
- **utils**: Context returns WsgError from all fallible methods; Renderer does not use errors directly.
- **pipeline**: Context requires Device reference during pipeline creation in new(); Renderer uses PipelineCache indirectly through Material.
- **resources**: Renderer consumes Mesh and Material instances for draw calls.
- **scene**: Renderer queries Scene for Material/Mesh pairs to render.
+23 -12
View File
@@ -1,20 +1,27 @@
//! # Context Module
//! # Context Module — Manager Layer (Hardware Lifecycle)
//!
//! 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().
//! Initializes the GPU at startup via async Builder pattern; orchestrates frame-by-frame rendering via begin_frame() / end_frame().
//! Does not own rendering logic (that belongs to Renderer) or shader compilation (PipelineCache).
//!
//! ## Interaction with Other Modules
//! - **renderer**: receives Device/Queue references to write rendered output into the TextureView.
//! - **pipeline_cache**: requires Device reference during pipeline creation in Context::new().
//! - **error**: returns WsgError variants from all fallible methods.
//!
//! ## Architecture Notes (per ARCHI_APP.md)
//! - **Phase de Déclaration**: Context is created once at application startup before the render loop begins.
//! This follows the declarative workflow where all GPU state is configured upfront.
//! - **Injection Async**: Context::new() is async because the runtime must be injected at creation time.
//! - **Accès Bas-Niveau**: Advanced users can bypass the Scene facade and manipulate Context directly
//! through App.renderer(), App.context(), etc., for fine-grained control over wgpu handles.
use std::sync::Arc;
use wgpu::{Adapter, Device, Instance, Queue, Surface};
use winit::window::Window;
use crate::error::WsgError;
use crate::frame::Frame;
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.
@@ -34,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();
@@ -70,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,
@@ -106,13 +117,13 @@ impl Context {
};
self.surface.configure(&self.device, &config);
// On retourne le format choisi pour que le Renderer puisse le stocker
// Return the chosen format so the Renderer can store it for pipeline creation
Ok(format)
}
/// Acquires the next surface texture for rendering this frame. Returns an error variant
/// describing why acquisition failed (timeout, occlusion, surface lost, etc.).
/// Typically called by the orchestrator (main.rs) at the start of each frame loop iteration.
/// 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.
+11 -4
View File
@@ -1,4 +1,4 @@
//! # Frame Module
//! # Frame Module — Per-Frame RAII Wrapper (Surface Texture + View)
//!
//! Defines `Frame`, a per-frame RAII wrapper around the surface texture and its TextureView.
//! A Frame exists only for the duration of a single rendering pass — it is acquired at the start
@@ -10,6 +10,10 @@
//! - **renderer**: passes Frame's TextureView to render() as the color attachment target.
//! - **error**: does not use errors directly; Frame::new() panics on acquisition failure while
//! Frame::try_new() returns Option<Self> for graceful recovery.
//!
//! ## Architecture Notes (per ARCHI_APP.md)
//! - **Phase d'Exécution**: Frame is acquired at the start of each render loop iteration and released after rendering.
//! - **Ergonomie**: Users interact with frames through Renderer::render() + Renderer::present(), not directly with wgpu handles.
pub struct Frame {
/// The GPU surface texture representing the current display buffer to be presented.
@@ -20,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 —
@@ -53,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> {
+19
View File
@@ -0,0 +1,19 @@
//! # Core Module — Manager and Executor Layers
//!
//! Defines the two architectural layers that drive rendering: **Context** (Manager) owns GPU hardware
//! resource lifecycle (Device, Queue, Surface), and **Renderer** (Executor) orchestrates draw calls by binding
//! Material pipelines and Mesh vertex data into a RenderPass.
//!
//! ## Interaction with Other Modules
//! - `context` consumes errors from `utils`, holds Frame references during frame loops.
//! - `renderer` receives Device/Queue references from Context, uses Materials from `resources`.
//! - `frame` is consumed by both Context (begin_frame → end_frame) and Renderer (render → present).
pub mod context;
pub mod frame;
pub mod renderer;
// Re-exports
pub use context::Context;
pub use frame::Frame;
pub use renderer::Renderer;
+34 -22
View File
@@ -1,6 +1,6 @@
//! # Renderer Module
//! # Renderer Module — Executor Layer (WGPU Command Execution)
//!
//! The **Specialist** (Executor) layer of the architecture. Owns rendering logic — orchestrates draw calls by binding
//! The **Executor** layer of the architecture. Executes WGPU rendering commands — orchestrates draw calls by binding
//! Material pipelines and Mesh vertex buffers into a RenderPass, then submits commands to the GPU queue.
//! Does not own hardware resources (Device, Queue); receives references when called by the orchestrator (main.rs).
//! Does not own RenderPipelines or shaders — those are managed by PipelineCache and accessed through Material.
@@ -11,23 +11,35 @@
//! - **pipeline_cache**: indirectly via Material — Renderer uses pipelines that PipelineCache compiled.
//! - **mesh**: passes vertex/index buffers into set_vertex_buffer/set_index_buffer during draw.
//! - **material**: provides the RenderPipeline reference via set_pipeline during draw.
//!
//! ## Architecture Notes (per ARCHI_APP.md)
//! - **Phase d'Exécution**: Renderer executes per-frame render loops. During this phase it iterates Scene entities
//! and draws each one by binding the appropriate Material+Mesh pair.
//! - **Performance**: Entity sorting within the render loop minimizes pipeline switches (batching par matériau).
//! - **Accès Bas-Niveau**: Advanced users can bypass Scene and call Renderer directly for custom rendering paths.
use crate::context::Context;
use crate::core::Context;
use crate::core::Frame;
use crate::resources::{Material, Mesh};
/// The Executor layer of the architecture. Owns Device, Queue, and Format after initialization from Context.
/// Orchestrates all GPU draw calls without owning raw hardware resources externally.
/// The Executor layer of the architecture. Holds shared references to Device and Queue from Context,
/// plus the surface texture format. Executes WGPU rendering commands by binding Materials and Meshes
/// into RenderPasses during each frame. Does not own raw hardware resources (they are Arc-cloned from Context).
pub struct Renderer {
/// GPU command submission queue — owned by the Renderer after initialization from Context.
/// GPU command submission queue — holds an Arc clone from Context; shared with other Context users.
queue: wgpu::Queue,
/// GPU device — creates buffers, textures, pipelines; owned by the Renderer after initialization.
/// GPU device — creates buffers, textures, pipelines; holds an Arc clone from Context.
device: wgpu::Device,
/// Surface texture output format — stored here so it can be passed to PipelineCache on Material creation.
format: wgpu::TextureFormat,
}
impl Renderer {
/// Creates a Renderer by taking ownership of Device, Queue, and Format from the Context.
/// Called once at application startup during scene setup. The Renderer becomes the sole owner of these resources.
/// 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 {
Self {
queue: context.queue.clone(),
@@ -38,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: &crate::mesh::Mesh,
material: &crate::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.
@@ -90,16 +100,18 @@ impl Renderer {
/// Presents the rendered frame by submitting the acquired surface texture to the GPU queue.
/// The frame must have been obtained via Context::begin_frame() or Frame::try_new(); calling present()
/// twice on the same texture is undefined behavior. Called by the orchestrator after render().
pub fn present(&self, frame: crate::frame::Frame) {
pub fn present(&self, frame: Frame) {
self.queue.present(frame.surface_texture);
}
/// Returns a reference to the owned Device for direct access when needed (e.g., PipelineCache creation).
/// 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
}
+34
View File
@@ -0,0 +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);
}
+42
View File
@@ -0,0 +1,42 @@
//! # WSG Library Crate Root
//!
//! 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
//! use wsg_lib::core::{Context, Renderer};
//! use wsg_lib::resources::{Mesh, Material, Vertex};
//! use wsg_lib::utils::BASIC_SHADER;
//! ```
pub mod app;
pub mod core;
pub mod handler;
pub mod pipeline;
pub mod resources;
pub mod scene;
pub mod utils;
/// 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;
+20
View File
@@ -0,0 +1,20 @@
# Pipeline Module — Shader Compilation Cache
## Overview
The `pipeline` module contains the shader compilation cache that avoids duplicate GPU work by reusing compiled RenderPipelines.
| File | Responsibility |
|------|---------------|
| **pipeline_cache** | PipelineCache — maps (shader_id, format) keys to compiled RenderPipelines. Loads WGSL from disk or falls back to embedded BASIC_SHADER constant. Creates pipelines on-demand via build_pipeline(). |
## Interaction with Other Modules
- **utils::conf**: Provides BASIC_SHADER_PATH (disk path) and BASIC_SHADER (embedded fallback).
- **resources::vertex**: Vertex struct field offsets define the CPU-side layout that build_pipeline() uses as the vertex buffer contract.
- **resources::material**: Material::new() calls get_or_create() during construction to obtain a shared RenderPipeline.
## Technical Points
- Pipelines are stored behind Arc so multiple Materials share the same compiled object without copying.
- wgpu 30 requires `compilation_options` in VertexState/FragmentState and `depth_slice` in color attachments.
+14
View File
@@ -0,0 +1,14 @@
//! # Pipeline Module — Shader Compilation Cache
//!
//! Defines the **PipelineCache**, which owns WGSL shader loading and RenderPipeline creation. It caches compiled
//! pipelines keyed by (shader_id, format) to avoid duplicate GPU work — multiple Materials sharing the same ID
//! share one Arc-wrapped pipeline without copying.
//!
//! ## Interaction with Other Modules
//! - `material` calls `get_or_create()` during its own construction to obtain a shared RenderPipeline.
//! - `conf::BASIC_SHADER` provides fallback WGSL source when an external file is not found.
//! - `vertex::Vertex` defines the CPU-side layout that `build_pipeline()` uses as the vertex buffer contract.
pub mod pipeline_cache;
// Re-exports
pub use pipeline_cache::PipelineCache;
@@ -1,7 +1,7 @@
//! # PipelineCache Module
//! # PipelineCache Module — Translation of resources/ Data Toward GPU Pipelines
//!
//! 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.
//! Defines `PipelineCache`, the library's shader compilation cache. It translates WGSL shader source and resources/ data types
//! into compiled RenderPipelines, storing them in a HashMap keyed by shader_id + texture format to avoid duplicate GPU work.
//! Materials request pipelines through this cache; if a pipeline for the given key exists, it is returned directly
//! via Arc cloning. Otherwise the cache compiles one on-the-fly, caches it, then returns it.
//!
@@ -13,15 +13,18 @@
//! ## Technical Points
//! - Pipelines are stored behind `Arc` so multiple Materials share the same compiled object without copying.
//! - wgpu 30 requires `compilation_options` in VertexState/FragmentState and `depth_slice` in color attachments.
//! - **Batching**: Multiple Materials with the same shader_id share one pipeline, enabling material-level batching in Renderer.
use crate::resources::Vertex;
use crate::utils::BASIC_SHADER;
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 {
device: Arc<wgpu::Device>,
/// Cached pipelines keyed by their shader identifier string. Multiple Materials sharing the same ID share one Arc-wrapped pipeline.
pipelines: HashMap<String, Arc<wgpu::RenderPipeline>>,
/// Maps shader IDs to file paths on disk for WGSL loading in `load_shader()`.
@@ -30,9 +33,12 @@ 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() -> Self {
pub fn new(device: Arc<wgpu::Device>) -> Self {
Self {
device,
pipelines: HashMap::new(),
// Maps shader IDs to file paths on disk for WGSL loading in load_shader().
// When a path exists, it reads from it; otherwise falls back to BASIC_SHADER constant.
@@ -41,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));
@@ -52,23 +58,25 @@ 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,
device: &wgpu::Device,
format: wgpu::TextureFormat,
shader_id: &str,
) -> Arc<wgpu::RenderPipeline> {
@@ -83,8 +91,8 @@ impl PipelineCache {
.get(shader_id)
.map(|s| s.as_str())
.unwrap_or(shader_id);
let shader = self.load_shader(device, path);
let pipeline = Self::build_pipeline(device, format, &shader);
let shader = self.load_shader(&self.device, path);
let pipeline = Self::build_pipeline(&self.device, format, &shader);
// Step 3: Cache the new pipeline behind Arc and return it
let pipeline_arc = Arc::new(pipeline);
@@ -94,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);
@@ -110,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,
+17
View File
@@ -0,0 +1,17 @@
# Resources Module — Data Types
## Overview
The `resources` module defines three immutable data types that flow through the rendering pipeline. These are created once during scene initialization and consumed by Renderer for draw calls every frame.
| File | Responsibility |
|------|---------------|
| **vertex** | Vertex struct — CPU-side per-attribute tuple (position [f32;3], normal [f32;3], uv [f32;2], color [f32;4]). Must match PipelineCache::build_pipeline() vertex buffer layout byte-for-byte. |
| **mesh** | Mesh struct — persistent GPU geometry container with vertex_buffer (wgpu::Buffer), optional index_buffer, and draw call counters. Created via Mesh::new() which uploads data from CPU to GPU buffers. |
| **material** | Material struct — lightweight appearance descriptor pairing shader_id with a shared RenderPipeline Arc. Multiple Materials referencing the same shader_id point to the identical compiled GPU pipeline. |
## Interaction with Other Modules
- **pipeline**: build_pipeline() reads Vertex field offsets to construct VertexBufferLayout attributes array.
- **scene**: Scene stores Arc<Mesh> and Arc<Material> instances keyed by identifier strings.
- **utils**: Mesh creation uses BASIC_SHADER fallback when external shader files are missing.
@@ -1,10 +1,15 @@
//! # Material Module
//! # Material Module — Appearance Descriptor (shader_id → RenderPipeline)
//!
//! Defines `Material`, a lightweight appearance descriptor that pairs a shader identifier with
//! a shared RenderPipeline. Materials are created via PipelineCache to ensure pipeline reuse—
//! multiple materials referencing the same shader_id point to the identical compiled GPU pipeline.
//!
//! ## Architecture Notes (per ARCHI_APP.md)
//! - **Identifiants**: Each Material is registered in Scene by string identifier, enabling dynamic access
//! during the render loop without borrow checker issues. The shader_id serves as the Handle<T> key.
//! - **Phase de Déclaration**: Materials are instantiated once in the declarative phase before the render loop begins.
use crate::pipeline_cache::PipelineCache;
use crate::pipeline::PipelineCache;
use std::sync::Arc;
/// Lightweight appearance descriptor: links a shader ID to a shared RenderPipeline.
@@ -18,17 +23,12 @@ 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.
pub fn new(
device: &wgpu::Device,
format: wgpu::TextureFormat,
shader_id: &str,
cache: &mut PipelineCache,
) -> Self {
/// 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(device, format, shader_id);
let pipeline = cache.get_or_create(format, shader_id);
Self {
shader_id: shader_id.to_string(),
pipeline,
+13 -3
View File
@@ -1,9 +1,15 @@
//! # Mesh Module
//! # Mesh Module — Persistent GPU Geometry Container
//!
//! Defines `Mesh`, a persistent GPU geometry container. Mesh data is uploaded to the GPU once at creation time
//! and remains valid across all frames until dropped. It holds no rendering knowledge—only raw geometric data.
//!
//! ## Architecture Notes (per ARCHI_APP.md)
//! - **Identifiants**: Each Mesh is registered in Scene by string identifier, enabling dynamic access
//! during the render loop without borrow checker issues. The identifier serves as the Handle<T> key.
//! - **Phase de Déclaration**: Meshes are instantiated once in the declarative phase before the render loop begins.
//! - **Performance**: Multiple entities can reference the same Mesh, reducing memory footprint for repeated geometry.
use crate::vertex::Vertex;
use crate::resources::vertex::Vertex;
use wgpu::util::DeviceExt;
/// Persistent GPU geometry: vertex positions, optional indices, and draw call counters.
@@ -21,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"),
+20
View File
@@ -0,0 +1,20 @@
//! # Resources Module — Data Types
//!
//! Defines the three core data types that flow through the rendering pipeline: **Vertex** (CPU-side per-attribute
//! tuple), **Mesh** (GPU geometry container with vertex/index buffers), and **Material** (appearance descriptor
//! pairing shader ID with a compiled RenderPipeline). These are immutable after creation and consumed by Renderer
//! for draw calls.
//!
//! ## Interaction with Other Modules
//! - `pipeline_cache::build_pipeline()` reads Vertex field offsets to construct the vertex buffer layout.
//! - `mesh::new()` uploads Vertex arrays from CPU memory into GPU vertex buffers via DeviceExt::create_buffer_init().
//! - `material::new()` requests RenderPipelines from PipelineCache during scene initialization.
pub mod material;
pub mod mesh;
pub mod vertex;
// Re-exports
pub use material::Material;
pub use mesh::Mesh;
pub use vertex::Vertex;
@@ -1,4 +1,4 @@
//! # Vertex Module
//! # Vertex Module — CPU-Side Per-Attribute Tuple (GPU Contract)
//!
//! Defines `Vertex`, the CPU-side data layout that is sent to the GPU as vertex attribute buffers.
//! The struct's field order and offsets must exactly match the shader input attributes defined in
@@ -7,19 +7,21 @@
//! ## Technical Points
//! - `#[repr(C)]` ensures fields are laid out contiguously without Rust padding reordering, matching C ABI.
//! - `bytemuck::Pod + bytemuck::Zeroable` enables safe `cast_slice()` conversion for GPU buffer uploads.
//! - **Performance**: The 56-byte stride per vertex is the contract between CPU data and GPU shader inputs;
//! PipelineCache::build_pipeline() reads this layout to construct VertexBufferLayout attributes array.
/// Per-vertex attribute tuple: position (3D), normal (3D), texture coordinate (2D), color (RGBA).
/// Must match the vertex buffer layout in PipelineCache::build_pipeline() byte-for-byte.
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct Vertex {
/// XYZ coordinates of this vertex in world space. Offset: 0 bytes.
/// 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],
}
+22
View File
@@ -0,0 +1,22 @@
# Scene Module — Resource Depot and Entity Graph
## Overview
The `scene` module defines Scene, the declarative layer of the WSG architecture. Users register resources (Meshes, Materials) by identifier before the render loop starts, then associate entities via labels. At runtime, Scene provides immutable access to these resources without exposing raw wgpu handles.
## Files
| File | Responsibility |
|------|---------------|
| **scene** | Scene struct — resource depot storing Meshes and Materials keyed by string identifiers, plus entity graph mapping labels to (mesh_id, material_id) pairs for rendering iteration. |
## Interaction with Other Modules
- **core::renderer**: Renderer queries Scene for Material/Mesh pairs during frame rendering; Context does not interact directly.
- **pipeline**: PipelineCache creates Materials keyed by shader_id; Scene stores references to those Materials.
- **resources**: Scene owns Arc<Mesh> and Arc<Material> instances; Vertex is only used at Mesh creation time.
- **utils**: Scene uses WsgError if resource registration fails.
## Architecture Note
Per [ARCHI_APP](../../docs/ARCHI_APP.md), Scene is one half of the "App" facade pattern. It enables a declarative workflow where all resources are declared before the render loop begins, while keeping the freedom to build the engine "brick by brick" through direct Context/PipelineCache/Renderer manipulation.
+22
View File
@@ -0,0 +1,22 @@
//! # Scene Module — Resource Depot and Entity Graph
//!
//! Defines `Scene`, the repository for resources (Mesh, Material) and the graph of entity associations between them.
//! Scene is the declarative layer of the architecture: users register Meshes and Materials by identifier before
//! the render loop starts, then associate entities via labels during initialization. At runtime, Scene provides
//! immutable access to these resources without exposing raw wgpu handles.
//!
//! ## Interaction with Other Modules
//! - **core**: Renderer queries Scene for Material/Mesh pairs to draw; Context does not interact directly.
//! - **pipeline**: PipelineCache creates Materials keyed by shader_id; Scene stores references to those Materials.
//! - **resources**: Scene owns Mesh and Material instances; Vertex is only used at Mesh creation time.
//! - **utils**: Scene uses WsgError if resource registration fails.
//!
//! ## Architecture Note
//! Per ARCHI_APP_FACADE.md, Scene is one half of the "App" facade pattern. It enables a declarative workflow where
//! all resources are declared before the render loop begins, while keeping the freedom to build the engine
//! "brick by brick" through direct Context/PipelineCache/Renderer manipulation.
pub mod scene;
// Re-export
pub use scene::Scene;
+121
View File
@@ -0,0 +1,121 @@
//! # Scene Module — Resource Depot and Entity Graph (per ARCHI_APP.md)
//!
//! Defines `Scene`, the declarative layer of the WSG architecture. Users register resources (Meshes, Materials) by identifier before
//! the render loop starts, then associate entities via labels. At runtime, Scene provides immutable access to these resources without exposing raw wgpu handles.
//!
//! ## Architecture Notes (per ARCHI_APP.md)
//! - **La Recette**: Scene is central to the "App" facade workflow. In the Phase de Déclaration, users call add_mesh(), add_material(), and add_entity()
//! to build the resource depot. During Phase d'Exécution, Renderer iterates Scene entities for rendering.
//! - **Identifiants**: All resource registration uses string identifiers (Handle<T>/String pattern), guaranteeing memory safety
//! and avoiding borrow checker issues during dynamic updates.
//! - **Ergonomie**: Users interact only with entity-level operations (add/remove/get) rather than wgpu buffers/pipelines directly.
use crate::resources::{Material, Mesh};
use std::collections::HashMap;
use std::sync::Arc;
/// Resource depot and entity graph. Stores Meshes and Materials keyed by identifier strings,
/// and maps entity labels to their associated mesh+material pairs for rendering iteration.
/// Created once during application setup; entities are added before the render loop starts.
pub struct Scene {
/// Map of mesh identifiers to owned Arc<Mesh> instances. Populated via `add_mesh()`.
meshes: HashMap<String, Arc<Mesh>>,
/// 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)>,
}
impl Scene {
/// Creates an empty scene with no registered resources or entities.
/// Called at application startup before any resource registration.
pub fn new() -> Self {
Self {
meshes: HashMap::new(),
materials: HashMap::new(),
entities: HashMap::new(),
}
}
/// Registers a Mesh in the scene under a unique identifier.
/// Inputs: id (unique key), mesh (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) {
return Err(format!("Mesh ID '{}' already exists.", id));
}
self.meshes.insert(id.to_string(), mesh);
Ok(id.to_string())
}
/// Registers a Material in the scene under a unique identifier.
/// Inputs: id (unique key), material (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) {
return Err(format!("Material ID '{}' already exists.", id));
}
self.materials.insert(id.to_string(), material);
Ok(id.to_string())
}
/// Associates an entity label with a mesh and material pair for rendering iteration.
/// Inputs: label (entity identifier 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,
mesh_id: &str,
material_id: &str,
) -> Result<String, String> {
if !self.meshes.contains_key(mesh_id) {
return Err(format!("Mesh '{}' does not exist.", mesh_id));
}
if !self.materials.contains_key(material_id) {
return Err(format!("Material '{}' does not exist.", material_id));
}
self.entities.insert(
label.to_string(),
(mesh_id.to_string(), material_id.to_string()),
);
Ok(label.to_string())
}
/// Retrieves a Mesh by its registered identifier.
/// Called by Renderer during frame rendering to obtain vertex data for draw calls.
pub fn get_mesh(&self, id: &str) -> Option<&Arc<Mesh>> {
self.meshes.get(id)
}
/// Retrieves a Material by its registered identifier.
/// Called by Renderer during frame rendering to obtain pipeline reference for draw calls.
pub fn get_material(&self, id: &str) -> Option<&Arc<Material>> {
self.materials.get(id)
}
/// Iterates all entity associations, yielding (label, mesh_ref, material_ref) triples.
/// Called by the orchestrator during each render pass to draw every entity in order.
pub fn iter_entities(&self) -> impl Iterator<Item = (&str, &Arc<Mesh>, &Arc<Material>)> + '_ {
self.entities.iter().map(|(label, (mesh_id, mat_id))| {
let mesh = self.meshes.get(mesh_id).unwrap(); // safe: add_entity validates existence
let mat = self.materials.get(mat_id).unwrap(); // same invariant
(label.as_str(), mesh, mat)
})
}
/// Removes an entity from the graph without freeing its underlying resources.
/// The referenced Mesh and Material remain registered; only the association is dropped.
/// Called during dynamic updates when an entity should be hidden or removed temporarily.
pub fn remove_entity(&mut self, label: &str) -> bool {
self.entities.remove(label).is_some()
}
/// Returns the number of registered entities in this scene.
/// Called for diagnostic logging or culling decisions (e.g., skip rendering empty scenes).
pub fn entity_count(&self) -> usize {
self.entities.len()
}
}
+28
View File
@@ -0,0 +1,28 @@
# Shaders Directory
## Overview
Contains WGSL shader source files used by the PipelineCache module. These are loaded at runtime from disk when referenced by their registered ID in PipelineCache.register_shader(). If a file is missing, PipelineCache falls back to the embedded BASIC_SHADER constant defined in utils::conf.
## Files
| File | Purpose |
|------|---------|
| **basic_shader.wgsl** | Default vertex/fragment shader pair (vs_main / fs_main entry points) with position, uv, and color attributes. |
## Shader Contract (basic_shader.wgsl)
The WGSL shader defines:
- `@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.
+42
View File
@@ -0,0 +1,42 @@
//! # 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>,
@location(2) color: vec3<f32>,
};
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) color: vec3<f32>,
};
@vertex
fn vs_main(model: VertexInput) -> VertexOutput {
var out: VertexOutput;
out.clip_position = vec4<f32>(model.position, 1.0);
out.color = model.color; // On transmet la couleur au fragment shader
return out;
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
return vec4<f32>(in.color, 1.0);
}
+16
View File
@@ -0,0 +1,16 @@
# Utils Module — Configuration and Error Handling
## Overview
The `utils` module defines two leaf concepts that other modules consume but have no internal dependencies on. As a leaf module, it does not import from any other library submodule.
| File | Responsibility |
|------|---------------|
| **conf** | Shared constants for shader paths (BASIC_SHADER_PATH) and embedded WGSL source code (BASIC_SHADER). Centralized here so all submodules import from one place instead of duplicating literal strings. Enables PipelineCache to fall back to an embedded default shader when the file-based one is missing. |
| **error** | WsgError enum — application-level error type mapping specific wgpu failure modes to user-friendly messages via thiserror. Every variant maps a GPU initialization or rendering failure to a recoverable or fatal outcome. |
## Interaction with Other Modules
- **pipeline::pipeline_cache**: load_shader() reads BASIC_SHADER_PATH from disk; falls back to BASIC_SHADER if unreadable.
- **core::context**: Returns WsgError variants from all fallible methods (new, configure, begin_frame).
- **core::renderer**: Does not use errors directly — panics on invalid state rather than returning Result.
+12 -1
View File
@@ -3,14 +3,25 @@
//! 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";
/// 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!("../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;
+18
View File
@@ -0,0 +1,18 @@
//! # Utils Module — Configuration and Error Handling
//!
//! Defines two leaf concepts: **conf** (shared constants for shader paths and embedded WGSL source) and
//! **error** (WsgError, the application-level error type mapping wgpu failure modes to user-friendly messages).
//! Both are consumed by other modules but have no internal dependencies on them.
//!
//! ## Interaction with Other Modules
//! - `pipeline_cache` loads shaders from disk using conf::BASIC_SHADER_PATH; falls back to BASIC_SHADER.
//! - `context` returns WsgError variants from all fallible methods (new, configure, begin_frame).
//! - `renderer` does not use errors directly (panics on invalid state rather than returning Result).
pub mod conf;
pub mod error;
// Re-exports
pub use conf::BASIC_SHADER;
pub use conf::BASIC_SHADER_PATH;
pub use error::WsgError;