particules: integrate decisions D15-D19 in ARCHI, rewrite DRAFT Step A
ARCHI_PARTICULES.md: - D15 uv_rect, D16 single fixed layout, D17 compaction+indirect draw - D18 color_range + uv_rects + alpha_scale (spawn randomization, EmitterParams 48 B) - D19 layout audit: 80 B without padding (storage space, vec3/vec4 align 4), single shared Particle struct (no ParticleAlive prefix), compact_index 1 u32 per slot via storage binding, indirect_args 16 B - apply to sections 2/3/4/5/6/7/8/9/10/14/15/17 + new section 18 DRAFT.md (Step A) full rewrite against final decisions: - Particle 80 B no-padding, 4 pool buffers (data, compact_index, indirect_args, camera_params), 5-binding layout, empty vertex layout, draw_indirect, no early-out, SceneGpu gains queue+sample_count
This commit is contained in:
+111
-70
@@ -15,7 +15,7 @@
|
||||
│ ┌───────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ PARTICLE POOL (ressource GPU, créée une fois) │ │
|
||||
│ │ │ │
|
||||
│ │ • Buffer storage (N × 64 bytes) │ │
|
||||
│ │ • Buffer storage (N × 80 bytes [D15/D19]) │ │
|
||||
│ │ • Pipeline render (billboard instancé) │ │
|
||||
│ │ • Texture + Sampler + Blending mode │ │
|
||||
│ │ • Draw call (instanced, 1 par frame) │ │
|
||||
@@ -52,27 +52,25 @@
|
||||
|
||||
---
|
||||
|
||||
## 2. État par particule (80 bytes avec `uv_rect` [D15])
|
||||
## 2. État par particule (80 bytes : `uv_rect` [D15], sans padding [D19])
|
||||
|
||||
```rust
|
||||
/// Miroir du struct WGSL `Particle`.
|
||||
/// 80 bytes avec `uv_rect` [D15], `#[repr(C)]`, `Pod + Zeroable`.
|
||||
/// Miroir du struct WGSL `Particle` (§8.1/§8.2 — un seul struct partagé compute + render, [D19]).
|
||||
/// 80 bytes avec `uv_rect` [D15] : espace **storage** → vec3/vec4 align 4 → **pas de padding** [D19].
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Pod, Zeroable)]
|
||||
pub struct Particle {
|
||||
pub pos: [f32; 3], // offset 0 — position monde (xyz)
|
||||
pub _pad0: f32, // offset 12
|
||||
pub vel: [f32; 3], // offset 16 — vélocité (xyz)
|
||||
pub _pad1: f32, // offset 28
|
||||
pub life: f32, // offset 32 — vie restante (seconds)
|
||||
pub max_life: f32, // offset 36 — vie initiale (pour fade normalisé)
|
||||
pub size: f32, // offset 40 — taille courante (world units)
|
||||
pub size_growth: f32, // offset 44 — croissance par seconde (+ = grandir, - = rétrécir)
|
||||
pub angle: f32, // offset 48 — rotation 2D courante (radians)
|
||||
pub angular_vel: f32, // offset 52 — vitesse angulaire (rad/s)
|
||||
pub color: [f32; 4], // offset 56 — RGBA (alpha modulée par le driver)
|
||||
pub vel: [f32; 3], // offset 12 — vélocité (xyz)
|
||||
pub life: f32, // offset 24 — vie restante (seconds)
|
||||
pub max_life: f32, // offset 28 — vie initiale (pour fade normalisé)
|
||||
pub size: f32, // offset 32 — taille courante (world units)
|
||||
pub size_growth: f32, // offset 36 — croissance par seconde (+ = grandir, - = rétrécir)
|
||||
pub angle: f32, // offset 40 — rotation 2D courante (radians)
|
||||
pub angular_vel: f32, // offset 44 — vitesse angulaire (rad/s)
|
||||
pub color: [f32; 4], // offset 48 — RGBA (alpha : `alpha_scale` [D18])
|
||||
pub uv_rect: [f32; 4], // offset 64 — zone UV (ox, oy, sx, sy) [D15]
|
||||
// Total : 80 bytes
|
||||
// Total : 80 bytes (layout identique Rust = WGSL storage, align 4) [D19]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -141,18 +139,24 @@ pub enum BlendingMode {
|
||||
|
||||
```rust
|
||||
pub struct ParticlePool {
|
||||
/// Buffer storage (N × 64 bytes). COPY_DST | STORAGE.
|
||||
/// Buffer storage (N × 80 B [D15/D19]). COPY_DST | STORAGE.
|
||||
pub(crate) buffer: wgpu::Buffer,
|
||||
/// Pipeline render (billboard instancé).
|
||||
pub(crate) pipeline: wgpu::RenderPipeline,
|
||||
/// Pipeline layout du render.
|
||||
pub(crate) layout: wgpu::BindGroupLayout,
|
||||
/// Bind group (buffer + texture + sampler + camera uniform ref).
|
||||
/// Bind group (particles + compact_index + camera_params + texture + sampler — tout est
|
||||
/// possédé par le pool, [D17/D19]).
|
||||
pub(crate) bind_group: wgpu::BindGroup,
|
||||
/// Sampler (Linear, ClampToEdge).
|
||||
pub(crate) sampler: wgpu::Sampler,
|
||||
/// Buffer uniform 4 bytes : alive_count (écrit par le driver, lu par le vertex shader).
|
||||
pub(crate) count_buffer: wgpu::Buffer,
|
||||
/// Index compact : 1 u32 par slot [D17/D19], lu en VS par storage (binding 2).
|
||||
/// STORAGE | COPY_DST.
|
||||
pub(crate) compact_index: wgpu::Buffer,
|
||||
/// Args indirect draw (16 B : 4 × u32, `drawIndirect`) [D17/D19]. STORAGE | COPY_DST.
|
||||
pub(crate) indirect_args: wgpu::Buffer,
|
||||
/// Camera params (128 B : view + proj), possédée par le pool, écrite par le renderer chaque frame.
|
||||
pub(crate) camera_params: wgpu::Buffer,
|
||||
/// Taille max du pool.
|
||||
pub max_count: u32,
|
||||
/// Driver actuellement attaché (None = pool inactif).
|
||||
@@ -212,11 +216,11 @@ Frame loop (driver GPU) :
|
||||
c. Trouver n_spawn slots morts dans le pool
|
||||
d. Pour chaque slot : tirer pos/vel/life/size/angle/color (random CPU)
|
||||
e. queue.write_buffer(&pool.buffer, offset_morts, new_particles)
|
||||
f. Écrire alive_count estimé dans count_buffer
|
||||
f. [D17] rien ici — les args indirect sortent de la compaction (fused dans le compute, §18)
|
||||
2. Compute dispatch (le pool fait le dispatch) :
|
||||
- workgroups = ceil(max_count / 64)
|
||||
- Le shader intègre TOUS les slots alive
|
||||
- Le shader écrit alive_count exact dans count_buffer (atomic ou 2e pass)
|
||||
- Le shader écrit `compact_index` + `indirect_args` (compaction fused, [D17])
|
||||
3. post_compute : (optionnel, ex: sync alive_count)
|
||||
4. Draw (si alive_count > 0)
|
||||
```
|
||||
@@ -245,8 +249,15 @@ pub struct GpuEmitterConfig {
|
||||
pub size_growth: f32,
|
||||
/// Plage de vitesse angulaire initiale (rad/s).
|
||||
pub angular_vel_range: (f32, f32),
|
||||
/// Couleur de base RGBA (alpha initiale = 1.0, modulée par life/max_life).
|
||||
pub color: [f32; 4],
|
||||
/// Couleur de base : [min, max] par canal **RGB** (le canal alpha est ignoré → `alpha_scale`).
|
||||
/// Chaque canal est tiré uniformément dans [min, max] au spawn [D18].
|
||||
pub color_range: ([f32; 4], [f32; 4]),
|
||||
/// Palette de zones UV : chaque spawn tire un rect uniformément au hasard [D15/D18].
|
||||
/// `uv_rects_count = 1` = toute la texture (comportement d'avant D15).
|
||||
pub uv_rects: [[f32; 4]; UV_RECTS_MAX], // UV_RECTS_MAX = 8
|
||||
pub uv_rects_count: u8, // ≤ UV_RECTS_MAX
|
||||
/// Alpha cible (0..1, défaut 1.0) : l'intégration écrit `color.a = alpha_scale × life/max_life` [D18].
|
||||
pub alpha_scale: f32,
|
||||
}
|
||||
```
|
||||
|
||||
@@ -268,13 +279,13 @@ Frame loop (driver CPU) :
|
||||
c. Accumulateur de spawn : acc += rate * dt
|
||||
d. Pour chaque nouveau spawn : écrire dans un slot mort
|
||||
e. queue.write_buffer(&pool.buffer, 0, &all_alive_particles)
|
||||
f. Écrire alive_count dans count_buffer
|
||||
f. [D17] Construire `compact_index` + `indirect_args` au CPU (compaction exacte)
|
||||
2. Pas de compute dispatch
|
||||
3. post_compute : (rien)
|
||||
4. Draw (si alive_count > 0)
|
||||
```
|
||||
|
||||
**Config** : identique à `GpuEmitterConfig` (mêmes paramètres de simulation).
|
||||
**Config** : identique à `GpuEmitterConfig` (mêmes paramètres de simulation, y compris `color_range`/`uv_rects`/`alpha_scale` [D18] — les tirages se font en Rust au spawn).
|
||||
La différence est **où** l'intégration se fait.
|
||||
|
||||
**Avantage du CPU** : possibilité d'interactions (collision avec objets de la scène,
|
||||
@@ -389,16 +400,18 @@ pub base_dir: [f32; 3], // utilisé par Fixed et Cone, ignoré par Sphere et Bo
|
||||
|
||||
### 5.4 Table des presets
|
||||
|
||||
| Preset | Shape | Velocity | base_dir | speed | gravity | drag | life | size | growth | ang_vel | color | blending |
|
||||
|--------|-------|----------|----------|-------|---------|------|------|------|--------|---------|-------|----------|
|
||||
| **Explosion** | Point | Sphere | — | 2-8 | (0,-9.8,0) | 0.1 | 0.5-1.5 | 0.05-0.15 | -0.05 | ±5 | [1,0.8,0.3,1] | Additive |
|
||||
| **Jet d'eau** | Point | Cone 10° | (0,1,0) | 3-5 | (0,-9.8,0) | 0.01 | 1.0-2.0 | 0.03-0.05 | 0 | 0 | [0.3,0.6,1,0.8] | Alpha |
|
||||
| **Pluie** | Plan (0,1,0) 20×20 | Box | — | vy:-5, vx/z:±0.3 | (0,0,0) | 0 | 3.0-5.0 | 0.01-0.02 | 0 | 0 | [1,1,1,0.3] | Alpha |
|
||||
| **Fumée** | Box [0.15,0,0.15] | Cone 45° | (0,1,0) | 0.3-1.0 | (0,0.3,0) | 0.3 | 2.0-4.0 | 0.2-0.4 | +0.3 | ±2 | [0.5,0.5,0.5,0.4] | Alpha |
|
||||
| **Feu** | Point | Cone 20° | (0,1,0) | 1-3 | (0,0.5,0) | 0.2 | 0.3-0.8 | 0.15-0.3 | -0.2 | ±8 | [1,0.5,0,1] | Additive |
|
||||
| **Neige** | Plan (0,1,0) 10×10 | Box | — | vy:-0.5, vx/z:±0.2 | (0,0,0) | 0 | 5-10 | 0.02-0.04 | 0 | ±3 | [1,1,1,0.8] | Alpha |
|
||||
| **Sparkles** | Sphere r=0.3 | Sphere | — | 0.1-0.5 | (0,0,0) | 0.5 | 1-3 | 0.02-0.05 | -0.01 | 0 | [1,1,0.8,1] | Additive |
|
||||
| **Débris** | Point | Sphere | — | 1-6 | (0,-9.8,0) | 0.02 | 1-3 | 0.03-0.08 | 0 | ±10 | [0.6,0.4,0.2,1] | Alpha |
|
||||
| Preset | Shape | Velocity | base_dir | speed | gravity | drag | life | size | growth | ang_vel | color_range (RGB min→max) [D18] | α_scale [D18] | blending |
|
||||
|--------|-------|----------|----------|-------|---------|------|------|------|--------|---------|---------------------------|---------|----------|
|
||||
| **Explosion** | Point | Sphere | — | 2-8 | (0,-9.8,0) | 0.1 | 0.5-1.5 | 0.05-0.15 | -0.05 | ±5 | [0.8,0.3,0]→[1,0.8,0.3] | 1.0 | Additive |
|
||||
| **Jet d'eau** | Point | Cone 10° | (0,1,0) | 3-5 | (0,-9.8,0) | 0.01 | 1.0-2.0 | 0.03-0.05 | 0 | 0 | [0.3,0.5,0.8]→[0.5,0.8,1] | 0.8 | Alpha |
|
||||
| **Pluie** | Plan (0,1,0) 20×20 | Box | — | vy:-5, vx/z:±0.3 | (0,0,0) | 0 | 3.0-5.0 | 0.01-0.02 | 0 | 0 | [0.7,0.8,1]→[1,1,1] | 0.3 | Alpha |
|
||||
| **Fumée** | Box [0.15,0,0.15] | Cone 45° | (0,1,0) | 0.3-1.0 | (0,0.3,0) | 0.3 | 2.0-4.0 | 0.2-0.4 | +0.3 | ±2 | [0.3,0.3,0.3]→[0.7,0.7,0.7] | 0.4 | Alpha |
|
||||
| **Feu** | Point | Cone 20° | (0,1,0) | 1-3 | (0,0.5,0) | 0.2 | 0.3-0.8 | 0.15-0.3 | -0.2 | ±8 | [1,0.3,0]→[1,0.9,0.2] | 1.0 | Additive |
|
||||
| **Neige** | Plan (0,1,0) 10×10 | Box | — | vy:-0.5, vx/z:±0.2 | (0,0,0) | 0 | 5-10 | 0.02-0.04 | 0 | ±3 | [0.9,0.9,1]→[1,1,1] | 0.8 | Alpha |
|
||||
| **Sparkles** | Sphere r=0.3 | Sphere | — | 0.1-0.5 | (0,0,0) | 0.5 | 1-3 | 0.02-0.05 | -0.01 | 0 | [1,0.8,0.4]→[1,1,1] | 1.0 | Additive |
|
||||
| **Débris** | Point | Sphere | — | 1-6 | (0,-9.8,0) | 0.02 | 1-3 | 0.03-0.08 | 0 | ±10 | [0.4,0.3,0.15]→[0.8,0.6,0.3] | 1.0 | Alpha |
|
||||
|
||||
Par défaut `uv_rects = [(0,0,1,1)]` (toute la texture) [D15]. Exemple d'atlas : `Neige` avec 3 flocons → `uv_rects: [(0,0,⅓,1), (⅓,0,⅓,1), (⅔,0,⅓,1)]` — chaque spawn tire un flocon [D18].
|
||||
|
||||
Disponibles comme constructors : `GpuEmitterConfig::fire()`, `::smoke()`, `::rain()`, etc.
|
||||
|
||||
@@ -408,10 +421,11 @@ Disponibles comme constructors : `GpuEmitterConfig::fire()`, `::smoke()`, `::rai
|
||||
|
||||
| Buffer | Type | Taille | Écrit par | Lu par |
|
||||
|--------|------|--------|-----------|--------|
|
||||
| `particle_data` | Storage (RW) | N × 64 B | Driver (spawn/update) | Compute + Render |
|
||||
| `count_buffer` | Uniform (RW) | 4 B | Driver / Compute | Vertex shader (instance_count) |
|
||||
| `particle_data` | Storage (RW) | N × 80 B [D15/D19] | Driver (spawn/update) | Compute + Render |
|
||||
| `compact_index` | Storage (RO) | N × u32 [D17/D19] | Compute (compaction) / Driver | Vertex shader (slot via `compact_index[ii]`) |
|
||||
| `indirect_args` | Storage (RO) | 16 B [D17/D19] | Compute (compaction) / Driver | `drawIndirect` |
|
||||
| `emitter_params` | Uniform (RO) | 64 B | Driver GPU (par frame) | Compute shader |
|
||||
| `camera_params` | Uniform (RO) | 160 B | Renderer (existant) | Vertex shader (view, proj) |
|
||||
| `camera_params` | Uniform (RO) | 128 B (view + proj) | Renderer (par frame) | Vertex shader |
|
||||
|
||||
### `emitter_params` (uniform, 64 bytes)
|
||||
|
||||
@@ -422,9 +436,10 @@ struct EmitterParams {
|
||||
drag: f32, // friction
|
||||
alive_count: f32, // count courant (écrit par le compute via atomic)
|
||||
pool_size: f32, // taille max du pool
|
||||
alpha_scale: f32, // [D18] alpha cible (modulée par life/max_life dans le compute)
|
||||
_pad: vec2<f32>, // alignment
|
||||
}
|
||||
// Total : 4+12+4+4+4+8 = 36 → pad à 48 (align 16)
|
||||
// Total : 48 B (espace uniform, vec3 align 16) [D18]
|
||||
```
|
||||
|
||||
### Bind groups
|
||||
@@ -440,14 +455,14 @@ struct EmitterParams {
|
||||
|
||||
| Group | Binding | Type | Contenu | Visibility |
|
||||
|-------|---------|------|---------|------------|
|
||||
| 0 | 0 | Uniform RO | `camera_params` (view + proj) | VERTEX |
|
||||
| 0 | 0 | Uniform RO | `camera_params` (view + proj, 128 B) | VERTEX |
|
||||
| 0 | 1 | Storage RO | `particle_data` | VERTEX |
|
||||
| 0 | 2 | Uniform RO | `count_buffer` (alive_count) | VERTEX |
|
||||
| 0 | 2 | Storage RO | `compact_index` [D17] | VERTEX |
|
||||
| 0 | 3 | Sampler | Sampler | FRAGMENT |
|
||||
| 0 | 4 | Texture | Texture particule | FRAGMENT |
|
||||
|
||||
> **Note** : le `count_buffer` est lu par le vertex shader pour déterminer
|
||||
> `instance_count` (via `@builtin(instance_index)` et un early-out si `ii >= count`).
|
||||
> **Note [D17/D19]** : le slot de l'instance arrive par **storage** (`compact_index[ii]`), pas par
|
||||
> attribut vertex (layout vide, pattern TM). Le count exact vient des args indirect — plus d'early-out.
|
||||
|
||||
---
|
||||
|
||||
@@ -471,21 +486,21 @@ Dispatch : `workgroups = ceil(max_count / 64)`, un workgroup de 64 threads.
|
||||
```rust
|
||||
wgpu::RenderPipeline {
|
||||
layout: RenderPipelineLayout {
|
||||
bind_group_layouts: [bgl_render], // camera + storage RO + count + sampler + texture
|
||||
bind_group_layouts: [bgl_render], // camera + particles + compact_index + sampler + texture (5 bindings) [D17]
|
||||
},
|
||||
vertex: vs_main (billboard),
|
||||
fragment: fs_main (texture × color),
|
||||
primitive: TriangleList,
|
||||
vertex_buffer_layouts: [], // PAS de vertex buffer ! (quad généré en shader)
|
||||
vertex_buffer_layouts: [], // layout VIDE — quad via QUAD[vi], slot via storage binding 2 [D17/D19]
|
||||
multisample: sample_count du contexte,
|
||||
color_states: [blending mode du pool],
|
||||
depth_stencil: Some(LessEqual, ALWAYS), // depth test oui, depth write non (transparence)
|
||||
}
|
||||
```
|
||||
|
||||
> **Pas de vertex buffer** : le quad billboard est généré dans le vertex shader
|
||||
> via `@builtin(vertex_index)` (4 vertices) — même pattern que le fullscreen triangle
|
||||
> du tone mapping, mais avec 4 sommets au lieu de 3.
|
||||
> **Vertex layout vide** (comme le fullscreen du tone mapping) : le quad est généré en shader
|
||||
> (`QUAD[vi]`), et le slot de l'instance arrive par **storage** (`compact_index[ii]`) — pas
|
||||
> d'attribut vertex [D17/D19].
|
||||
|
||||
### 7.3 Blend states
|
||||
|
||||
@@ -512,11 +527,9 @@ Le blending est **figé au build du pipeline** (2 pipelines par pool si on veut
|
||||
// particle_update.wgsl
|
||||
// Compute pass : intègre toutes les particules alive.
|
||||
|
||||
struct Particle {
|
||||
struct Particle { // 80 B — espace storage, vec3/vec4 align 4, pas de padding [D19]
|
||||
pos: vec3<f32>,
|
||||
pad0: f32,
|
||||
vel: vec3<f32>,
|
||||
pad1: f32,
|
||||
life: f32,
|
||||
max_life: f32,
|
||||
size: f32,
|
||||
@@ -527,13 +540,14 @@ struct Particle {
|
||||
uv_rect: vec4<f32>, // [D15]
|
||||
}
|
||||
|
||||
struct EmitterParams {
|
||||
struct EmitterParams { // espace uniform : vec3 align 16 → padding conservé [D19]
|
||||
dt: f32,
|
||||
gravity: vec3<f32>,
|
||||
drag: f32,
|
||||
alive_count: f32,
|
||||
pool_size: f32,
|
||||
pad: vec2<f32>,
|
||||
alpha_scale: f32, // [D18]
|
||||
_pad: vec2<f32>, // → total 48 B
|
||||
}
|
||||
|
||||
@group(0) @binding(0) var<storage, read_write> particles: array<Particle>;
|
||||
@@ -563,8 +577,8 @@ fn cs_update() {
|
||||
// Rotation
|
||||
p.angle += p.angular_vel * params.dt;
|
||||
|
||||
// Fade out
|
||||
p.color.a = clamp(p.life / p.max_life, 0.0, 1.0);
|
||||
// Fade out [D18]
|
||||
p.color.a = params.alpha_scale * clamp(p.life / p.max_life, 0.0, 1.0);
|
||||
|
||||
// Kill
|
||||
if p.life <= 0.0 {
|
||||
@@ -587,9 +601,9 @@ fn cs_update() {
|
||||
```wgsl
|
||||
// particle_billboard.wgsl (vertex)
|
||||
|
||||
struct Particle {
|
||||
pos: vec3<f32>, pad0: f32,
|
||||
vel: vec3<f32>, pad1: f32,
|
||||
struct Particle { // 80 B — espace storage, vec3/vec4 align 4, pas de padding [D19]
|
||||
pos: vec3<f32>,
|
||||
vel: vec3<f32>,
|
||||
life: f32, max_life: f32,
|
||||
size: f32, size_growth: f32,
|
||||
angle: f32, angular_vel: f32,
|
||||
@@ -712,7 +726,7 @@ App::render_scene(frame) :
|
||||
│ │ d. Si driver.needs_draw() :
|
||||
│ │ → render_pass.set_pipeline(pool.pipeline)
|
||||
│ │ → render_pass.set_bind_group(0, pool.bind_group)
|
||||
│ │ → render_pass.draw(4, pool.max_count) // early-out en shader
|
||||
│ │ → render_pass.draw_indirect(pool.indirect_args, 0) // [D17] args écrits par le compute/driver
|
||||
│ │
|
||||
│ └─ (fin pools)
|
||||
│
|
||||
@@ -802,6 +816,7 @@ impl AppHandler for MyScene {
|
||||
size: 0.1, size_growth: 0.0,
|
||||
angle: t, angular_vel: 1.0,
|
||||
color: [1.0, 1.0, 1.0, 1.0],
|
||||
uv_rect: [0.0, 0.0, 1.0, 1.0], // [D15]
|
||||
..Particle::ZERO
|
||||
});
|
||||
}
|
||||
@@ -874,7 +889,7 @@ impl Scene {
|
||||
| Condition | Coût |
|
||||
|-----------|------|
|
||||
| Aucun pool créé | **Zéro**. Pas de buffer, pas de pipeline, pas de draw. |
|
||||
| Pool créé, pas de driver | Buffer alloué (N × 64 B). Pas de compute, pas d'upload, pas de draw. |
|
||||
| Pool créé, pas de driver | Buffer alloué (N × 80 B). Pas de compute, pas d'upload, pas de draw (args = 0 → no-op). |
|
||||
| Pool + driver, `active = false` | Idem ci-dessus. |
|
||||
| Pool + driver GPU actif | 1 compute dispatch + 1 draw. CPU : spawns seulement. |
|
||||
| Pool + driver CPU actif | 1 write_buffer + 1 draw. CPU : O(N) simulation. |
|
||||
@@ -959,9 +974,9 @@ Chaque étape est un DRAFT séparé, testable indépendamment.
|
||||
| D1 | **Pool ≠ Driver** (séparation stricte) | Flexibilité, swappability, zéro waste |
|
||||
| D2 | **3 drivers** : GPU, CPU, Manual | Gradient de contrôle |
|
||||
| D3 | **1 driver par pool** (v1) | Simplicité. Multi-drivers = V2. |
|
||||
| D4 | **80 bytes/particule** (avec `uv_rect`, [D15]) | Couvre pos/vel/life/size/angle/color/uv. Align 16. |
|
||||
| D4 | **80 bytes/particule** (avec `uv_rect` [D15] ; **confirmé sans padding** [D19]) | Couvre pos/vel/life/size/angle/color/uv. Storage align 4 — Rust = WGSL [D19]. |
|
||||
| D5 | **Billboard camera-facing** (pas world-facing) | Standard pour les VFX. Plus simple. |
|
||||
| D6 | **Quad via vertex_index** (⚠️ [D17] ajoute un vertex buffer d'index 1 u32/vertex ; le quad reste généré en shader) | Cohérent avec TM/bloom. |
|
||||
| D6 | **Quad via vertex_index** (vertex layout vide ; le slot arrive par storage `compact_index[ii]` [D17/D19]) | Cohérent avec TM/bloom. |
|
||||
| D7 | ~~**`draw(4, max_count)` + early-out**~~ → **supplanté par D17** (§18) | — |
|
||||
| D8 | **Spawn toujours CPU** (même driver GPU) | Le random + la décision "qui spawn" est CPU. Le GPU intègre. |
|
||||
| D9 | **Blend figé au pipeline** (1 mode par pool) | Pas de switch de pipeline par frame. |
|
||||
@@ -972,22 +987,24 @@ Chaque étape est un DRAFT séparé, testable indépendamment.
|
||||
| D14 | **`custom_force` (driver CPU)** | Le seul cas où CPU > GPU : interactions. |
|
||||
| D15 | **UV par particule** (`uv_rect` vec4 → struct 80 B) | WebGPU : pas d'indexage dynamique de textures → multi-motifs via UV. §18 |
|
||||
| D16 | **Layout complet figé, pas d'opt-in par attribut** | Gain nul, explosion de variants, coupling pool/émetteur. §18 |
|
||||
| D17 | **Compaction + indirect draw** (fused dans le compute) | Scale avec count actif, pas capacity. Count exact GPU. §18 |
|
||||
| D17 | **Compaction + indirect draw** (fused dans le compute ; détails corrigés par D19) | Scale avec count actif, pas capacity. Count exact GPU. §18 |
|
||||
| D18 | **Randomisation par particule au spawn** (`color_range` + `uv_rects` + `alpha_scale`) | Principe D16 : les plages vivent dans la config (coût GPU zéro), le résultat dans l'état. §18 |
|
||||
| D19 | **Audit layout/détails** : 80 B sans padding, un seul struct partagé, `compact_index` ×1 par storage, args 16 B | L'align uniform space du draft avait fuité en storage space. §18 |
|
||||
|
||||
---
|
||||
|
||||
## 18. Décisions de la session de design (à appliquer par DRAFT)
|
||||
|
||||
Ces 3 décisions tranchent les questions §15 (Q1, Q2) et supplantent D4/D6/D7.
|
||||
Ces 5 décisions tranchent les questions §15 (Q1, Q2), supplantent D4/D6/D7 et corrigent l'application initiale de D15/D17 (audit D19).
|
||||
Le reste du doc décrit la version initiale : appliquer les impacts listés ici.
|
||||
|
||||
### D15 — UV par particule (`uv_rect`)
|
||||
|
||||
- `uv_rect: vec4<f32>` (ox, oy, sx, sy en UV texture) ajouté à `Particle` → struct 64 → **80 B** (déjà appliqué dans §2, §8.1, §8.2).
|
||||
- `uv_rect: vec4<f32>` (ox, oy, sx, sy en UV texture) ajouté à `Particle` → struct 64 → **80 B** (appliqué dans §2, §8.1, §8.2 — D19 a corrigé l'application initiale : padding résiduel de l'espace uniform).
|
||||
- VS : `out.uv = uv_rect.xy + (q + 0.5) * uv_rect.zw` (au lieu de `q + 0.5`). Défaut (0,0,1,1) = comportement d'avant.
|
||||
- **Pourquoi** : WebGPU interdit l'indexage dynamique de textures en uniform (1 texture = 1 bind group = 1 pipeline). La variété de motifs ne peut venir QUE des UV → multi-motifs depuis UNE texture (atlas partagé entre presets).
|
||||
- Statique par vie (fixé au spawn). Animation d'atlas (uv = f(t)) = **v2** (cf §14).
|
||||
- Impact restant : §5.4 (`uv_rect` dans `GpuEmitterConfig`, défaut (0,0,1,1)), §10.2 (exemple manual : `uv_rect` à poser), §3.2/§6 (tailles × 1.25).
|
||||
- Impact : §2/§3.2/§6/§8.1/§8.2 appliqués ; §5.2/§5.4 appliqués par D18 (la zone unique devient la palette `uv_rects`) ; §10.2 appliqué.
|
||||
|
||||
### D16 — Layout COMPLET et figé, PAS d'opt-in par attribut
|
||||
|
||||
@@ -1001,11 +1018,35 @@ Le reste du doc décrit la version initiale : appliquer les impacts listés ici.
|
||||
|
||||
### D17 — Compaction + indirect draw (supplante D7, tranche Q1+Q2)
|
||||
|
||||
- Emulation d'instancing standard WebGPU : **index buffer** (`compact_index`, 1 u32 par vertex = 4 copies du slot par instance, vertex buffer layout 1 × u32 stride 4) + storage load en VS (`particles[idx]` au lieu de `particles[ii]`).
|
||||
- La **compaction est FUSIONNÉE dans le compute d'intégration** (~15 lignes WGSL, pas de 3e pass) : chaque slot alive (life > 0 après intégration) fait `atomicAdd(&compact_count)` et écrit son slot 4× dans `compact_index` ; le dernier workgroup (détection par atomic global vs nb de workgroups) écrit les args indirect.
|
||||
- Draw : **`drawIndirect`** (vertexCount = 4 × alive, instanceCount = alive) au lieu de `draw(4, max_count)` + early-out. Le VS n'a plus besoin du count → `count_buffer` remplacé par `indirect_args` (20 B : 5 × u32).
|
||||
- Emulation d'instancing standard WebGPU : buffer `compact_index` (**1 u32 par slot** [D19], pas 4 copies) lié en **storage** (binding 2) — le VS fait `let slot = compact_index[ii]` puis `particles[slot]`. Le vertex layout reste **vide** (quad généré en shader via `QUAD[vi]`, pattern TM).
|
||||
- La **compaction est FUSIONNÉE dans le compute d'intégration** (~15 lignes WGSL, pas de 3e pass) : chaque slot alive (life > 0 après intégration) fait `atomicAdd(&compact_count)` et écrit son slot **une fois** dans `compact_index` [D19] ; le dernier workgroup (détection par atomic global vs nb de workgroups) écrit les args indirect.
|
||||
- Draw : **`drawIndirect`** (vertexCount = 4, instanceCount = alive → 4 × alive vertices au total) au lieu de `draw(4, max_count)` + early-out. Le VS n'a plus besoin du count → `count_buffer` remplacé par `indirect_args` (**16 B : 4 × u32**, draw non-indexé [D19]).
|
||||
- **Pourquoi** : la compaction scale avec le count **actif** ; le template statique (early-out) taxe la **capacity** à CHAQUE frame — scène idle avec grand pool = 0.3–1 ms/frame de VS inutile. Coût compaction : pire cas ~0.3 ms sur iGPU faible (scène active).
|
||||
- Conséquence : le count est **exact et GPU** (sortie de la compaction) → tranche Q2 (plus de count CPU estimé).
|
||||
- Par driver : **GPU** = compaction fused (ci-dessus). **CPU** = sait exactement les slots alive → build lui-même l'index (4 copies) + args au CPU. **Manual** = `set_count` remplit l'index identité (l'utilisateur pack ses alive en tête de buffer) + args.
|
||||
- Non-régression : pool sans driver → args indirect = 0 → `drawIndirect` no-op (§12 inchangé).
|
||||
- Impact restant : §3.2/§6 (ajouter `compact_index` N × 4 × u32 + `indirect_args` 20 B), §7.2 (vertex buffer layout), §8.1 (compaction fused), §8.2 (supprimer early-out + `count_buf`, lire l'index), §9 (d = `drawIndirect`), §4.2/§4.3/§4.4 (chacun produit index + args).
|
||||
- Impact : §3.2/§6/§7.2/§9 appliqués (buffers + bind group + pipeline + frame loop, détails corrigés par D19) ; §8.1 (compaction fused) + §8.2 (supprimer early-out + `count_buf`, lire l'index) + §4.2/§4.3/§4.4 (chaque driver produit index + args) → à appliquer par le DRAFT B (les corps v0 restent, notes ci-dessus).
|
||||
|
||||
---
|
||||
|
||||
### D18 — Randomisation par particule au spawn : `color_range` + `uv_rects` + `alpha_scale`
|
||||
|
||||
Demande : « randomiser un peu la couleur, et randomiser un peu le rect UV ». Principe général (extension de D16) : **tout** champ de l'état 80 B randomisable au spawn a sa **plage** dans la config d'émetteur (coût GPU zéro — les tirages sont CPU au spawn [D8]) et son **résultat** dans l'état (par particule). Trois paramètres ajoutés aux configs d'émetteurs :
|
||||
|
||||
- `color_range: ([f32; 4], [f32; 4])` — [min, max] par canal **RGB** (le canal alpha est ignoré → `alpha_scale`). Spawn : `c = lerp(min, max, rand)` par canal.
|
||||
- `uv_rects: [[f32; 4]; UV_RECTS_MAX]` + `uv_rects_count: u8` (UV_RECTS_MAX = 8) — **palette** de zones UV (généralise la zone unique de D15). Spawn : tirage uniforme d'un rect. `count = 1` → défaut D15 (toute la texture, comportement inchangé).
|
||||
- `alpha_scale: f32` (0..1, défaut 1.0) — alpha cible, modulé par la vie normalisée. **Source unique de l'alpha** : l'intégration (compute GPU ou simulation CPU) réécrit à chaque frame `color.a = alpha_scale × clamp(life/max_life, 0, 1)` (remplace l'« alpha initiale » de `color`). Driver Manual : pas d'intégration → l'utilisateur écrit `color` (avec alpha) directement, `alpha_scale` est inopérant.
|
||||
|
||||
`EmitterParams` (uniform lu par le compute) gagne `alpha_scale` → struct **48 B** (toujours dans le buffer 64 B déclaré).
|
||||
|
||||
Impact : §4.2/§4.3 (configs — appliqué), §5.4 (presets : colonne `color` → `color_range` + `α_scale` — appliqué), §6/§8.1 (EmitterParams + formule de fade — appliqué), §10.2 (manual : pas de config, écriture directe — inchangé). **Pas dans l'étape A du DRAFT** (étapes B/C/D).
|
||||
|
||||
### D19 — Audit layout/détails (préparation DRAFT) : 80 B sans padding, un seul struct partagé, `compact_index` ×1
|
||||
|
||||
Audit fait en écrivant le DRAFT. Trois corrections, toutes dans le même sens : **l'alignement de l'espace uniform du draft avait fuité dans l'espace storage**.
|
||||
|
||||
1. **Pas de padding dans les structs particule.** `Particle` vit en **espace storage**, où `vec3<f32>` a **align 4** (l'espace uniform a align 16 — d'où venaient `pad0`/`pad1` du draft). Le Rust `[f32; 3]` est aussi align 4 → layout naturellement identique dans les deux langages : pos@0, vel@12, life@24, max_life@28, size@32, size_growth@36, angle@40, angular_vel@44, color@48, uv_rect@64 → **80 B exactement, sans padding**. L'état précédent (avec padding) faisait en réalité **88 B** — en contradiction avec la mention « 80 bytes » (le commentaire d'offset 64 de `uv_rect` incohérent avec color@56+16=72). Appliqué : §2, §8.1, §8.2.
|
||||
2. **Un seul struct `Particle` partagé** entre les entry points compute et render : le double struct `ParticleAlive` du draft (64 B, pad) était un **préfixe incompatible** du `Particle` padé (life@32 vs life@24) → le cull aurait lu les mauvais offsets. Supprimé : un storage load ne dépend pas de la taille du struct déclaré, la vue « sous-ensemble » n'a aucun gain. (`EmitterParams` **conserve** son padding — espace uniform, vec3 align 16.)
|
||||
3. **`compact_index` : 1 u32 par slot (pas 4 copies), lu par **storage binding** (pas d'attribut vertex).** Le buffer est lié en storage (binding 2) et le VS fait `compact_index[ii]` — le vertex layout du pipeline reste vide (pattern TM). La compaction écrit **un** u32 par particule alive (pas quatre). `indirect_args` = **16 B** (4 × u32, `drawIndirect` non-indexé) et non 20 B. Usage du buffer : STORAGE | COPY_DST (pas de VERTEX). Appliqué : §3.2, §6, §7.2, §17 (D6/D17), §18 (D17).
|
||||
|
||||
Règle future : ajouter un champ à `Particle` → alignement storage (vec3/vec4 = align 4, pas de padding) + mettre à jour les deux miroirs (Rust §2 + WGSL §8) + les tests de layout (DRAFT A).
|
||||
|
||||
Reference in New Issue
Block a user