Files
wsg/docs/DRAFT.md
T
Jérôme Bousquié 9614156848 MSAA
2026-09-25 11:20:20 +02:00

299 lines
9.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Étape 24 — MSAA 4× (Anti-aliasing multi-échantillons)
**Statut** : ⬜ En cours
**Roadmap** : 6.4
**Prérequis** : Pipeline HDR (étape 20) + Bloom (étape 23)
---
## Problème
Sans anti-aliasing, les bords des géométries présentent du **staircasing** (aliasing) :
les silhouettes ont des escaliers visibles, surtout sur les contours fins et les
lointains. C'est le défaut visuel le plus flagrant d'un moteur sans post-process.
## Solution
Rendre la scène avec **N échantillons par pixel** (4× par défaut), puis **résoudre**
(en moyenne) vers une texture single-sample. Le reste du pipeline (bloom, TM)
opère sur la texture résolue — aucun changement.
---
## Pipeline actuel vs avec MSAA
### Sans HDR, sans MSAA (actuel)
```
Scene → swapchain (Rgba8UnormSrgb) → present
```
### Sans HDR, avec MSAA
```
Scene → MSAA texture (4×, format surface) ──resolve──→ swapchain → present
+ MSAA depth (4×)
```
Le resolve est fait **automatiquement par wgpu** dans le render pass
(`resolve_target` sur la color attachment).
### Avec HDR, sans MSAA (actuel)
```
Scene → HDR texture (Rgba16Float) → [Bloom] → TM → swapchain → present
```
### Avec HDR, avec MSAA (nouveau)
```
Scene → MSAA HDR (4×, Rgba16Float) ──resolve──→ HDR texture (Rgba16Float)
+ MSAA depth (4×) → [Bloom] → TM → swapchain → present
```
**Principe** : MSAA s'insère **uniquement** entre le rasterizer et le premier
consommateur de la texture de scène. Les post-processes (bloom, TM) voient
toujours une texture single-sample.
---
## Décisions de design
| # | Décision | Rationale |
|---|----------|-----------|
| D1 | `AppBuilder::with_msaa(count)` — opt-in, zero cost désactivé | Principe WSG : chaque effet est optionnel |
| D2 | `sample_count` configurable (2, 4, 8) — default 4 | 4× est le bon rapport qualité/coût ; 8× pour du "max" |
| D3 | Texture MSAA **offscreen** (jamais le swapchain en MSAA direct) | Uniformité : même code path que HDR, resize plus simple |
| D4 | Depth buffer recréé en MSAA quand actif | Le depth doit avoir le même `sample_count` que le color |
| D5 | Resolve via `RenderPassColorAttachment::resolve_target` | Natif wgpu, pas de shader supplémentaire |
| D6 | **Aucun nouveau shader** | MSAA est une feature rasterizer, pas un post-process |
| D7 | Format MSAA = format de la cible (Rgba16Float si HDR, surface format sinon) | Le resolve produit la même texture qu'avant |
| D8 | Resize recrée les textures MSAA + depth | Même pattern que HDR resize |
| D9 | Bloom/TM inchangés — ils lisent la texture résolue (single-sample) | Zéro impact sur les passes post |
| D10 | `MsaaConfig { sample_count: u32 }` — public, re-exporté | API minimale, extensible |
---
## API utilisateur
```rust
use wsg_lib::prelude::*;
let app = AppBuilder::new()
.title("MSAA Demo")
.with_hdr(ToneMapper::Aces)
.with_msaa(4) // ← 4 échantillons (2, 4, ou 8)
.with_bloom(BloomConfig::default())
.build()
.await?;
```
Sans `.with_msaa(...)` → comportement identique à aujourd'hui (0 échantillon overhead).
---
## Implémentation
### 24.1 — `MsaaConfig` (public)
Fichier : `lib/src/core/msaa.rs`
```rust
/// Configuration MSAA. `sample_count` doit être 2, 4, ou 8
/// (valeur supportée par le GPU — vérifiée à l'init).
#[derive(Clone, Copy, Debug)]
pub struct MsaaConfig {
pub sample_count: u32,
}
impl Default for MsaaCapable {
fn default() -> Self {
Self { sample_count: 4 }
}
}
```
### 24.2 — Champs `Renderer`
Ajouter à `Renderer` :
```rust
msaa_config: MsaaConfig, // toujours présent (sample_count=1 si désactivé)
msaa_color_texture: Option<wgpu::Texture>, // Some si msaa active
msaa_color_view: Option<wgpu::TextureView>,
msaa_depth_texture: Option<wgpu::Texture>,
msaa_depth_view: Option<wgpu::TextureView>,
```
Quand `msaa_config.sample_count > 1` :
- `msaa_color_texture` = texture `sample_count=N`, format = HDR ou surface
- `msaa_depth_texture` = texture depth `sample_count=N`
- Le render pass scène utilise ces views + `resolve_target`
Quand `sample_count == 1` :
- Tous les `Option` sont `None`
- Le render pass utilise la texture/view existante (comportement actuel)
### 24.3 — Allocation à l'init (`Renderer::new`)
```rust
let sample_count = msaa_config.map(|c| c.sample_count).unwrap_or(1);
// Vérifier que le format supporte ce sample_count
let formats = device.limits(); // ou surface.capabilities()
// Pour le surface : surface.capabilities().formats
// Pour l'offscreen : device.limits().max_color_attachments, etc.
// En pratique : Rgba16Float et Rgba8Unorm supportent 4× partout.
if sample_count > 1 {
let msaa_tex = device.create_texture(&TextureDescriptor {
size: Extent3d { width, height, depth_or_array_layers: 1 },
sample_count,
dimension: Dimension::D2,
format: target_format, // Rgba16Float ou surface format
usage: TextureUsages::RENDER_ATTACHMENT,
..
});
// + depth MSAA
}
```
### 24.4 — Render pass scène (modification `render_scene`)
Le render pass principal doit utiliser les views MSAA quand actif :
```rust
// Déterminer la color attachment
let (color_view, resolve_target) = if let Some(msaa_view) = &self.msaa_color_view {
// MSAA actif : render dans MSAA, resolve vers la texture single
let resolve = if self.hdr.is_some() {
Some(self.hdr.as_ref().unwrap().view.clone()) // resolve → HDR tex
} else {
Some(view.clone()) // resolve → swapchain
};
(msaa_view.clone(), resolve)
} else {
// Pas de MSAA : comportement actuel
let color_view = if let Some(hdr) = &self.hdr {
hdr.view.clone()
} else {
view.clone()
};
(color_view, None)
};
// Depth : MSAA ou single
let depth_view = if let Some(msaa_depth) = &self.msaa_depth_view {
msaa_depth.clone()
} else {
self.depth_view.clone()
};
encoder.render_pass(RenderPassDescriptor {
color_attachments: &[Some(RenderPassColorAttachment {
view: &color_view,
resolve_target: resolve_target.as_ref(),
load_op: LoadOp::Clear,
store_op: StoreOp::Store, // Store même en MSAA (wgpu gère le resolve)
})],
depth_stencil_attachment: Some(RenderPassDepthStencilAttachment {
view: &depth_view,
..
}),
..
});
```
### 24.5 — Resize
```rust
fn resize(&mut self, device, width, height) {
// ... resize depth existant ...
if self.msaa_config.sample_count > 1 {
// Recréer MSAA color + depth
self.msaa_color_texture = Some(create_msaa_texture(...));
self.msaa_color_view = Some(...);
self.msaa_depth_texture = Some(create_msaa_depth(...));
self.msaa_depth_view = Some(...);
}
// HDR resize existant
// Bloom resize existant
}
```
### 24.6 — Plomberie App/AppBuilder
```rust
// AppBuilder
pub fn with_msaa(mut self, sample_count: u32) -> Self {
assert!((2..=8).contains(&sample_count) && sample_count.is_power_of_two(),
"sample_count must be 2, 4, or 8");
self.msaa_config = Some(MsaaConfig { sample_count });
self
}
// App
pub fn msaa_enabled(&self) -> bool { ... }
pub fn set_msaa(&mut self, sample_count: u32) { ... } // nécessite resize
```
Plomberie : `AppBuilder → App → AppRunner → Renderer::new(msaa_config)`
### 24.7 — Re-exports
`lib.rs` + `prelude.rs` : `pub use crate::core::MsaaConfig;`
### 24.8 — Exemple `msaa.rs`
Scène simple (cube + sphere + ground) avec/without MSAA commutable à la runtime
(clavier `M`). Camera orbitale pour voir les bords de près.
Contrôles :
- `M` — toggle MSAA (nécessite un resize/recréation des textures)
- `R`/`1`/`2`/`3` — presets caméra
- Drag/wheel — orbit/zoom
### 24.9 — Documentation
- `docs/user/msaa.md` : activation, config, coûts, limitations
- `docs/user/README.md` : section "Anti-aliasing"
- `lib/examples/README.md` : entrée `msaa`
- `docs/ROADMAP.md` : 6.4 → ✅
---
## Coût GPU
| Config | Coût relatif |
|--------|:---:|
| Sans MSAA | 1× |
| MSAA 4× | ~1.3–1.5× (le rasterizer sur-échantillonne, le fill rate est partagé) |
| MSAA 8× | ~1.5–2× |
MSAA est **beaucoup** moins coûteux qu'un post-process AA (FXAA, TAA) car le
sur-coût est dans le rasterizer (edges only) et pas dans un blur fullscreen.
Les post-processes (bloom, TM) ne sont **pas** affectés — ils tournent sur la
texture résolue (1 échantillon/pixel).
## Limitations / non-goals
- **Pas de TAA** (temporal AA) — nécessiterait un history buffer + motion vectors,
bien plus complexe. MSAA 4× couvre 90% du besoin pour un lib "simple".
- **Pas de MSAA sur les post-processes** — le bloom/TM lissent déjà l'image.
- **Pas de coverage sampling** (DX12-only) — WebGPU expose seulement MSAA.
- **Resize = recréation** des textures MSAA (pas de resize in-place).
## Tests
- [ ] `MsaaConfig` Default = 4
- [ ] Validation `sample_count` (rejette 3, 5, 16)
- [ ] Build avec `with_msaa(4)` + HDR + Bloom → compile
- [ ] Exemple `msaa.rs` compile
- [ ] WGSL validation inchangé (pas de nouveau shader)
- [ ] Test unit : `Renderer::new` avec `msaa_config=Some(4)` ne panique pas
(nécessite un device mock — peut être un test intégration ignoré)
## Critères d'acceptation
1. `cargo check` 0 errors, 0 warnings
2. `cargo test` tous verts
3. `cargo run -p wsg-lib --example msaa` → bords lisses avec M, escaliers sans M
4. `cargo run -p wsg-lib --example demo` → inchangé (pas de `with_msaa`)
5. Combinaison HDR + Bloom + MSAA fonctionne (demo avec `.with_msaa(4)`)