doc
This commit is contained in:
Generated
+14
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
+63
-46
@@ -1,65 +1,82 @@
|
||||
# ARCHI_APP.md : Architecture et Responsabilités
|
||||
# Architecture du Moteur wsg_lib
|
||||
|
||||
Ce document définit l'architecture modulaire du moteur `wsg_lib`. L'objectif est de séparer la plomberie système de la logique métier tout en facilitant l'usage via une façade unifiée.
|
||||
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. Organisation des répertoires (`lib/src/`)
|
||||
## 1. Philosophie et Principes
|
||||
|
||||
L'organisation respecte les conventions Rust pour une bibliothèque modulaire :
|
||||
- **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.
|
||||
|
||||
* `core/` : Plomberie système (Context, Renderer, Frame).
|
||||
* `pipeline/` : Gestion des états GPU et compilation des shaders.
|
||||
* `resources/` : Dépôt de données (Mesh, Material, Vertex, Texture).
|
||||
* `scene/` : Logique métier et hiérarchie (Entités, Transformations).
|
||||
* `shaders/` : Shaders intégrés (accessibles via `include_str!`).
|
||||
* `utils/` : Transverses (Configuration, Erreurs).
|
||||
## 2. Organisation des Modules (`lib/src/`)
|
||||
|
||||
## 2. Répartition des responsabilités
|
||||
- **`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.
|
||||
|
||||
| Module | Responsabilité |
|
||||
| :--- | :--- |
|
||||
| **App** | Façade orchestratrice (Point d'entrée unique). |
|
||||
| **Renderer** | Exécution des commandes WGPU. |
|
||||
| **PipelineCache** | Traduction des données de `resources/` vers les pipelines GPU. |
|
||||
| **Scene** | Stockage et gestion des entités et de leurs relations. |
|
||||
| **AppHandler** | Trait implémenté par l'utilisateur pour la boucle de jeu. |
|
||||
## 3. Interfaces de Haut Niveau (`App` & `AppHandler`)
|
||||
|
||||
## 3. Workflow de l'utilisateur ("La Recette")
|
||||
### L'objet `App`
|
||||
|
||||
### Phase de Déclaration (Initialisation)
|
||||
L'utilisateur configure sa scène avant le démarrage de la boucle.
|
||||
```rust
|
||||
let mut app = App::builder()
|
||||
.with_runtime(my_runtime)
|
||||
.build()
|
||||
.await;
|
||||
La façade `App` orchestre la boucle de jeu. Elle encapsule :
|
||||
|
||||
let mat_id = app.resources.create_material("name", shader_id);
|
||||
let mesh_id = app.resources.load_mesh("path");
|
||||
app.scene.add_entity("id", mesh_id, mat_id);
|
||||
```
|
||||
- Le cycle de vie de la fenêtre.
|
||||
- La boucle d'événements.
|
||||
- La gestion automatique des Frame (acquisition et présentation).
|
||||
|
||||
### Phase d'Exécution (Render Loop)
|
||||
### Le trait `AppHandler`
|
||||
|
||||
L'utilisateur implémente `AppHandler` pour manipuler ses objets.
|
||||
L'utilisateur implémente ce trait pour définir la logique métier :
|
||||
|
||||
```rust
|
||||
impl AppHandler for MyGame {
|
||||
fn render(&mut self, app: &mut App) {
|
||||
// La scène est rendue automatiquement, l'utilisateur modifie l'état
|
||||
app.scene.get("id").transform.rotation += 0.01;
|
||||
}
|
||||
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);
|
||||
}
|
||||
app.run(MyGame::new());
|
||||
```
|
||||
|
||||
### 4. Points d'attention pour l'utilisateur avancé
|
||||
## 4. Workflow et Cycle de Vie
|
||||
|
||||
- **Accès Bas-Niveau** : App expose ses composants internes (`renderer`, `context`, `cache`). Un utilisateur avancé peut ignorer la Scene pour faire des appels manuels.
|
||||
- **Injection Async** : Le runtime est injecté à la création via le Builder.
|
||||
- **Identifiants** : La gestion des ressources repose sur des identifiants (`Handle<T>` ou `String`), garantissant la sécurité mémoire et évitant les problèmes de durée de vie (*borrow checker*).
|
||||
### A. Initialisation (Configuration)
|
||||
|
||||
### 5. Pourquoi cette architecture ?
|
||||
- **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`.
|
||||
|
||||
- **Performance** : La centralisation dans App permet d'optimiser le tri des entités et le *batching* par matériau.
|
||||
- **Maintenance** : Chaque dossier est indépendant. Ajouter une nouvelle fonctionnalité (ex: Lumières) consiste à créer un nouveau module dans `resources/` ou `scene/` sans impacter le cœur du rendu.
|
||||
- **Ergonomie** : L'utilisateur n'est plus confronté à la gestion des pipelines et des buffers, mais uniquement à la gestion de sa scène et de ses entités.
|
||||
### 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`.
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
# ARCHI_APP_FACADE.md : Vers une Architecture Orientée Scène
|
||||
|
||||
## 1. Vision et Objectifs
|
||||
|
||||
Le passage d'une orchestration manuelle à une façade `App` vise à réduire le "boilerplate" tout en conservant la modularité. L'utilisateur utilise désormais une **"Recette par défaut"** basée sur une structure de **Scène**, tout en gardant la liberté de construire son moteur "brique par brique" s'il le souhaite.
|
||||
|
||||
### Les piliers :
|
||||
|
||||
* **Déclaratif** : Toutes les ressources sont déclarées avant le lancement de la boucle.
|
||||
* **Orienté Scène** : L'utilisateur gère des relations (associations) plutôt que des appels de rendu directs.
|
||||
* **Flexible** : L'injection de l'async est gérée via un `Runtime` injecté.
|
||||
* **Transparent** : La "recette" est documentée, permettant une déconstruction totale vers les briques de bas niveau (`Context`, `Renderer`, `PipelineCache`).
|
||||
|
||||
---
|
||||
|
||||
## 2. Le Workflow de l'utilisateur (Exemple)
|
||||
|
||||
```rust
|
||||
// 1. Déclaration : Initialisation asynchrone
|
||||
let mut app = App::builder()
|
||||
.with_runtime(tokio::runtime::Runtime::new().unwrap()) // Injection de l'async
|
||||
.build()
|
||||
.await;
|
||||
|
||||
// 2. Enregistrement des ressources (Labels identifiants ou références)
|
||||
let shader_id = app.register_shader("basic", "assets/basic.wgsl");
|
||||
let mat_id = app.create_material("basic_mat", shader_id);
|
||||
let mesh_id = app.create_mesh("my_quad", &vertices, &indices);
|
||||
|
||||
// 3. Association dans la scène
|
||||
app.scene.add_entity("main_quad", mesh_id, mat_id);
|
||||
|
||||
// 4. Exécution via un Trait pour la boucle
|
||||
struct MyGame { /* ... */ }
|
||||
impl AppHandler for MyGame {
|
||||
fn render(&mut self, app: &mut App) {
|
||||
// Modification dynamique (ex: transparence, visibilité)
|
||||
app.scene.get_material("basic_mat").set_opacity(0.5);
|
||||
}
|
||||
}
|
||||
app.run(MyGame::new());
|
||||
```
|
||||
|
||||
## 3. Gestion des Identifiants (Handles)
|
||||
|
||||
Pour éviter les problèmes de Borrow Checker, nous utilisons un système de Handles (ou Label) :
|
||||
|
||||
* **Référencement** : String (label) ou Handle<T> (interne) pour accéder aux ressources.
|
||||
* **Accès** : L'utilisateur manipule ses ressources via `app.scene.get_material("nom")` ou en conservant les IDs retournés lors de la création.
|
||||
* **Sécurité** : Les IDs garantissent que la ressource existe toujours dans le dépôt de la Scene.
|
||||
|
||||
## 4. La "Recette" : Comment reproduire manuellement
|
||||
|
||||
Si App ne convient pas, voici les étapes de la "recette" interne que l'utilisateur peut répliquer :
|
||||
|
||||
1. **Init GPU** : `Context::new(window)` → configure().
|
||||
2. **Setup PipelineCache** : Instancier le cache et compiler les shaders nécessaires.
|
||||
3. **Setup Renderer** : Créer le Renderer avec le format de surface.
|
||||
4. **Boucle winit** :
|
||||
- RedrawRequested → Frame::try_new()
|
||||
- renderer.render() → renderer.present()
|
||||
5. **Nettoyage** : Gestion propre de la fermeture via elwt.exit().
|
||||
|
||||
## 5. Avantages et Points d'attention
|
||||
|
||||
### Avantages
|
||||
|
||||
* **Performance** : Le moteur peut trier les entités pour minimiser les changements de pipelines.
|
||||
* **Ergonomie** : Suppression des appels manuels à device et format dans le main.
|
||||
* **Sécurité** : Séparation claire entre la phase de déclaration (Init) et la phase d'exécution (Loop).
|
||||
|
||||
### Points d'attention (Portes de sortie)
|
||||
|
||||
* **Accès Bas-Niveau** : App expose ses champs `renderer`, `context` et `cache` en public. Un utilisateur avancé peut toujours bypasser `app.scene` pour des besoins très spécifiques.
|
||||
* **Gestion Async** : Le choix du runtime reste la responsabilité de l'utilisateur. App se contente d'exécuter les futurs fournis.
|
||||
* **Dynamisme** : Si une ressource doit être créée en cours de jeu, l'utilisateur doit l'ajouter dans la Scene via une méthode `app.scene.add_entity(...)` qui est thread-safe.
|
||||
|
||||
## 6. Synthèse des responsabilités
|
||||
|
||||
* **App** : Orchestrateur principal, propriétaire de la Window et de la Surface.
|
||||
* **Scene** : Dépôt de ressources et gestionnaire de visibilité/associations.
|
||||
* **AppHandler** : Trait de logique utilisateur, séparant update et render.
|
||||
@@ -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 !
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
+1
-1
@@ -13,4 +13,4 @@ thiserror = "2"
|
||||
bytemuck = { version = "1.25.0", features = ["derive"] }
|
||||
|
||||
[dev-dependencies]
|
||||
pollster = "0.4.0"
|
||||
pollster = { version="0.4.0", features = ["macro"] }
|
||||
|
||||
+3
-14
@@ -1,6 +1,6 @@
|
||||
use wsg_lib::app::{App, AppHandler};
|
||||
use wsg_lib::resources::{Material, Mesh, Vertex};
|
||||
use wsg_lib::utils;
|
||||
use wsg_lib::{App, AppHandler};
|
||||
|
||||
// 1. On définit notre "Jeu" qui implémente le comportement
|
||||
struct MonQuad {
|
||||
@@ -9,24 +9,13 @@ struct MonQuad {
|
||||
}
|
||||
|
||||
impl AppHandler for MonQuad {
|
||||
fn render(&mut self, app: &mut App) {
|
||||
// Le rendu devient simple : on accède aux outils via &mut app
|
||||
if let Some(frame) = app.context.get_next_frame() {
|
||||
app.renderer
|
||||
.render(frame.view(), &self.mesh, &self.material);
|
||||
app.renderer.present(frame);
|
||||
}
|
||||
}
|
||||
fn render(&mut self, app: &mut App) {}
|
||||
}
|
||||
|
||||
#[pollster::main]
|
||||
async fn main() -> Result<(), wsg_lib::utils::WsgError> {
|
||||
// 2. Initialisation via le Builder
|
||||
let mut app = App::builder()
|
||||
.title("Exemple Simple")
|
||||
.size(800, 600)
|
||||
.build()
|
||||
.await?;
|
||||
let mut app = App::new().await?;
|
||||
|
||||
// 3. Setup des ressources (déclaration)
|
||||
app.cache
|
||||
|
||||
+4
-2
@@ -2,7 +2,7 @@
|
||||
|
||||
## Overview
|
||||
|
||||
This is the source tree for `wsg-lib`, a Rust library wrapping [wgpu](https://github.com/gfx-rs/wgpu) for simple 3D drawing operations. The crate follows a layered architecture organized into five modules:
|
||||
This is the source tree for `wsg-lib`, a Rust library wrapping [wgpu](https://github.com/gfx-rs/wgpu) for simple 3D drawing operations. The crate follows a layered architecture organized into seven modules:
|
||||
|
||||
| Module | Responsibility |
|
||||
|--------|---------------|
|
||||
@@ -11,12 +11,14 @@ This is the source tree for `wsg-lib`, a Rust library wrapping [wgpu](https://gi
|
||||
| **pipeline** | PipelineCache — WGSL shader loading and RenderPipeline compilation cache |
|
||||
| **scene** | Scene — resource depot and entity graph for declarative rendering setup |
|
||||
| **utils** | Configuration constants and WsgError type |
|
||||
| **app** | App facade — high-level application orchestration with window lifecycle, event loop, and render automation |
|
||||
| **handler** | AppHandler trait — user-defined game logic interface injected into the render loop |
|
||||
|
||||
## Architecture Pattern
|
||||
|
||||
The library supports two workflows:
|
||||
|
||||
- **Declarative (recommended)**: Use `Scene` to register resources and associate entities before the render loop begins. This is the "App facade" pattern described in [ARCHI_APP_FACADE](../../docs/ARCHI_APP_FACADE.md).
|
||||
- **Declarative (recommended)**: Use `Scene` to register resources and associate entities before the render loop begins. This is the "App facade" pattern described in [ARCHI_APP](../../docs/ARCHI_APP.md).
|
||||
- **Manual**: Manipulate Context, Renderer, and PipelineCache directly for fine-grained control.
|
||||
|
||||
## Dependency Flow
|
||||
|
||||
+53
-1
@@ -1,3 +1,18 @@
|
||||
//! # App Facade Module — High-Level Application Orchestration
|
||||
//!
|
||||
//! Defines the `App` facade type and `AppHandler` trait that provide the high-level user-facing API.
|
||||
//! `App` encapsulates window lifecycle, event loop, frame acquisition, and rendering automation.
|
||||
//! Users implement `AppHandler` to inject their game logic into the render loop without touching wgpu directly.
|
||||
//! The `AppBuilder` provides a builder-style constructor for creating configured `App` instances.
|
||||
//!
|
||||
//! ## Interaction with Other Modules
|
||||
//! - **core::context**: Consumes GPU hardware lifecycle via Context; acquires frames for rendering.
|
||||
//! - **core::renderer**: Delegates draw call execution to Renderer per frame.
|
||||
//! - **pipeline::pipeline_cache**: Holds PipelineCache instance for shader/pipeline management.
|
||||
//! - **scene::scene**: Exposes Scene as mutable field so users can register resources and entities.
|
||||
//! - **utils::conf**: Provides default window title, dimensions, and embedded WGSL source.
|
||||
//! - **handler**: Defines the AppHandler trait that users implement for custom logic.
|
||||
|
||||
use crate::AppHandler;
|
||||
use crate::core::{Context, Renderer};
|
||||
use crate::pipeline::PipelineCache;
|
||||
@@ -8,16 +23,33 @@ use std::sync::Arc;
|
||||
use winit::event_loop::EventLoop;
|
||||
use winit::window::Window;
|
||||
|
||||
/// High-level application facade that orchestrates window lifecycle, event loop, and rendering automation.
|
||||
/// Encapsulates all five WGPU objects (Instance, Surface, Adapter, Device, Queue) plus the render loop.
|
||||
/// Users create an App via AppBuilder, then run it with their implementation of AppHandler.
|
||||
pub struct App {
|
||||
/// GPU hardware context — owns Instance, Surface, Adapter, Device, Queue lifecycle.
|
||||
pub context: Context,
|
||||
/// Executor layer — binds Materials and Meshes into RenderPasses during draw calls.
|
||||
pub renderer: Renderer,
|
||||
/// Winit event loop for window management. Set to None after run() consumes it.
|
||||
pub event_loop: Option<EventLoop<()>>, // On met en Option pour pouvoir faire .take() facilement
|
||||
/// Shader compilation cache — manages RenderPipelines keyed by shader_id.
|
||||
pub cache: PipelineCache,
|
||||
/// Resource depot and entity graph — users register Meshes/Materials here before the render loop begins.
|
||||
pub scene: Scene,
|
||||
/// The OS-level window backing this application. Shared via Arc for multi-owner access.
|
||||
pub window: Arc<Window>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
/// Runs the application's main loop: processes events, updates logic per frame, renders, and presents.
|
||||
/// Inputs: handler — user-provided AppHandler implementation containing game logic.
|
||||
/// Returns Ok(()) on success or Err(WsgError::WindowSystem) if the event loop exits abnormally.
|
||||
/// Called once at application entry point; runs until the window is closed or an error occurs.
|
||||
/// Internal steps: 1) take EventLoop from Option → 2) enter winit event loop →
|
||||
/// 3a) on AboutToWait: call handler.update() + request_redraw →
|
||||
/// 3b) on RedrawRequested: acquire frame → call handler.render() → present frame →
|
||||
/// 3c) on CloseRequested: exit event loop.
|
||||
pub fn run<H: AppHandler + 'static>(mut self, mut handler: H) -> Result<(), WsgError> {
|
||||
// On extrait l'event_loop de manière sûre grâce au Option
|
||||
let event_loop = self.event_loop.take().ok_or(WsgError::WindowSystem)?; // Erreur si déjà pris
|
||||
@@ -33,8 +65,13 @@ impl App {
|
||||
event: winit::event::WindowEvent::RedrawRequested,
|
||||
..
|
||||
} => {
|
||||
// render logic
|
||||
// Rendering logic
|
||||
let frame = self.context.get_next_frame();
|
||||
|
||||
// On appelle le render() de l'utilisateur
|
||||
handler.render(&mut self);
|
||||
// On présente automatiquement
|
||||
self.renderer.present(frame);
|
||||
}
|
||||
winit::event::Event::WindowEvent {
|
||||
event: winit::event::WindowEvent::CloseRequested,
|
||||
@@ -49,13 +86,20 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for constructing a configured `App` instance with custom title and dimensions.
|
||||
/// Provides a fluent API for setting window properties before building the full application context.
|
||||
pub struct AppBuilder {
|
||||
/// Window title displayed in the OS taskbar/window decorations.
|
||||
title: String,
|
||||
/// Window width in pixels.
|
||||
width: u32,
|
||||
/// Window height in pixels.
|
||||
height: u32,
|
||||
}
|
||||
|
||||
impl AppBuilder {
|
||||
/// Creates an AppBuilder with default values: "WSG App" title, 800x600 resolution.
|
||||
/// Called as the entry point of the builder pattern — always start here.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
title: APP_DEFAULT_TITLE.to_string(),
|
||||
@@ -63,15 +107,23 @@ impl AppBuilder {
|
||||
height: APP_DEFAULT_HEIGHT,
|
||||
}
|
||||
}
|
||||
/// Sets the window title to display in the OS taskbar/window decorations.
|
||||
/// Inputs: title (string reference). Returns Self for method chaining.
|
||||
pub fn title(mut self, title: &str) -> Self {
|
||||
self.title = title.to_string();
|
||||
self
|
||||
}
|
||||
/// Sets the window dimensions in pixels.
|
||||
/// Inputs: width (pixel count), height (pixel count). Returns Self for method chaining.
|
||||
pub fn size(mut self, width: u32, height: u32) -> Self {
|
||||
self.width = width;
|
||||
self.height = height;
|
||||
self
|
||||
}
|
||||
/// Builds the configured `App` instance by creating all required components in order:
|
||||
/// EventLoop → Window → Context → Renderer → PipelineCache → Scene.
|
||||
/// Returns Ok(App) on success or Err(WsgError) if any component fails during creation.
|
||||
/// Called after setting desired properties via the builder pattern; triggers async GPU initialization.
|
||||
pub async fn build(self) -> Result<App, WsgError> {
|
||||
let event_loop = EventLoop::new().unwrap();
|
||||
let window = Arc::new(
|
||||
|
||||
+12
-8
@@ -20,8 +20,8 @@ use std::sync::Arc;
|
||||
use wgpu::{Adapter, Device, Instance, Queue, Surface};
|
||||
use winit::window::Window;
|
||||
|
||||
use crate::utils::WsgError;
|
||||
use crate::core::Frame;
|
||||
use crate::utils::WsgError;
|
||||
|
||||
/// Represents the GPU context. Holds all WGPU objects needed for rendering.
|
||||
/// Created once at startup and shared across frames via Arc.
|
||||
@@ -41,8 +41,11 @@ pub struct Context {
|
||||
impl Context {
|
||||
/// Initializes the WGPU context. Creates the surface from the window, requests a device from the adapter,
|
||||
/// and stores all required objects (instance, surface, adapter, device, queue).
|
||||
/// Called once at application startup. Returns an error if GPU initialization fails.
|
||||
/// Internal steps: 1) create Instance → 2) create Surface bound to Window lifecycle → 3) request_adapter for compatible GPU → 4) request_device for Device + Queue.
|
||||
/// Inputs: window (owned Arc reference to winit Window, provides display surface binding).
|
||||
/// Returns Ok(Context) on success or Err(WsgError) describing why initialization failed.
|
||||
/// Called once at application startup before any rendering occurs.
|
||||
/// Internal steps: 1) create Instance → 2) create Surface bound to Window lifecycle →
|
||||
/// 3) request_adapter for compatible GPU → 4) request_device for Device + Queue.
|
||||
pub async fn new(window: Arc<Window>) -> Result<Self, WsgError> {
|
||||
// WGPU instance
|
||||
let instance = wgpu::Instance::default();
|
||||
@@ -77,10 +80,11 @@ impl Context {
|
||||
}
|
||||
|
||||
/// Configures the surface with a render format and alpha mode for rendering.
|
||||
/// Inputs: adapter (GPU capabilities), width/height (surface resolution).
|
||||
/// Returns Ok(()) on success or SurfaceIncompatible if no SRGB format + alpha mode exist.
|
||||
/// Typically called by the renderer when window size changes.
|
||||
/// Internal steps: 1) get_capabilities(adapter) → 2) find SRGB format (fallback to first available) → 3) select first alpha mode → 4) build SurfaceConfiguration → 5) configure() the surface.
|
||||
/// Inputs: adapter (GPU capabilities), width/height (surface resolution in pixels).
|
||||
/// Returns Ok(format) with the chosen texture format or Err(SurfaceIncompatible) if no valid config exists.
|
||||
/// Typically called by the renderer when window size changes. Called once during AppBuilder::build().
|
||||
/// Internal steps: 1) get_capabilities(adapter) → 2) find SRGB format (fallback to first available) →
|
||||
/// 3) select first alpha mode → 4) build SurfaceConfiguration → 5) configure() the surface.
|
||||
pub fn configure(
|
||||
&self,
|
||||
adapter: &wgpu::Adapter,
|
||||
@@ -119,7 +123,7 @@ impl Context {
|
||||
|
||||
/// Acquires the next surface texture for rendering this frame. Returns an error variant
|
||||
/// describing why acquisition failed (timeout, occlusion, surface lost, etc.).
|
||||
/// Typically called by the orchestrator (main.rs) at the start of each frame loop iteration.
|
||||
/// Called by the orchestrator (App::run) at the start of each frame loop iteration.
|
||||
pub fn begin_frame(&self) -> Result<wgpu::SurfaceTexture, WsgError> {
|
||||
// In wgpu 30, get_current_texture() returns CurrentSurfaceTexture enum directly (not Result).
|
||||
// All variants are matched to provide explicit error handling instead of panicking.
|
||||
|
||||
@@ -24,9 +24,11 @@ pub struct Frame {
|
||||
|
||||
impl Frame {
|
||||
/// Acquires the next surface texture and creates a TextureView over it.
|
||||
/// Called by the orchestrator (main.rs) at the start of each frame loop iteration.
|
||||
/// Panics if the surface cannot be acquired (e.g., lost, occluded). For non-panicking
|
||||
/// alternatives, use try_new(). Internal steps: 1) get_current_texture() → 2) match Success/Suboptimal → 3) create_view.
|
||||
/// Inputs: surface (borrowed reference to wgpu Surface providing access to display buffers).
|
||||
/// Returns a new Frame instance. Panics if the surface cannot be acquired (e.g., lost, occluded).
|
||||
/// Called by the orchestrator (App::run) at the start of each frame loop iteration.
|
||||
/// Internal steps: 1) get_current_texture() → 2) match Success/Suboptimal variants →
|
||||
/// 3) create_view on texture → 4) construct Frame with both fields.
|
||||
pub fn new(surface: &wgpu::Surface) -> Self {
|
||||
// In wgpu 30, get_current_texture() returns CurrentSurfaceTexture enum directly (not Result).
|
||||
// All variants are matched to provide explicit error handling instead of panicking —
|
||||
@@ -57,6 +59,7 @@ impl Frame {
|
||||
}
|
||||
|
||||
/// Attempts to acquire the next surface texture without panicking.
|
||||
/// Inputs: surface (borrowed reference to wgpu Surface providing access to display buffers).
|
||||
/// Returns Some(Frame) on success (Success/Suboptimal) or None on any error variant.
|
||||
/// Called when graceful frame skipping is preferred over crashing.
|
||||
pub fn try_new(surface: &wgpu::Surface) -> Option<Self> {
|
||||
|
||||
+15
-14
@@ -19,9 +19,8 @@
|
||||
//! - **Accès Bas-Niveau**: Advanced users can bypass Scene and call Renderer directly for custom rendering paths.
|
||||
|
||||
use crate::core::Context;
|
||||
use crate::resources::{Mesh, Material};
|
||||
use crate::core::Frame;
|
||||
|
||||
use crate::resources::{Material, Mesh};
|
||||
|
||||
/// The Executor layer of the architecture. Holds shared references to Device and Queue from Context,
|
||||
/// plus the surface texture format. Executes WGPU rendering commands by binding Materials and Meshes
|
||||
@@ -37,6 +36,8 @@ pub struct Renderer {
|
||||
|
||||
impl Renderer {
|
||||
/// Creates a Renderer by cloning Device and Queue Arc references from the Context, plus capturing the surface format.
|
||||
/// Inputs: context (borrowed reference to Context providing GPU resource handles), format (surface texture format).
|
||||
/// Returns a new Renderer instance sharing the same underlying GPU resources as Context.
|
||||
/// Called once at application startup during scene setup. The Renderer shares these resources via Arc;
|
||||
/// Context retains ownership and can continue using them after this call.
|
||||
pub fn new(context: &Context, format: wgpu::TextureFormat) -> Self {
|
||||
@@ -49,20 +50,18 @@ impl Renderer {
|
||||
|
||||
/// Orchestrates rendering of a single object: binds Material pipeline + Mesh vertex data into a RenderPass,
|
||||
/// then submits commands to the GPU queue for execution. Called per-frame by the orchestrator (main.rs).
|
||||
/// Inputs: view (TextureView color attachment target), mesh (geometry to render), material (shader+pipeline).
|
||||
/// Internal steps: 1) create CommandEncoder → 2) begin RenderPass with color attachment →
|
||||
/// 3) set_pipeline(material.pipeline) → 4) set_vertex_buffer(mesh.vertex_buffer) →
|
||||
/// 5) draw_indexed or draw based on index buffer presence → 6) drop render_pass end scope →
|
||||
/// 7) submit encoder via queue.
|
||||
pub fn render(
|
||||
&self,
|
||||
view: &wgpu::TextureView,
|
||||
mesh: &Mesh,
|
||||
material: &Material,
|
||||
) {
|
||||
/// 3) set_pipeline(material.pipeline) → 4) set_vertex_buffer(mesh.vertex_buffer) →
|
||||
/// 5) draw_indexed or draw based on index buffer presence → 6) drop render_pass end scope →
|
||||
/// 7) submit encoder via queue.
|
||||
pub fn render(&self, view: &wgpu::TextureView, mesh: &Mesh, material: &Material) {
|
||||
// Create per-frame command encoder; its lifetime is scoped to this function only.
|
||||
let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||
label: Some("render encoder"),
|
||||
});
|
||||
let mut encoder = self
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||
label: Some("render encoder"),
|
||||
});
|
||||
|
||||
// RenderPass borrows encoder mutably — must end (drop) before encoder.finish() below.
|
||||
// This scope boundary enforces Rust's borrow checker rules for GPU synchronization.
|
||||
@@ -106,11 +105,13 @@ impl Renderer {
|
||||
}
|
||||
|
||||
/// Returns a reference to the owned Device for direct access when needed (e.g., PipelineCache creation).
|
||||
/// Called internally during scene setup; not typically used by external code.
|
||||
pub fn device(&self) -> &wgpu::Device {
|
||||
&self.device
|
||||
}
|
||||
|
||||
/// Returns the surface texture output format used for rendering.
|
||||
/// Called internally during Material/PipelineCache initialization to ensure pipeline compatibility.
|
||||
pub fn format(&self) -> wgpu::TextureFormat {
|
||||
self.format
|
||||
}
|
||||
|
||||
@@ -1,6 +1,34 @@
|
||||
//! # AppHandler Trait — User-Defined Game Logic Interface
|
||||
//!
|
||||
//! Defines the `AppHandler` trait that users implement to inject their game logic into the render loop.
|
||||
//! Provides two callback points: `update()` for pre-render logic (physics, input processing) and
|
||||
//! `render()` for draw call execution. Both methods receive mutable access to the `App` facade so
|
||||
//! users can modify resources, entities, or other state during each frame iteration.
|
||||
//!
|
||||
//! ## Interaction with Other Modules
|
||||
//! - **app**: The orchestrator calls update() before rendering and render() during the RedrawRequested event.
|
||||
//! AppHandler has no direct knowledge of wgpu internals — it operates only through the App facade.
|
||||
//! - **scene::Scene**: Users typically manipulate app.scene inside these callbacks to add/remove entities.
|
||||
//! - **pipeline::PipelineCache**: Users may create new Materials via cache.get_or_create() in update().
|
||||
//!
|
||||
//! ## Architecture Note
|
||||
//! Per ARCHI_APP.md, this trait is one half of the "App" facade pattern. It enables a declarative workflow
|
||||
//! where users define their game logic without touching WGPU directly, while keeping the freedom to build
|
||||
//! the engine "brick by brick" through direct Context/PipelineCache/Renderer manipulation if needed.
|
||||
|
||||
use crate::app::App;
|
||||
|
||||
/// Trait defining user-provided game logic injected into the render loop at two callback points.
|
||||
/// Users implement this trait to define what happens per-frame: update (pre-render logic) and
|
||||
/// render (draw call execution). Default implementations provide empty update for convenience.
|
||||
pub trait AppHandler {
|
||||
/// Called once per frame before rendering begins. Used for physics updates, input processing,
|
||||
/// entity management, and any other pre-render logic. Default implementation does nothing.
|
||||
/// Inputs: _app — mutable reference to the App facade providing access to all subsystems.
|
||||
fn update(&mut self, _app: &mut App) {}
|
||||
/// Called during each RedrawRequested event after frame acquisition. Used for executing draw calls
|
||||
/// by iterating Scene entities and calling app.renderer.render(view, mesh, material) per entity.
|
||||
/// Must be implemented — called every frame that needs rendering.
|
||||
/// Inputs: app — mutable reference to the App facade providing access to all subsystems.
|
||||
fn render(&mut self, app: &mut App);
|
||||
}
|
||||
|
||||
+19
-4
@@ -1,14 +1,22 @@
|
||||
//! # WSG Library Crate Root
|
||||
//!
|
||||
//! The top-level entry point for the wsg-lib crate. Exposes five public modules organized by architectural responsibility:
|
||||
//! **core** (Manager + Executor layers), **resources** (data types), **pipeline** (shader compilation cache),
|
||||
//! **scene** (resource graph and entity management), and **utils** (configuration and error handling).
|
||||
//! The top-level entry point for the wsg-lib crate. Exposes seven public modules organized by architectural responsibility:
|
||||
//! **app** (App facade + AppBuilder), **handler** (AppHandler trait), **core** (Manager + Executor layers),
|
||||
//! **resources** (data types), **pipeline** (shader compilation cache), **scene** (resource graph and entity management),
|
||||
//! and **utils** (configuration and error handling).
|
||||
//!
|
||||
//! ## Module Interaction Map
|
||||
//! - `core` consumes resources from `resources`, pipelines from `pipeline`, and errors from `utils`.
|
||||
//! - `scene` aggregates Resources, Materials, and Pipelines into an entity graph.
|
||||
//! - `app` orchestrates all subsystems plus the event loop; depends on everything else.
|
||||
//! - `handler` defines the user-facing interface consumed by `app`.
|
||||
//! - `utils` is a leaf module — no internal dependencies on other library modules.
|
||||
//!
|
||||
//! ## Top-Level Re-Exports
|
||||
//! These are the two primary types users interact with when building applications:
|
||||
//! - `App` — high-level application facade wrapping window lifecycle, GPU context, and render automation.
|
||||
//! - `AppHandler` — trait users implement to inject game logic into the render loop.
|
||||
//!
|
||||
//! ## Usage
|
||||
//! Consumers import through the re-exports defined in each submodule's `mod.rs`:
|
||||
//! ```ignore
|
||||
@@ -16,6 +24,7 @@
|
||||
//! use wsg_lib::resources::{Mesh, Material, Vertex};
|
||||
//! use wsg_lib::utils::BASIC_SHADER;
|
||||
//! ```
|
||||
|
||||
pub mod app;
|
||||
pub mod core;
|
||||
pub mod handler;
|
||||
@@ -24,4 +33,10 @@ pub mod resources;
|
||||
pub mod scene;
|
||||
pub mod utils;
|
||||
|
||||
pub use handler::AppHandler;
|
||||
/// Re-export of the high-level application facade for convenient top-level access.
|
||||
/// Users create App instances via `AppBuilder`, then call `.run(handler)` to start the application.
|
||||
pub use crate::app::App;
|
||||
|
||||
/// Re-export of the user-defined game logic interface for convenient top-level access.
|
||||
/// Users implement this trait to define update/render callbacks injected into the render loop.
|
||||
pub use crate::handler::AppHandler;
|
||||
|
||||
@@ -33,6 +33,8 @@ pub struct PipelineCache {
|
||||
|
||||
impl PipelineCache {
|
||||
/// Creates an empty pipeline cache with no pre-loaded shaders or pipelines.
|
||||
/// Inputs: device (owned Arc reference to wgpu Device, required for creating ShaderModules and RenderPipelines).
|
||||
/// Returns a new PipelineCache ready for shader registration via register_shader().
|
||||
/// Called at application startup before any Material creation. Shader paths must be registered via register_shader() first.
|
||||
pub fn new(device: Arc<wgpu::Device>) -> Self {
|
||||
Self {
|
||||
@@ -45,7 +47,7 @@ impl PipelineCache {
|
||||
}
|
||||
/// Registers an external WGSL shader file path associated with a given ID.
|
||||
/// Inputs: id (unique key for this shader), path (filesystem path to .wgsl file).
|
||||
/// Returns Ok(id) on success or Err if the ID is already registered. Called during scene setup to register custom shaders.
|
||||
/// Returns Ok(id) on success or Err(String) if the ID is already registered. Called during scene setup to register custom shaders.
|
||||
pub fn register_shader(&mut self, id: &str, path: &str) -> Result<String, String> {
|
||||
if self.shader_paths.contains_key(id) {
|
||||
return Err(format!("ID '{}' already exists.", id));
|
||||
@@ -56,20 +58,23 @@ impl PipelineCache {
|
||||
|
||||
/// Unregisters a shader by its ID, removing both the path reference and any cached compiled pipeline.
|
||||
/// Inputs: id (the shader identifier to remove).
|
||||
/// Returns Ok(id) on success or Err if the ID does not exist. Called when a shader should be freed from GPU memory.
|
||||
/// Returns Ok(id) on success or Err(String) if the ID does not exist. Called when a shader should be freed from GPU memory.
|
||||
pub fn unregister_shader(&mut self, id: &str) -> Result<String, String> {
|
||||
if self.shader_paths.remove(id).is_none() {
|
||||
return Err(format!("ID '{}' does not exist.", id));
|
||||
}
|
||||
// Remove cached pipeline so GPU memory is freed (wgpu drops it automatically)
|
||||
self.pipelines.remove(id);
|
||||
|
||||
Ok(id.to_string())
|
||||
}
|
||||
|
||||
/// Retrieves a cached RenderPipeline by shader_id, or creates one on-demand if not present.
|
||||
/// Inputs: device (GPU command source), format (surface texture format for fragment output),
|
||||
/// shader_id (unique key into the cache).
|
||||
/// Inputs: format (surface texture format for fragment output), shader_id (unique key into the cache).
|
||||
/// Returns an Arc-wrapped RenderPipeline ready for rendering. Called by Material::new().
|
||||
/// Internal steps: 1) check pipelines HashMap for existing entry →
|
||||
/// 2a) if found: clone Arc and return →
|
||||
/// 2b) if not found: load_shader() + build_pipeline() → cache behind Arc → insert and return.
|
||||
pub fn get_or_create(
|
||||
&mut self,
|
||||
format: wgpu::TextureFormat,
|
||||
@@ -97,7 +102,8 @@ impl PipelineCache {
|
||||
}
|
||||
|
||||
/// Loads a WGSL shader module: reads from disk first, falls back to the embedded BASIC_SHADER constant.
|
||||
/// Called internally by `get_or_create()` when compiling a new pipeline.
|
||||
/// Inputs: device (GPU command source for shader compilation), path (file path or shader_id string).
|
||||
/// Returns a compiled wgpu::ShaderModule. Called internally by `get_or_create()` when compiling a new pipeline.
|
||||
fn load_shader(&self, device: &wgpu::Device, path: &str) -> wgpu::ShaderModule {
|
||||
let source = std::fs::read_to_string(path).unwrap_or_else(|_| {
|
||||
println!("Shader not found: {}, falling back to default", path);
|
||||
@@ -113,6 +119,9 @@ impl PipelineCache {
|
||||
/// Builds a RenderPipeline from a shader module, device, and surface texture format.
|
||||
/// Inputs: device (GPU command source), format (output texture format), shader (compiled WGSL module).
|
||||
/// Returns a fully configured RenderPipeline ready for draw calls. Called internally by `get_or_create()`.
|
||||
/// Internal steps: 1) define VertexBufferLayout from Vertex struct offsets →
|
||||
/// 2) create PipelineLayout with bind_group_layouts + immediate_size →
|
||||
/// 3) create RenderPipeline with vertex/fragment states, primitive config, multisample state.
|
||||
fn build_pipeline(
|
||||
device: &wgpu::Device,
|
||||
format: wgpu::TextureFormat,
|
||||
|
||||
@@ -23,9 +23,9 @@ pub struct Material {
|
||||
|
||||
impl Material {
|
||||
/// Creates a new Material by requesting the cache to provide (or create) its RenderPipeline.
|
||||
/// Inputs: device (GPU command source for pipeline creation), format (surface texture format),
|
||||
/// shader_id (unique key into PipelineCache), cache (mutable ref for potential insertion).
|
||||
/// Returns a Material holding the Arc-wrapped pipeline. Called at scene initialization time.
|
||||
/// Inputs: format (surface texture format required for fragment output), shader_id (unique key into PipelineCache),
|
||||
/// cache (mutable reference for potential insertion of new pipelines).
|
||||
/// Returns a Material holding the Arc-wrapped pipeline. Called at scene initialization time only.
|
||||
pub fn new(format: wgpu::TextureFormat, shader_id: &str, cache: &mut PipelineCache) -> Self {
|
||||
// Request pipeline from cache — returns cached instance if already exists, creates new otherwise
|
||||
let pipeline = cache.get_or_create(format, shader_id);
|
||||
|
||||
@@ -27,8 +27,12 @@ pub struct Mesh {
|
||||
|
||||
impl Mesh {
|
||||
/// Creates a new Mesh by uploading vertex and optional index data to GPU buffers.
|
||||
/// Inputs: device (GPU command source), vertices (CPU-side vertex array), indices (optional CPU-side index array).
|
||||
/// Inputs: device (GPU command source for buffer creation), vertices (CPU-side vertex array to upload),
|
||||
/// indices (optional CPU-side index array for indexed drawing).
|
||||
/// Returns a Mesh with two GPU buffers ready for rendering. Called at scene initialization time only.
|
||||
/// Internal steps: 1) create_buffer_init for vertex data →
|
||||
/// 2) if indices provided: create_buffer_init for index data and set num_indices = len →
|
||||
/// else: set index_buffer = None and num_indices = 0.
|
||||
pub fn new(device: &wgpu::Device, vertices: &[Vertex], indices: Option<&[u16]>) -> Self {
|
||||
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("Mesh Vertex Buffer"),
|
||||
|
||||
@@ -15,13 +15,13 @@
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct Vertex {
|
||||
/// XYZ coordinates of this vertex in world space. Offset: 0 bytes.
|
||||
/// XYZ coordinates of this vertex in world space. Offset: 0 bytes (12 bytes total as [f32;3]).
|
||||
pub position: [f32; 3],
|
||||
/// XYZ coordinates of the vertex normal. Offset: 12 bytes.
|
||||
/// XYZ coordinates of the vertex normal. Offset: 12 bytes (12 bytes total as [f32;3]).
|
||||
pub normal: [f32; 3],
|
||||
/// UV texture coordinates. Offset: 24 bytes
|
||||
/// UV texture coordinates. Offset: 24 bytes (8 bytes total as [f32;2]).
|
||||
pub uv: [f32; 2],
|
||||
/// RGBA color values. Offset: 32 bytes.
|
||||
/// RGBA color values. Offset: 32 bytes (16 bytes total as [f32;4]).
|
||||
pub color: [f32; 4],
|
||||
}
|
||||
|
||||
|
||||
+14
-11
@@ -18,9 +18,9 @@ use std::sync::Arc;
|
||||
/// and maps entity labels to their associated mesh+material pairs for rendering iteration.
|
||||
/// Created once during application setup; entities are added before the render loop starts.
|
||||
pub struct Scene {
|
||||
/// Map of mesh identifiers to owned Mesh instances. Populated via `add_mesh()`.
|
||||
/// Map of mesh identifiers to owned Arc<Mesh> instances. Populated via `add_mesh()`.
|
||||
meshes: HashMap<String, Arc<Mesh>>,
|
||||
/// Map of material identifiers to owned Material instances. Populated via `add_material()`.
|
||||
/// Map of material identifiers to owned Arc<Material> instances. Populated via `add_material()`.
|
||||
materials: HashMap<String, Arc<Material>>,
|
||||
/// Map of entity labels to (mesh_id, material_id) associations. Populated via `add_entity()`.
|
||||
entities: HashMap<String, (String, String)>,
|
||||
@@ -38,7 +38,7 @@ impl Scene {
|
||||
}
|
||||
|
||||
/// Registers a Mesh in the scene under a unique identifier.
|
||||
/// Inputs: id (unique key), mesh (Mesh instance). Returns Ok(id) on success or Err if already exists.
|
||||
/// Inputs: id (unique key), mesh (Arc-wrapped Mesh instance). Returns Ok(id) on success or Err(String) if already exists.
|
||||
/// Called during scene initialization when building the resource depot.
|
||||
pub fn add_mesh(&mut self, id: &str, mesh: Arc<Mesh>) -> Result<String, String> {
|
||||
if self.meshes.contains_key(id) {
|
||||
@@ -49,7 +49,7 @@ impl Scene {
|
||||
}
|
||||
|
||||
/// Registers a Material in the scene under a unique identifier.
|
||||
/// Inputs: id (unique key), material (Material instance). Returns Ok(id) on success or Err if already exists.
|
||||
/// Inputs: id (unique key), material (Arc-wrapped Material instance). Returns Ok(id) on success or Err(String) if already exists.
|
||||
/// Called during scene initialization when building the resource depot.
|
||||
pub fn add_material(&mut self, id: &str, material: Arc<Material>) -> Result<String, String> {
|
||||
if self.materials.contains_key(id) {
|
||||
@@ -60,9 +60,11 @@ impl Scene {
|
||||
}
|
||||
|
||||
/// Associates an entity label with a mesh and material pair for rendering iteration.
|
||||
/// Inputs: label (entity identifier), mesh_id (key into meshes map), material_id (key into materials map).
|
||||
/// Returns Ok(label) on success or Err if either referenced resource does not exist.
|
||||
/// Inputs: label (entity identifier string), mesh_id (key into meshes map), material_id (key into materials map).
|
||||
/// Returns Ok(label) on success or Err(String) if either referenced resource does not exist.
|
||||
/// Called during scene initialization to build the renderable entity graph.
|
||||
/// Internal steps: 1) validate mesh_id exists → 2) validate material_id exists →
|
||||
/// 3) insert association into entities HashMap.
|
||||
pub fn add_entity(
|
||||
&mut self,
|
||||
label: &str,
|
||||
@@ -75,7 +77,10 @@ impl Scene {
|
||||
if !self.materials.contains_key(material_id) {
|
||||
return Err(format!("Material '{}' does not exist.", material_id));
|
||||
}
|
||||
self.entities.insert(label.to_string(), (mesh_id.to_string(), material_id.to_string()));
|
||||
self.entities.insert(
|
||||
label.to_string(),
|
||||
(mesh_id.to_string(), material_id.to_string()),
|
||||
);
|
||||
Ok(label.to_string())
|
||||
}
|
||||
|
||||
@@ -93,12 +98,10 @@ impl Scene {
|
||||
|
||||
/// Iterates all entity associations, yielding (label, mesh_ref, material_ref) triples.
|
||||
/// Called by the orchestrator during each render pass to draw every entity in order.
|
||||
pub fn iter_entities(
|
||||
&self,
|
||||
) -> impl Iterator<Item = (&str, &Arc<Mesh>, &Arc<Material>)> + '_ {
|
||||
pub fn iter_entities(&self) -> impl Iterator<Item = (&str, &Arc<Mesh>, &Arc<Material>)> + '_ {
|
||||
self.entities.iter().map(|(label, (mesh_id, mat_id))| {
|
||||
let mesh = self.meshes.get(mesh_id).unwrap(); // safe: add_entity validates existence
|
||||
let mat = self.materials.get(mat_id).unwrap(); // same invariant
|
||||
let mat = self.materials.get(mat_id).unwrap(); // same invariant
|
||||
(label.as_str(), mesh, mat)
|
||||
})
|
||||
}
|
||||
|
||||
+17
-10
@@ -4,18 +4,25 @@
|
||||
|
||||
Contains WGSL shader source files used by the PipelineCache module. These are loaded at runtime from disk when referenced by their registered ID in PipelineCache.register_shader(). If a file is missing, PipelineCache falls back to the embedded BASIC_SHADER constant defined in utils::conf.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| **basic_shader.wgsl** | Default vertex/fragment shader pair (vs_main / fs_main entry points) with position, normal, UV, and color attributes matching the Vertex struct layout. |
|
||||
| **basic_shader.wgsl** | Default vertex/fragment shader pair (vs_main / fs_main entry points) with position, uv, and color attributes. |
|
||||
|
||||
## Shader Contract
|
||||
## Shader Contract (basic_shader.wgsl)
|
||||
|
||||
The WGSL shader must define:
|
||||
The WGSL shader defines:
|
||||
|
||||
- `@vertex fn vs_main() -> @builtin(position) vec4<f32>` — vertex entry point
|
||||
- `@fragment fn fs_main() -> @location(0) vec4<f32>` — fragment entry point writing RGBA output
|
||||
- Vertex input attributes matching the 56-byte stride of resources::Vertex:
|
||||
- `@location(0)` → position `[f32; 3]` (offset 0)
|
||||
- `@location(1)` → normal `[f32; 3]` (offset 12)
|
||||
- `@location(2)` → uv `[f32; 2]` (offset 24)
|
||||
- `@location(3)` → color `[f32; 4]` (offset 32)
|
||||
- `@vertex fn vs_main(model: VertexInput) -> VertexOutput` — vertex entry point
|
||||
- `@fragment fn fs_main(in: VertexOutput) -> @location(0) vec4<f32>` — fragment entry point writing RGBA output
|
||||
|
||||
### Vertex Input Layout
|
||||
|
||||
| Location | Attribute | Type | Offset (bytes) |
|
||||
|----------|-----------|------|----------------|
|
||||
| 0 | position | vec3<f32> | 0 |
|
||||
| 1 | uv | vec2<f32> | 12 |
|
||||
| 2 | color | vec3<f32> | 24 |
|
||||
|
||||
**Note**: This shader uses a 39-byte vertex stride (3+2+3 floats). It does NOT include normal data or alpha channel interpolation — it outputs fully opaque geometry with per-vertex color passthrough. This differs from the full `Vertex` struct layout (56 bytes with normal + alpha) defined in resources::Vertex; if a full shader matching the Vertex struct is needed, extend this shader accordingly.
|
||||
|
||||
@@ -1,4 +1,22 @@
|
||||
//! # Basic Shader Module
|
||||
//!
|
||||
//! Default vertex/fragment shader pair used by PipelineCache when no external .wgsl file is found.
|
||||
//! This shader implements a simple unlit rendering path: passes through position and color attributes
|
||||
//! from VertexInput to fragment output, producing flat-colored geometry without lighting calculations.
|
||||
//!
|
||||
//! ## Shader Contract
|
||||
//! Must define entry points matching PipelineCache::build_pipeline():
|
||||
//! - @vertex fn vs_main(model: VertexInput) -> VertexOutput
|
||||
//! - model.position → @location(0), vec3<f32>, offset 0 bytes in vertex buffer
|
||||
//! - model.uv → @location(1), vec2<f32>, offset 12 bytes in vertex buffer
|
||||
//! - model.color → @location(2), vec3<f32>, offset 24 bytes in vertex buffer
|
||||
//! - @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4<f32>
|
||||
//! - Writes RGBA output where alpha is hardcoded to 1.0 (fully opaque).
|
||||
//!
|
||||
//! ## Technical Notes
|
||||
//! - No normal or UV interpolation — this is an unlit shader that directly outputs the per-vertex color.
|
||||
//! - The clip_position is computed as vec4<f32>(position, 1.0), assuming position is already in NDC space.
|
||||
|
||||
struct VertexInput {
|
||||
@location(0) position: vec3<f32>,
|
||||
@location(1) uv: vec2<f32>,
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
//! Holds shared constants for the WSG library — primarily shader paths and embedded WGSL source code.
|
||||
//! Centralized here so all submodules import from one place instead of duplicating literal strings.
|
||||
//! This enables the PipelineCache to fall back to an embedded default shader when the file-based one is missing.
|
||||
//! Also provides application defaults for window title, width, and height used by AppBuilder.
|
||||
//!
|
||||
//! ## Interaction with Other Modules
|
||||
//! - **pipeline_cache::load_shader()** reads `BASIC_SHADER_PATH` from disk; if unreadable, falls back to `BASIC_SHADER`.
|
||||
//! - Both constants are compile-time values (`include_str!`) ensuring the fallback shader is always available even without external files.
|
||||
//! - **app::AppBuilder** reads APP_DEFAULT_TITLE, APP_DEFAULT_WIDTH, and APP_DEFAULT_HEIGHT for default window configuration.
|
||||
|
||||
/// Path to the default WGSL shader file on disk (runtime). Used by PipelineCache::load_shader() for file-based loading.
|
||||
pub const BASIC_SHADER_PATH: &str = "assets/shaders/basic_shader.wgsl";
|
||||
@@ -15,6 +17,11 @@ pub const BASIC_SHADER_PATH: &str = "assets/shaders/basic_shader.wgsl";
|
||||
/// Serves as a fallback when `BASIC_SHADER_PATH` cannot be read at runtime.
|
||||
pub const BASIC_SHADER: &str = include_str!("../shaders/basic_shader.wgsl");
|
||||
|
||||
/// Default application title displayed in the OS taskbar/window decorations.
|
||||
pub const APP_DEFAULT_TITLE: &str = "WSG App";
|
||||
|
||||
/// Default window width in pixels.
|
||||
pub const APP_DEFAULT_WIDTH: u32 = 800;
|
||||
|
||||
/// Default window height in pixels.
|
||||
pub const APP_DEFAULT_HEIGHT: u32 = 600;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user