Files
wsg/docs/tech/ARCHI_CPU_GPU.md
T
Jérôme Bousquié 004761252b LOD: fige les jumeaux de fente (slit twins) + blend UV linéaire (le fold est supprimé)
- weld: Δ UV = 0.5 exact n'est plus soudé (ambigu: fente à sa plus large
  vs saut légitime — la colonne u=1 du cône vs le chart disque du cap
  tombait exactement dessus et mélangeait les charts)
- collapse: les UV se blendent linéairement. Le fold par coordonnée
  (Δ entier → 0) figeait l'UV du sommet sur les vertices de base
  (bande de rayures, signalée par l'utilisateur) — il était inutile:
  les jumeaux de fente sont gelés, aucun repli ne traverse la fente
- welded renvoie un struct Welded (clippy)
- test cône: dual-chart (fan latéral bilinéaire + disque du cap),
  surface latérale r = λ; test seam/span réécrit selon la sémantique finale
- doc: gpu-driven.md, ARCHI_CPU_GPU.md (box LOD), DRAFT.md (D10) alignés
  sur la sémantique finale (gel des jumeaux, blend linéaire, seuil strict)
- AGENTS.md: gotcha 'ne jamais folder un saut entier de tuile'

102 tests passent, demo lance et rend sans erreur.
2026-09-23 16:55:44 +02:00

11 KiB
Raw Blame History

type, title, description, tags, actor, sources, generated, verified, status, stale_after
type title description tags actor sources generated verified status stale_after
Technical Specification GPU-Driven 3D Rendering Architecture with wGPU Technical specification for GPU-driven 3D rendering architecture using wgpu, focusing on CPU-GPU workload distribution and performance optimization
architecture
rendering
gpu
cpu
performance
wgpu
person/jerome
by at
human:jerome 2026-07-31T00:00:00Z
true current 2027-01-31

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.

État du document : ACTUEL (implémenté — Phase 3 du ROADMAP, Étapes 17–19, validé 2026-09-22). La répartition CPU/GPU, le compute pass (World Matrices + Frustum Culling), l'Indirect Draw Buffer et les buffers persistants en VRAM décrits ici sont en place : shaders/gpu_driven.wgsl (deux entry points compute_matrices + cull, un module, layout explicite à 3 groupes) et les buffers de slots du Renderer (TransformSlot/MatSlot/BBoxSlot/DrawSlot/CullUniforms, capacité fixe de 256 slots). Ce document est la référence durable de la conception : le draft d'origine de l'Étape 17 (décisions D1–D14, layouts, plan de validation) a été vidé de docs/DRAFT.md après validation et vit dans le git (git show 3a424af:docs/DRAFT.md) ; l'essentiel en est repris ci-dessous. Écarts documentés (numérotation du draft d'origine) : (D1) un draw indirect par slot plutôt qu'une commande unique fusionnée ; (D12) 256 slots, slot matrice padded à 256 o (plafond uniform WebGPU) ; (D5) culling par sphère conservative dérivée de l'AABB locale du mesh, pas par l'AABB transformée exacte ; (D4) single buffer, pas de double-buffering. Piège connu (2026-09-22, D14) : l'ordre des arguments de select en WGSL est l'inverse de la convention HLSL — l'avoir inversé a produit un bug « fenêtre noire » (entités visibles remises à 0), corrigé et vérifié par readback GPU. Documenté en tête de gpu_driven.wgsl et dans AGENTS.md. Batching par material (Étape 18, 2026-09-22) : la passe principale émet désormais les draws groupés par Material (1 set_pipeline + 1 bind group @2 par matériau distinct, pas par entité ; le pass d'ombre — un seul pipeline — est inchangé). Réordonnancement sûr car tous les pipelines sont opaques (BlendState::REPLACE) ; les no-ops cullés restent émis dans leur groupe. Détail : docs/user/gpu-driven.md § « Batching by material ». LOD (Étape 19, 2026-09-23) : le pass cull remplit désormais les arguments indirects à partir du niveau de détail du slot, et non d'un seul jeu de comptes. Le choix du niveau est fait côté CPU (rayon de la sphère bounding projeté en pixels + hystérésis asymétrique — math/lod.rs, pur et unit-testé) ; le GPU n'effectue que le mappage niveau → ligne de la table LOD du mesh. Les niveaux d'un mesh sont générés par quadric edge collapse (Garland–Heckbert) au setup (Geometry::decimated : les arêtes au coût quadrique minimal sont repliées en premier ; soudure consciente des attributs — un doublon ne fusionne que si UV strictement < ½ tuile par coordonnée (un Δ = ½ exact est ambigu : fente à sa plus large vs saut légitime) ET normales proches (dot > 0.9) ; les paires refusées à UV écart d'entier sont enregistrées (jumeaux de fente) — ; un mesh sans couture reste fermé (pas de trous, pas de « books »), et sur un mesh couturé les jumeaux de fente sont gelés (toute arête qui y touche est exclue de la file — la fente zéro-largeur reste fermée à tous les niveaux) ; UVs/couleurs/normales blendés linéairement au repli (le chart est bilinéaire → exact au nouveau point — jamais de fold de tuile, qui figeait l'UV d'un sommet sur les vertices de base), normales héritées de la source (jamais recalculées — l'éclairage reste identique au niveau 0 quelle que soit l'orientation source) ; rebase u16) et empilés dans les buffers vertex/index du mesh (offsets en unités d'élément, pas d'octet — c'est ce qu'exigent les arguments drawIndirect* de WebGPU ; plafond u16 : 65 535 sommets/mesh, 4 niveaux max). LOD activé par défaut ; set_lod_enabled(false) restaure un rendu bit-à-bit identique au pré-LOD (niveau 0 partout = comptes complets). Détail : docs/user/gpu-driven.md § « Level of Detail ».

  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 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 (le culling frustum reste 100 % GPU).
  • Ce qu'il fait en plus (LOD, Étape 19) : le choix du niveau de détail par entité — O(N) projections de sphères en pixels + hystérésis, coût négligeable. C'est l'unique décision de visibilité/détail conservée côté CPU : elle dépend de la taille écran (un choix artistique), pas de la géométrie, et l'hystérésis a besoin de l'état de la frame précédente.

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.
  1. 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é (single buffer en phase initiale — la synchronisation est assurée par queue.submit() qui garantit la séquence d'exécution), et le niveau LOD de chaque slot (1 u32/slot, Étape 19). Double buffering sera ajouté uniquement si des artefacts apparaissent à haute fréquence (> 90 fps).
  • 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 : Un compute pass dédié (cull) compare la sphère bounding de chaque objet (D5 — conservative, dérivée de l'AABB locale du mesh et de l'échelle de l'entité) avec les 6 plans du frustum de la caméra.
    • Remplissage du Buffer Indirect : le pass cull lit le niveau LOD du slot, en choisit la ligne dans la table LOD du mesh (LodTable : 4 lignes d'offsets/comptes en unités d'élément) et écrit les arguments dans le DrawSlot (80 o) — mis à 0 si l'objet est cullé ou inactif (no-op). Le niveau 0 porte les comptes du mesh complet, donc LOD désactivé ≡ pré-LOD bit-à-bit.
  • Pass de Rendu (Render Pass) :
    • Le CPU émet un draw indirect par slot (écart D1 — la spécification initiale prévoyait une commande unique fusionnée) ; les slots à compte 0 (cullés/inactifs/vides) sont des no-ops.
    • Le GPU pioche directement dans le buffer préparé par le compute pass et dessine uniquement les objets visibles, sans intervention du CPU.
  1. 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.
  1. Synthèse des Structures de Données en VRAM

L'implémentation utilise les buffers wGPU suivants (tous créés par le Renderer à l'initialisation, capacité fixe de 256 slots) :

Buffer Rôle Type wGPU Direction du flux
Transform Buffer Positions/rotations/échelles brutes + flags par entité (64 o/slot) Storage Buffer CPU → GPU (chaque frame, write_buffer)
Matrix Buffer World Matrices finales calculées (256 o/slot, padded — D12) Storage + Uniform Buffer GPU (Calculé) → GPU (Lu par le Render)
Bounding Box Buffer Coins min/max de l'AABB de chaque mesh (32 o/slot) Storage Buffer CPU → GPU (quand l'ensemble des meshes change)
Indirect Draw Buffer Comptes de draw par slot (80 o/slot, zéro = no-op) Indirect + Storage Buffer GPU (Rempli par Compute) → GPU (Lu par le Render)
CullUniforms 6 plans du frustum + num_slots + culling (112 o) Uniform Buffer CPU → GPU (chaque frame) — réservé au compute (group 2)
Lod Levels Niveau LOD par slot choisi par le CPU (4 o/slot) Storage Buffer (read-only) CPU → GPU (chaque frame)
Lod Tables Table par mesh : count + 4 lignes de 16 o (offset/compte d'éléments) (80 o/mesh) Storage Buffer (read-only) CPU → GPU (quand l'ensemble des meshes change)

Buffers de géométrie LOD (Étape 19) : les niveaux d'un mesh sont empilés — un seul buffer vertex et un seul buffer index par mesh, contenant les niveaux concaténés (L0, L1, …). Les lignes de la table LOD portent les offsets en unités d'élément (premier vertex / premier index), car les arguments drawIndirect* de WebGPU s'expriment en éléments, et le buffer est lié en entier à l'offset 0. Conséquences : un mesh LOD ne peux dépasser 65 535 sommets au total (indices u16) et 4 niveaux (MAX_LOD_LEVELS) ; le mélange indexé/non-indexé dans un même mesh est supporté (la commande de draw par slot suit le niveau choisi par le CPU).

Liens