--- type: Technical Specification title: Frame Loop Architecture description: Technical specification for the frame loop architecture in wsg_lib, detailing the immutable frame lifetime cycle and resource management tags: [architecture, rendering, frame-loop, gpu, wgpu] actor: person/jerome sources: [] generated: { by: human:jerome, at: 2026-07-31T00:00:00Z } verified: true status: current stale_after: 2027-01-31 --- # La Boucle de Rendu (Frame Loop) > **État du document : ACTUEL (implémenté).** Ce document décrit la frame lifetime telle qu'elle est > réellement implémentée. **Deux flux coexistent** : le flux **facade `App`** (rendu automatique de la > scène, `App::render_scene` — le workflow recommandé, exemples `simple`/`cube`/`demo`) et le flux > **manuel** (`Context`/`Renderer`/`Frame` pilotés à la main — exemple `manual`, un objet par soumission). > Le pipeline **GPU-driven** (compute pass + draw indirect) de l'état **visé** est décrit dans > [ARCHI_APP](ARCHI_APP.md) / [ARCHI_CPU_GPU](ARCHI_CPU_GPU.md) (cible). Pour afficher quelque chose, nous suivons un cycle immuable appelé la Frame Lifetime, basé sur `Frame` : **Flux facade `App` (recommandé — `App::run` + `AppHandler`) :** - **`Context::get_next_frame()`** : acquiert la surface texture et crée sa `TextureView` (dans `Frame`). - **`AppHandler::render` (défaut) → `App::render_scene(view)`** : le moteur itère les entités de la scène et les dessine en **une passe groupée** (un `CommandEncoder` + une soumission par frame ; passe d'ombre en tête si un caster est actif). - **`Renderer::present(frame)`** : présente l'image à l'écran. - Chaque frame, avant `update`, le moteur appelle `device.poll()` (les callbacks asynchrones wgpu — `on_submitted_work_done`, `map_async` — ne se déclenchent que lors d'un poll), et la fenêtre redimensionnée est gérée par `App::resize` (surface + depth texture recréées ensemble). **Flux `manual` (exemple `manual` — un objet par soumission) :** - **`Context::get_next_frame()`** (ou `Frame::try_new(&context.surface)`) : acquiert la surface texture et crée sa `TextureView` (dans `Frame`). - **`Renderer::render(&view, &mesh, &material)`** : crée un `CommandEncoder`, écrit les ordres de dessin dans la `TextureView`, puis soumet à la file (`queue`). - **`Renderer::present(frame)`** : présente l'image à l'écran. **Variante bas niveau (API `Context` brute, sans `Frame`) :** - **`Context::begin_frame()`** : acquiert la surface et renvoie la `wgpu::SurfaceTexture` (sans vue). - **`Context::end_frame(surface_texture)`** : soumet et présente cette texture. ## Liens - [ARCHI_APP](ARCHI_APP.md) · [ARCHI_RENDU](ARCHI_RENDU.md) · [ARCHI_CPU_GPU](ARCHI_CPU_GPU.md) · [ARCHI_ARENES](ARCHI_ARENES.md) - Documentation utilisateur : [docs/user](../user/README.md) · [README racine](../../README.md) · [ROADMAP](../ROADMAP.md) - Référence API : `cargo doc -p wsg-lib --no-deps` --- ## Pourquoi cette séparation est vitale Le bloc `{ let mut render_pass = ... }` est crucial. Dans Rust, `render_pass` emprunte mutablement `encoder`. Il doit être détruit (via la fin du bloc ou un `drop()`) avant que tu puisses appeler `encoder.finish()`. Si tu oublies cela, le compilateur Rust refusera de compiler, empêchant ainsi des bugs critiques de synchronisation GPU. --- ## Ressources : Persistantes vs Par-Frame Avec notre nouvelle architecture "Atelier", la distinction est devenue encore plus nette : | Élément | Durée de vie | Pourquoi ? | |---------|-------------|------------| | SurfaceConfiguration | Persistante | Ne change qu'au redimensionnement. | | RenderPipeline | Persistante | Stocké dans le PipelineCache (`Arc`), compilation unique. | | Material | Persistante | Définit le look ; partage le pipeline via `Arc`. | | Mesh | Persistante | Les données géométriques sont envoyées une fois au GPU. | | CommandEncoder | Par-Frame | Ton "carnet de notes" temporaire pour les ordres du GPU. | | TextureView | Par-Frame | Fenêtre temporaire sur la texture active du swapchain. | > **Ressources GPU persistantes (single buffer) — implémenté (Phase 3, 2026-09-22)** : les buffers > Transform, Matrix, BBox et Indirect Draw vivent en VRAM (créés à l'initialisation du `Renderer`, > capacité fixe de 256 slots). Le CPU écrit les transforms chaque frame par `queue.write_buffer` > **dans le même `CommandEncoder`** que les compute passes, qui les lisent **dans la même frame** > (l'ordre est garanti par l'encoder, pas par `queue.submit()` inter-frames). Le double buffering > reste la **cible** si des artefacts apparaissent à haute fréquence (voir ARCHI_CPU_GPU / ARCHI_APP). ---