From 73ef59173e3e8f55a2af46b5adf547396d61a6df Mon Sep 17 00:00:00 2001 From: Ralph Kuepper Date: Sat, 4 Jul 2026 17:17:25 +0200 Subject: [PATCH] =?UTF-8?q?feat(render):=20EN-022=20engine=20half=20?= =?UTF-8?q?=E2=80=94=20real=20prev-frame=20transforms=20for=20material=20d?= =?UTF-8?q?raws?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The material ABI carried PerDraw.prev_mvp "for motion vectors" since day one, but every submit path filled it with the CURRENT mvp — world motion vectors were identically zero by construction, so TAA's motion-adaptive clamp never engaged for material-drawn geometry (round-2 audit F8, the primary TSR-shimmer mechanism). - MaterialSystem keeps a per-slot model history (draw slot = submission order, stable frame-to-frame — the same identity convention the cached-model path relies on). submit_draw / submit_draw_instanced now compose prev_vp x prev_model[slot] into PerDraw.prev_mvp; a fresh slot falls back to the current model (zero object motion on its first frame). - reset_draw_slot rotates the history and pins the previous frame's view-projection (from Renderer::prev_vp_matrix, which already fed PerView.prev_view_proj correctly). - The legacy prev_mvp parameter on the submit APIs is ignored and renamed _legacy_prev_mvp (callers passed current-mvp garbage). - material_abi.wgsl gains `abi_motion_vector(curr, prev)` — the core path's exact NDC-delta x 0.5 convention — plus doc guidance for static vs wind-displaced vertex shaders (PerFrame.delta_time already enables prev-time wind evaluation with no ABI change). Behavioral no-op until materials read prev_mvp / write velocity (the shooter's material updates ride a stacked shooter PR). Suite green: 99 lib + 7 golden, all unchanged. --- native/shared/shaders/material_abi.wgsl | 16 ++++++ native/shared/src/renderer/material_system.rs | 56 ++++++++++++++++++- .../src/renderer/material_system_tests.rs | 2 +- native/shared/src/renderer/mod.rs | 6 +- 4 files changed, 74 insertions(+), 6 deletions(-) diff --git a/native/shared/shaders/material_abi.wgsl b/native/shared/shaders/material_abi.wgsl index 1c54612..7921db2 100644 --- a/native/shared/shaders/material_abi.wgsl +++ b/native/shared/shaders/material_abi.wgsl @@ -215,6 +215,22 @@ struct JointMatrices { @group(3) @binding(0) var draw: PerDraw; @group(3) @binding(1) var joints: JointMatrices; +// EN-022 — standard motion vector from current/previous clip positions. +// Matches the core path's convention exactly (NDC delta × 0.5). Vertex +// shaders pass their clip position plus a previous-frame clip position: +// static geometry uses `draw.prev_mvp * local` (or +// `view.prev_view_proj * world` — for static models the two agree); +// procedurally displaced geometry (wind sway) re-evaluates the +// displacement at `frame.time - frame.delta_time` and projects with +// `view.prev_view_proj`. Write the result to OpaqueOut.velocity — a +// zero write disables TAA's motion-adaptive clamp for that surface +// (the round-2 audit's primary TSR-shimmer mechanism, F8). +fn abi_motion_vector(curr_clip: vec4, prev_clip: vec4) -> vec2 { + let c = curr_clip.xy / curr_clip.w; + let p = prev_clip.xy / prev_clip.w; + return (c - p) * 0.5; +} + // ===================================================================== // Group 4 — SceneInputs (optional; `reads_scene = true` materials only) // ===================================================================== diff --git a/native/shared/src/renderer/material_system.rs b/native/shared/src/renderer/material_system.rs index f3489f6..bd57291 100644 --- a/native/shared/src/renderer/material_system.rs +++ b/native/shared/src/renderer/material_system.rs @@ -294,6 +294,19 @@ pub struct MaterialSystem { pub translucent_commands: Vec, // Transparent + Refractive + Additive next_draw_slot: usize, + /// EN-022 — per-slot model matrices from the PREVIOUS frame, keyed + /// by draw slot (= submission order, which is stable frame-to-frame + /// for immediate-mode games; the cached-model path relies on the + /// same convention). `submit_draw` composes prev_vp × prev_model + /// into PerDraw.prev_mvp so materials can output real motion + /// vectors — the ABI field existed since day one but was filled + /// with the CURRENT mvp, making world velocity identically zero + /// (round-2 audit F8). + prev_models: Vec<[[f32; 4]; 4]>, + cur_models: Vec<[[f32; 4]; 4]>, + /// Previous frame's view-projection (set in reset_draw_slot). + prev_vp: [[f32; 4]; 4], + /// EN-001 — instance buffers, indexed by InstanceBufferHandle /// (1-based; 0 = invalid). Each entry owns a wgpu Buffer + element /// count. Created via `create_instance_buffer`, consumed by @@ -622,6 +635,9 @@ impl MaterialSystem { commands: Vec::new(), translucent_commands: Vec::new(), next_draw_slot: 0, + prev_models: Vec::new(), + cur_models: Vec::new(), + prev_vp: super::IDENTITY_MAT4, instance_buffers: Vec::new(), texture_arrays: Vec::new(), material_texture_arrays: Vec::new(), @@ -694,8 +710,15 @@ impl MaterialSystem { /// Reset the per-draw slot cursor. Commands lists are cleared by /// the Renderer from its own `begin_frame` so the order of reset /// vs. submit is deterministic. - pub fn reset_draw_slot(&mut self) { + /// + /// EN-022 — also rotates the per-slot model history: this frame's + /// models become the previous frame's, and `prev_vp` is set to the + /// VP that was current when those models were submitted. + pub fn reset_draw_slot(&mut self, prev_vp: [[f32; 4]; 4]) { self.next_draw_slot = 0; + std::mem::swap(&mut self.prev_models, &mut self.cur_models); + self.cur_models.clear(); + self.prev_vp = prev_vp; } /// Phase 5 — set/replace `user_params` for a specific material. The @@ -1393,7 +1416,10 @@ impl MaterialSystem { mesh_idx: usize, mvp: [[f32; 4]; 4], model: [[f32; 4]; 4], - prev_mvp: [[f32; 4]; 4], + // EN-022: ignored — callers historically passed the CURRENT mvp + // here, zeroing every motion vector. The real previous-frame + // transform is reconstructed from the per-slot model history. + _legacy_prev_mvp: [[f32; 4]; 4], tint: [f32; 4], skin_info: [u32; 4], ) { @@ -1408,6 +1434,17 @@ impl MaterialSystem { self.next_draw_slot += 1; self.ensure_draw_slot(device, joint_buffer, slot); + // EN-022 — draw identity is the submission slot (stable order + // frame-to-frame, same convention the cached-model path uses). + // A fresh slot (first frame, or the frame after a draw-count + // change) falls back to the current model = zero object motion. + let prev_model = self.prev_models.get(slot).copied().unwrap_or(model); + let prev_mvp = super::mat4_multiply(self.prev_vp, prev_model); + if self.cur_models.len() <= slot { + self.cur_models.resize(slot + 1, model); + } + self.cur_models[slot] = model; + let per_draw = PerDrawUniforms { mvp, model, prev_mvp, model_tint: tint, skin_info }; queue.write_buffer(&self.per_draw_buffers[slot], 0, bytemuck::bytes_of(&per_draw)); @@ -1447,7 +1484,8 @@ impl MaterialSystem { instance_count: u32, mvp: [[f32; 4]; 4], model: [[f32; 4]; 4], - prev_mvp: [[f32; 4]; 4], + // EN-022: ignored — see submit_draw. + _legacy_prev_mvp: [[f32; 4]; 4], tint: [f32; 4], skin_info: [u32; 4], ) { @@ -1462,6 +1500,18 @@ impl MaterialSystem { self.next_draw_slot += 1; self.ensure_draw_slot(device, joint_buffer, slot); + // EN-022 — same slot-history reconstruction as submit_draw. + // Instance transforms live in the (static) instance buffer, so + // prev vs current only differ by the fallback model + camera; + // per-instance wind sway derives its own previous position in + // the vertex shader from frame.time - frame.delta_time. + let prev_model = self.prev_models.get(slot).copied().unwrap_or(model); + let prev_mvp = super::mat4_multiply(self.prev_vp, prev_model); + if self.cur_models.len() <= slot { + self.cur_models.resize(slot + 1, model); + } + self.cur_models[slot] = model; + let per_draw = PerDrawUniforms { mvp, model, prev_mvp, model_tint: tint, skin_info }; queue.write_buffer(&self.per_draw_buffers[slot], 0, bytemuck::bytes_of(&per_draw)); diff --git a/native/shared/src/renderer/material_system_tests.rs b/native/shared/src/renderer/material_system_tests.rs index 54583d9..5bee241 100644 --- a/native/shared/src/renderer/material_system_tests.rs +++ b/native/shared/src/renderer/material_system_tests.rs @@ -171,7 +171,7 @@ fn fs_main(_in: VsOut) -> TranslucentOut { }; let pv = bytemuck::Zeroable::zeroed(); sys.update_frame_uniforms(&queue, &pf, &pv); - sys.reset_draw_slot(); + sys.reset_draw_slot(crate::renderer::IDENTITY_MAT4); // MVP = identity so the fullscreen tri stays in NDC. let identity = [ diff --git a/native/shared/src/renderer/mod.rs b/native/shared/src/renderer/mod.rs index ad66ec3..62e6ccc 100644 --- a/native/shared/src/renderer/mod.rs +++ b/native/shared/src/renderer/mod.rs @@ -8885,10 +8885,12 @@ impl Renderer { self.uniform_slot_count = 0; self.render_mode = RenderMode::ScreenSpace; // Phase 1c — clear last frame's material draws so the new - // frame's submissions start from an empty list. + // frame's submissions start from an empty list. EN-022: the + // reset also rotates the per-slot model history and pins the + // previous frame's VP for motion-vector reconstruction. self.material_system.commands.clear(); self.material_system.translucent_commands.clear(); - self.material_system.reset_draw_slot(); + self.material_system.reset_draw_slot(self.prev_vp_matrix); // Write identity uniforms to slot 0 (2D uses logical points, // not physical pixels — see Renderer::new).