refactor(renderer): activate depth buffer on all render paths (Étape 9)

- 9.1: allocate shared Depth32Float depth texture + view in Renderer,
  sized to the initial surface (create_depth_texture helper, reusable
  for the Phase 4.4 resize); Renderer::new now takes width/height.
- 9.2: attach depth_stencil_attachment (clear 1.0 / store) to both the
  low-level render and high-level render_scene passes.
- 9.3: every pipeline declares a matching DepthStencilState (write true,
  compare Less) via the shared DEPTH_FORMAT constant (D1).

Décisions D1-D4 actées 2026-09-18 (DRAFT Étape 9).
This commit is contained in:
Jérôme Bousquié
2026-09-18 13:03:52 +02:00
parent 17989b177f
commit fca5d728ad
5 changed files with 94 additions and 7 deletions
+1 -1
View File
@@ -64,7 +64,7 @@ impl ApplicationHandler for App {
// Rendu 2D plat : `standard` en mode unlit (les bind groups frame+object sont posés par // Rendu 2D plat : `standard` en mode unlit (les bind groups frame+object sont posés par
// draw_entity, la matrice frame par défaut est l'identité → positions NDC inchangées). // draw_entity, la matrice frame par défaut est l'identité → positions NDC inchangées).
let mut renderer = Renderer::new(&context, format); let mut renderer = Renderer::new(&context, format, 800, 600);
renderer.set_unlit(true); renderer.set_unlit(true);
// 3. Material : On utilise renderer.device() et renderer.format() // 3. Material : On utilise renderer.device() et renderer.format()
+1 -1
View File
@@ -233,7 +233,7 @@ impl<H: AppHandler> ApplicationHandler for AppRunner<H> {
.configure(&context.adapter, self.width, self.height) .configure(&context.adapter, self.width, self.height)
.expect("Échec configuration surface"); .expect("Échec configuration surface");
let device = Arc::new(context.device.clone()); let device = Arc::new(context.device.clone());
let renderer = Renderer::new(&context, format); let renderer = Renderer::new(&context, format, self.width, self.height);
// Étape 7 (DRAFT 7.1) : the PipelineCache now lives in the Scene. We wire the GPU context // Étape 7 (DRAFT 7.1) : the PipelineCache now lives in the Scene. We wire the GPU context
// (device + format + cache) into the Scene before setup so it can build materials/meshes. // (device + format + cache) into the Scene before setup so it can build materials/meshes.
+71 -3
View File
@@ -21,7 +21,7 @@
use crate::core::Context; use crate::core::Context;
use crate::core::Frame; use crate::core::Frame;
use crate::math::Transform; use crate::math::Transform;
use crate::pipeline::create_uniform_bind_group_layouts; use crate::pipeline::{DEPTH_FORMAT, create_uniform_bind_group_layouts};
use crate::resources::uniform::{FRAME_UNIFORMS_SIZE, OBJECT_UNIFORM_SIZE}; use crate::resources::uniform::{FRAME_UNIFORMS_SIZE, OBJECT_UNIFORM_SIZE};
use crate::resources::{Camera, FrameUniforms, Material, Mesh, ObjectUniform}; use crate::resources::{Camera, FrameUniforms, Material, Mesh, ObjectUniform};
use crate::scene::Scene; use crate::scene::Scene;
@@ -42,6 +42,14 @@ pub struct Renderer {
device: wgpu::Device, device: wgpu::Device,
/// Surface texture output format — stored here so it can be passed to PipelineCache on Material creation. /// Surface texture output format — stored here so it can be passed to PipelineCache on Material creation.
format: wgpu::TextureFormat, format: wgpu::TextureFormat,
/// z-buffer texture backing `depth_view` (Étape 9). Held here only to keep the GPU resource
/// alive for the whole application lifetime (a `TextureView` alone does not guarantee the
/// underlying `Texture` stays valid in wgpu). Not read directly (hence `_` prefix → no
/// `dead_code`); reused when the depth texture is recreated at resize (ROADMAP Phase 4.4).
_depth_texture: wgpu::Texture,
/// Depth attachment view used by both render passes (`render`, `render_scene`). Format
/// `DEPTH_FORMAT` (Depth32Float) — matches every pipeline's `DepthStencilState` (D1).
depth_view: wgpu::TextureView,
/// Bind group layout for the per-object uniforms (group 1) — must match every pipeline layout. /// Bind group layout for the per-object uniforms (group 1) — must match every pipeline layout.
object_layout: wgpu::BindGroupLayout, object_layout: wgpu::BindGroupLayout,
/// Shared per-frame uniform buffer handle — kept so the camera matrices can be rewritten each /// Shared per-frame uniform buffer handle — kept so the camera matrices can be rewritten each
@@ -64,15 +72,21 @@ pub struct Renderer {
impl Renderer { impl Renderer {
/// Creates a Renderer by cloning Device and Queue Arc references from the Context, plus capturing /// Creates a Renderer by cloning Device and Queue Arc references from the Context, plus capturing
/// the surface format, and allocates the shared frame + object uniform buffers and their bind groups. /// the surface format, and allocates the shared frame + object uniform buffers and their bind groups.
/// Inputs: context (borrowed reference to Context providing GPU resource handles), format (surface texture format). /// Inputs: context (borrowed reference to Context providing GPU resource handles), format (surface
/// texture format), width (surface width in pixels) and height (surface height in pixels) — the
/// latter two size the depth texture allocated here (Étape 9).
/// Returns a new Renderer instance sharing the same underlying GPU resources as Context. /// Returns a new Renderer instance sharing the same underlying GPU resources as Context.
/// Called once at application startup during scene setup. The Renderer shares these resources via Arc; /// Called once at application startup during scene setup. The Renderer shares these resources via Arc;
/// Context retains ownership and can continue using them after this call. /// Context retains ownership and can continue using them after this call.
pub fn new(context: &Context, format: wgpu::TextureFormat) -> Self { pub fn new(context: &Context, format: wgpu::TextureFormat, width: u32, height: u32) -> Self {
let queue: wgpu::Queue = context.queue.clone(); let queue: wgpu::Queue = context.queue.clone();
let device: wgpu::Device = context.device.clone(); let device: wgpu::Device = context.device.clone();
let [frame_layout, object_layout] = create_uniform_bind_group_layouts(&device); let [frame_layout, object_layout] = create_uniform_bind_group_layouts(&device);
// Étape 9 (DRAFT 9.1) : depth texture + view, allouées une seule fois à la taille initiale
// de la surface (D3). Le helper isolé rendra trivial le recreate planifié en Phase 4.4.
let (depth_texture, depth_view) = create_depth_texture(&device, width, height);
// Shared frame uniforms: identity camera + white directional light, lit mode by default. // Shared frame uniforms: identity camera + white directional light, lit mode by default.
// Values become meaningful once an active camera is wired (Étape 4.3); for now the default // Values become meaningful once an active camera is wired (Étape 4.3); for now the default
// is a coherent scene when a shader actually reads them, and irrelevant to shaders that don't. // is a coherent scene when a shader actually reads them, and irrelevant to shaders that don't.
@@ -115,6 +129,8 @@ impl Renderer {
queue, queue,
device, device,
format, format,
_depth_texture: depth_texture,
depth_view,
object_layout, object_layout,
frame_buffer, frame_buffer,
frame_bind_group, frame_bind_group,
@@ -200,6 +216,16 @@ impl Renderer {
store: wgpu::StoreOp::Store, store: wgpu::StoreOp::Store,
}, },
})], })],
// Étape 9 (DRAFT 9.2) : depth attachment via la view partagée (D1 : clear 1.0
// = profondeur max au loin en début de frame, puis Store pour la garder).
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &self.depth_view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(1.0),
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
}),
..Default::default() ..Default::default()
}); });
@@ -245,6 +271,16 @@ impl Renderer {
store: wgpu::StoreOp::Store, store: wgpu::StoreOp::Store,
}, },
})], })],
// Étape 9 (DRAFT 9.2) : même depth attachment que le chemin bas niveau, pour un
// z-test cohérent (D2 — les deux render passes partagent la depth_view).
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &self.depth_view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(1.0),
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
}),
..Default::default() ..Default::default()
}); });
@@ -321,6 +357,38 @@ impl Renderer {
} }
} }
/// Allocates the depth texture + view backing the render passes' `depth_stencil_attachment`
/// (Étape 9, DRAFT 9.1). Format is the shared `DEPTH_FORMAT` (Depth32Float, D1) so it always
/// matches every pipeline's `DepthStencilState`. Sized to the surface (width x height), single
/// mip, no MSAA, used strictly as a render target.
///
/// Exposed as a standalone helper so the depth texture can be recreated cheaply at resize
/// (ROADMAP Phase 4.4) without touching the render-pass logic.
/// Inputs: device (GPU resource creator), width (surface width), height (surface height).
/// Returns the (texture, view) pair; the caller keeps both alive.
fn create_depth_texture(
device: &wgpu::Device,
width: u32,
height: u32,
) -> (wgpu::Texture, wgpu::TextureView) {
let depth_texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("depth texture"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: DEPTH_FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let depth_view = depth_texture.create_view(&wgpu::TextureViewDescriptor::default());
(depth_texture, depth_view)
}
/// Binds a Material pipeline, the two uniform bind groups, and Mesh buffers into an active render /// Binds a Material pipeline, the two uniform bind groups, and Mesh buffers into an active render
/// pass and issues the draw call. Shared by `Renderer::render` and `Renderer::render_scene`. /// pass and issues the draw call. Shared by `Renderer::render` and `Renderer::render_scene`.
/// The frame (group 0) and object (group 1) bind groups are **required** by every pipeline layout /// The frame (group 0) and object (group 1) bind groups are **required** by every pipeline layout
+1 -1
View File
@@ -11,4 +11,4 @@
pub mod pipeline_cache; pub mod pipeline_cache;
// Re-exports // Re-exports
pub use pipeline_cache::{PipelineCache, create_uniform_bind_group_layouts}; pub use pipeline_cache::{DEPTH_FORMAT, PipelineCache, create_uniform_bind_group_layouts};
+20 -1
View File
@@ -59,6 +59,15 @@ pub fn create_uniform_bind_group_layouts(device: &wgpu::Device) -> [wgpu::BindGr
] ]
} }
/// Depth texture format shared by the whole library (Étape 9, décision D1 du 2026-09-18).
///
/// Single z-buffer format used for **both** the depth attachment textures (`Renderer`) and the
/// `DepthStencilState` of every pipeline (`build_pipeline`). Keeping them on the same constant
/// guarantees by construction that the pipeline depth format always matches the texture format
/// (wgpu validation error otherwise). `Depth32Float` = portée maximale (comparaison précise),
/// avec clear `1.0` (profondeur maximale au loin), `depth_compare: Less`, write enabled.
pub const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
/// Shader pipeline cache: maps (shader_id, format) keys to compiled RenderPipelines. /// Shader pipeline cache: maps (shader_id, format) keys to compiled RenderPipelines.
/// Ensures each unique shader+format combination is compiled at most once; subsequent requests return cached instances. /// Ensures each unique shader+format combination is compiled at most once; subsequent requests return cached instances.
pub struct PipelineCache { pub struct PipelineCache {
@@ -230,7 +239,17 @@ impl PipelineCache {
})], })],
}), }),
primitive: wgpu::PrimitiveState::default(), primitive: wgpu::PrimitiveState::default(),
depth_stencil: None, // Étape 9 (DRAFT 9.3) : depth test activé sur TOUTE pipeline. Le format doit matcher
// l'attachment depth (DEPTH_FORMAT) — c'est garanti par la constante partagée D1.
// depth_write_enabled + depth_compare sont des Option en wgpu 30 : Some(true) → on
// écrit la profondeur ; Some(Less) → le fragment est gardé si son z est plus proche.
depth_stencil: Some(wgpu::DepthStencilState {
format: DEPTH_FORMAT,
depth_write_enabled: Some(true),
depth_compare: Some(wgpu::CompareFunction::Less),
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState::default(), multisample: wgpu::MultisampleState::default(),
// multiview → replaced by multiview_mask (NonZeroU32) and cache fields in wgpu 30. // multiview → replaced by multiview_mask (NonZeroU32) and cache fields in wgpu 30.
multiview_mask: None, multiview_mask: None,