299 lines
9.5 KiB
Markdown
299 lines
9.5 KiB
Markdown
# É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)`)
|