particle: VBState all paths, cached setupRender, @mulAdd — ~25% speedup

- VBState caching on all 5 vertex paths (was only 2D and sin/cos).
  Eliminates pointer re-reads: load once, emit 4 vertices, writeback.
- Cache setupRender() result per-frame via static + resetParticleCache()
  called from worldUpdateDetour. Saves ~7K function calls/frame.
- @mulAdd throughout for FMA codegen on vertex position and texcoord.
- A/B verified: BASELINE ~521ms → CUSTOM ~387ms (~25% reduction).
This commit is contained in:
MarcelineVQ
2026-03-23 22:29:12 -07:00
parent 1dc1350645
commit 0d994db4b3
2 changed files with 87 additions and 105 deletions
+85 -105
View File
@@ -230,6 +230,15 @@ inline fn emitVertex(vb: u32, px: f32, py: f32, pz: f32, color: u32, tu: f32, tv
wu32(vb + VB.texcoord, ru32(vb + VB.texcoord) + ru32(vb + VB.texcoord_stride)); wu32(vb + VB.texcoord, ru32(vb + VB.texcoord) + ru32(vb + VB.texcoord_stride));
} }
// Cached render state — setupRender() returns the same pointer all frame.
// Reset each frame via resetParticleCache() called from the frame hook.
var cached_render_state: u32 = 0;
/// Reset per-frame caches. Call from OnWorldUpdate or executeSceneRenderPass hook.
export fn resetParticleCache() void {
cached_render_state = 0;
}
// ============================================================================= // =============================================================================
// RenderParticleSprites (0x7B2A50) // RenderParticleSprites (0x7B2A50)
// __thiscall(ECX=emitter, stack=particleData, vertexBuffers), RET 0x8 // __thiscall(ECX=emitter, stack=particleData, vertexBuffers), RET 0x8
@@ -328,9 +337,15 @@ export fn renderParticleSprites_SSE(emitter: u32, particle_data: u32, vertex_buf
// ========================================================================= // =========================================================================
// Section 3: Render state setup (asm 0x7B2B46) // Section 3: Render state setup (asm 0x7B2B46)
// Cached: the render state pointer doesn't change within a frame.
// ========================================================================= // =========================================================================
const render_state = setupRender(); const render_state = blk: {
if (cached_render_state != 0) break :blk cached_render_state;
const rs = setupRender();
cached_render_state = rs;
break :blk rs;
};
// ========================================================================= // =========================================================================
// Section 4: Color byte swizzle (asm 0x7B2B4B-0x7B2B6E) // Section 4: Color byte swizzle (asm 0x7B2B4B-0x7B2B6E)
@@ -342,7 +357,6 @@ export fn renderParticleSprites_SSE(emitter: u32, particle_data: u32, vertex_buf
const b1: u8 = @truncate(color_value >> 8); const b1: u8 = @truncate(color_value >> 8);
const b2: u8 = @truncate(color_value >> 16); const b2: u8 = @truncate(color_value >> 16);
const b3: u8 = @truncate(color_value >> 24); const b3: u8 = @truncate(color_value >> 24);
// Swizzle: [B,G,R,A] → [R,B,A,G] (based on asm byte shuffling)
color_value = @as(u32, b2) | (@as(u32, b0) << 8) | (@as(u32, b3) << 16) | (@as(u32, b1) << 24); color_value = @as(u32, b2) | (@as(u32, b0) << 8) | (@as(u32, b3) << 16) | (@as(u32, b1) << 24);
} }
@@ -432,40 +446,23 @@ export fn renderParticleSprites_SSE(emitter: u32, particle_data: u32, vertex_buf
} }
} else { } else {
// --- 3D billboard (asm 0x7B2C25-0x7B2D04) --- // --- 3D billboard (asm 0x7B2C25-0x7B2D04) ---
// 4 vertices, using 3D offset table {
const table_base: u32 = G.billboard_3d; var vs = VBState.load(vb);
const ref_base: u32 = G.billboard_3d_base; const table_base: u32 = G.billboard_3d;
var vert: u32 = 0; const ref_base: u32 = G.billboard_3d_base;
while (vert < 4) : (vert += 1) { var vert: u32 = 0;
const tbl = ref_base + vert * 12; // stride 0xC per vertex in ref table while (vert < 4) : (vert += 1) {
const ox = sprite_scale * rf32(tbl - 4); const tbl = ref_base + vert * 12;
const oy = sprite_scale * rf32(tbl); vs.emit(
const oz = sprite_scale * rf32(tbl + 4); @mulAdd(f32, sprite_scale, rf32(tbl - 4), world_pos[0]),
const vx = ox + world_pos[0]; @mulAdd(f32, sprite_scale, rf32(tbl), world_pos[1]),
const vy = oy + world_pos[1]; @mulAdd(f32, sprite_scale, rf32(tbl + 4), world_pos[2]),
const vz = oz + world_pos[2]; color_value,
@mulAdd(f32, rf32(table_base + vert * 8 - 4), tex_scale_u, tex_u_base),
const pos_ptr = ru32(vb + VB.pos); @mulAdd(f32, rf32(table_base + vert * 8), tex_scale_v, tex_v_base),
wf32(pos_ptr, vx); );
wf32(pos_ptr + 4, vy); }
wf32(pos_ptr + 8, vz); vs.writeback();
const norm_ptr = ru32(vb + VB.normal);
wu32(norm_ptr, ru32(G.light_dir_x));
wu32(norm_ptr + 4, ru32(G.light_dir_y));
wu32(norm_ptr + 8, ru32(G.light_dir_z));
wu32(ru32(vb + VB.color), color_value);
const tc_ptr = ru32(vb + VB.texcoord);
const tu_off: u32 = table_base + vert * 8 - 4; // asm uses stride 8, offset -4
const tv_off: u32 = table_base + vert * 8;
wf32(tc_ptr, rf32(tu_off) * tex_scale_u + tex_u_base);
wf32(tc_ptr + 4, rf32(tv_off) * tex_scale_v + tex_v_base);
wu32(vb + VB.count, ru32(vb + VB.count) + 1);
wu32(vb + VB.pos, ru32(vb + VB.pos) + ru32(vb + VB.pos_stride));
wu32(vb + VB.normal, ru32(vb + VB.normal) + ru32(vb + VB.normal_stride));
wu32(vb + VB.color, ru32(vb + VB.color) + ru32(vb + VB.color_stride));
wu32(vb + VB.texcoord, ru32(vb + VB.texcoord) + ru32(vb + VB.texcoord_stride));
} }
} }
} else { } else {
@@ -513,46 +510,27 @@ export fn renderParticleSprites_SSE(emitter: u32, particle_data: u32, vertex_buf
_ = createRotMat(@intFromPtr(&rot_mat), emitter + E.rotation_axis, _ = createRotMat(@intFromPtr(&rot_mat), emitter + E.rotation_axis,
@bitCast(rot_angle), 1); @bitCast(rot_angle), 1);
const ref_base: u32 = G.billboard_3d_base; {
const tex_off_base: u32 = G.billboard_3d; // reused for tex offsets var vs = VBState.load(vb);
const ref_base: u32 = G.billboard_3d_base;
var vert: u32 = 0; const tex_off_base: u32 = G.billboard_3d;
while (vert < 4) : (vert += 1) { var vert: u32 = 0;
const tbl = ref_base + vert * 12; while (vert < 4) : (vert += 1) {
const ix = rf32(tbl - 4); const tbl = ref_base + vert * 12;
const iy = rf32(tbl); const ix = rf32(tbl - 4);
const iz = rf32(tbl + 4); const iy = rf32(tbl);
const iz = rf32(tbl + 4);
// mat3x3 * vec3 // mat3x3 * vec3, scaled, + worldPos
const rx = (rot_mat[0] * ix + rot_mat[1] * iy + rot_mat[2] * iz) * sprite_scale; vs.emit(
const ry = (rot_mat[3] * ix + rot_mat[4] * iy + rot_mat[5] * iz) * sprite_scale; @mulAdd(f32, rot_mat[2], iz, @mulAdd(f32, rot_mat[1], iy, rot_mat[0] * ix)) * sprite_scale + world_pos[0],
const rz = (rot_mat[6] * ix + rot_mat[7] * iy + rot_mat[8] * iz) * sprite_scale; @mulAdd(f32, rot_mat[5], iz, @mulAdd(f32, rot_mat[4], iy, rot_mat[3] * ix)) * sprite_scale + world_pos[1],
@mulAdd(f32, rot_mat[8], iz, @mulAdd(f32, rot_mat[7], iy, rot_mat[6] * ix)) * sprite_scale + world_pos[2],
const vx = rx + world_pos[0]; color_value,
const vy = ry + world_pos[1]; @mulAdd(f32, rf32(tex_off_base + vert * 8 - 4), tex_scale_u, tex_u_base),
const vz = rz + world_pos[2]; @mulAdd(f32, rf32(tex_off_base + vert * 8), tex_scale_v, tex_v_base),
);
const pos_ptr = ru32(vb + VB.pos); }
wf32(pos_ptr, vx); vs.writeback();
wf32(pos_ptr + 4, vy);
wf32(pos_ptr + 8, vz);
const norm_ptr = ru32(vb + VB.normal);
wu32(norm_ptr, ru32(G.light_dir_x));
wu32(norm_ptr + 4, ru32(G.light_dir_y));
wu32(norm_ptr + 8, ru32(G.light_dir_z));
wu32(ru32(vb + VB.color), color_value);
const tc_ptr = ru32(vb + VB.texcoord);
const tu_off: u32 = tex_off_base + vert * 8 - 4;
const tv_off: u32 = tex_off_base + vert * 8;
wf32(tc_ptr, rf32(tu_off) * tex_scale_u + tex_u_base);
wf32(tc_ptr + 4, rf32(tv_off) * tex_scale_v + tex_v_base);
wu32(vb + VB.count, ru32(vb + VB.count) + 1);
wu32(vb + VB.pos, ru32(vb + VB.pos) + ru32(vb + VB.pos_stride));
wu32(vb + VB.normal, ru32(vb + VB.normal) + ru32(vb + VB.normal_stride));
wu32(vb + VB.color, ru32(vb + VB.color) + ru32(vb + VB.color_stride));
wu32(vb + VB.texcoord, ru32(vb + VB.texcoord) + ru32(vb + VB.texcoord_stride));
} }
} }
} }
@@ -598,44 +576,46 @@ export fn renderParticleSprites_SSE(emitter: u32, particle_data: u32, vertex_buf
// Velocity-based trail: 4 vertices forming a quad along velocity direction // Velocity-based trail: 4 vertices forming a quad along velocity direction
const vel_z = tail_dist * transformed_vel[2] + world_pos[2]; const vel_z = tail_dist * transformed_vel[2] + world_pos[2];
const inv_len = sprite_scale / @sqrt(cos_sq); const inv_len = sprite_scale / @sqrt(cos_sq);
const perp_x = tx * inv_len; // perpendicular to velocity const perp_x = tx * inv_len;
const perp_y = inv_len * ty; const perp_y = inv_len * ty;
const tex_su = rf32(emitter + E.texScaleU); const tex_su = rf32(emitter + E.texScaleU);
const tex_sv = rf32(emitter + E.texScaleV); const tex_sv = rf32(emitter + E.texScaleV);
// Vertex 0: worldPos - perp var vs = VBState.load(vb);
emitVertex(vb, world_pos[0] - perp_y, perp_x + world_pos[1], world_pos[2], color_value, vs.emit(world_pos[0] - perp_y, perp_x + world_pos[1], world_pos[2], color_value,
rf32(G.sprite_tex_u) * tex_su + tail_tex_u, @mulAdd(f32, rf32(G.sprite_tex_u), tex_su, tail_tex_u),
rf32(G.sprite_tex_v) * tex_sv + tail_tex_v); @mulAdd(f32, rf32(G.sprite_tex_v), tex_sv, tail_tex_v));
// Vertex 1: worldPos + perp vs.emit(world_pos[0] + perp_y, world_pos[1] - perp_x, world_pos[2], color_value,
emitVertex(vb, world_pos[0] + perp_y, world_pos[1] - perp_x, world_pos[2], color_value, @mulAdd(f32, rf32(G.sprite_tex_u), tex_su, tail_tex_u),
rf32(G.sprite_tex_u) * tex_su + tail_tex_u, @mulAdd(f32, rf32(G.sprite_tex_v), tex_sv, tail_tex_v));
rf32(G.sprite_tex_v) * tex_sv + tail_tex_v); vs.emit(tx + world_pos[0] - perp_y, ty + world_pos[1] + perp_x, vel_z, color_value,
// Vertex 2: worldPos + vel - perp @mulAdd(f32, rf32(G.tail_tex_u0), tex_su, tail_tex_u),
emitVertex(vb, tx + world_pos[0] - perp_y, ty + world_pos[1] + perp_x, vel_z, color_value, @mulAdd(f32, rf32(G.tail_tex_v0), tex_sv, tail_tex_v));
rf32(G.tail_tex_u0) * tex_su + tail_tex_u, vs.emit(tx + world_pos[0] + perp_y, ty + world_pos[1] - perp_x, vel_z, color_value,
rf32(G.tail_tex_v0) * tex_sv + tail_tex_v); @mulAdd(f32, rf32(G.tail_tex_u1), tex_su, tail_tex_u),
// Vertex 3: worldPos + vel + perp @mulAdd(f32, rf32(G.tail_tex_v1), tex_sv, tail_tex_v));
emitVertex(vb, tx + world_pos[0] + perp_y, ty + world_pos[1] - perp_x, vel_z, color_value, vs.writeback();
rf32(G.tail_tex_u1) * tex_su + tail_tex_u,
rf32(G.tail_tex_v1) * tex_sv + tail_tex_v);
return 1; return 1;
} }
// Fallback: velocity too small for trail, render as flat billboard // Fallback: velocity too small for trail, render as flat billboard
var loop_off: u32 = 0; {
const tex_su = rf32(emitter + E.texScaleU); var vs = VBState.load(vb);
const tex_sv = rf32(emitter + E.texScaleV); const tex_su = rf32(emitter + E.texScaleU);
while (loop_off < 0x20) : (loop_off += 8) { const tex_sv = rf32(emitter + E.texScaleV);
const ox = rf32(G.billboard_offsets_x + loop_off); var loop_off: u32 = 0;
const oy = rf32(G.billboard_offsets_y + loop_off); while (loop_off < 0x20) : (loop_off += 8) {
const vx = sprite_scale * ox + world_pos[0]; vs.emit(
const vy = sprite_scale * oy + world_pos[1]; @mulAdd(f32, sprite_scale, rf32(G.billboard_offsets_x + loop_off), world_pos[0]),
const tu = rf32(G.sprite_tex_u + loop_off + 8) * tex_su + tail_tex_u; @mulAdd(f32, sprite_scale, rf32(G.billboard_offsets_y + loop_off), world_pos[1]),
const tv = rf32(G.sprite_tex_v + loop_off + 8) * tex_sv + tail_tex_v; world_pos[2],
emitVertex(vb, vx, vy, world_pos[2], color_value, tu, tv); color_value,
@mulAdd(f32, rf32(G.sprite_tex_u + loop_off + 8), tex_su, tail_tex_u),
@mulAdd(f32, rf32(G.sprite_tex_v + loop_off + 8), tex_sv, tail_tex_v),
);
}
vs.writeback();
} }
} }
+2
View File
@@ -24,6 +24,7 @@ extern fn multiplyMatrix4x4(u32, u32, u32) u32;
extern fn transformImpl_SSE(u32, u32, u32, u32, u32) callconv(.c) void; extern fn transformImpl_SSE(u32, u32, u32, u32, u32) callconv(.c) void;
extern fn calcColorValues_SSE(u32, u32, u32, u32, u32, u32, u32) callconv(.{ .x86_thiscall = .{} }) void; extern fn calcColorValues_SSE(u32, u32, u32, u32, u32, u32, u32) callconv(.{ .x86_thiscall = .{} }) void;
extern fn renderParticleSprites_SSE(u32, u32, u32) callconv(.{ .x86_thiscall = .{} }) u32; extern fn renderParticleSprites_SSE(u32, u32, u32) callconv(.{ .x86_thiscall = .{} }) u32;
extern fn resetParticleCache() void;
/// Thiscall wrapper for the SSE implementation. Lives here (baseline SSE2 unit) /// Thiscall wrapper for the SSE implementation. Lives here (baseline SSE2 unit)
/// so LLVM can't inline transformImpl_SSE's alignment into the thiscall frame. /// so LLVM can't inline transformImpl_SSE's alignment into the thiscall frame.
@@ -365,6 +366,7 @@ const WorldUpdateFn = fn (u32) callconv(hook.cc.fastcall) void;
var world_update_hook: hook.Detour(WorldUpdateFn) = .{}; var world_update_hook: hook.Detour(WorldUpdateFn) = .{};
fn worldUpdateDetour(frame_count: u32) callconv(hook.cc.fastcall) void { fn worldUpdateDetour(frame_count: u32) callconv(hook.cc.fastcall) void {
resetParticleCache(); // Clear per-frame caches before rendering
const now = rdtsc(); const now = rdtsc();
if (last_frame_tsc != 0) { if (last_frame_tsc != 0) {
const delta = now - last_frame_tsc; const delta = now - last_frame_tsc;