Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions native/shared/shaders/material_abi.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,22 @@ struct JointMatrices {
@group(3) @binding(0) var<uniform> draw: PerDraw;
@group(3) @binding(1) var<uniform> 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<f32>, prev_clip: vec4<f32>) -> vec2<f32> {
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)
// =====================================================================
Expand Down
56 changes: 53 additions & 3 deletions native/shared/src/renderer/material_system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,19 @@ pub struct MaterialSystem {
pub translucent_commands: Vec<MaterialDrawCommand>, // 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
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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],
) {
Expand All @@ -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));

Expand Down Expand Up @@ -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],
) {
Expand All @@ -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));

Expand Down
2 changes: 1 addition & 1 deletion native/shared/src/renderer/material_system_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
6 changes: 4 additions & 2 deletions native/shared/src/renderer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down