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));
}
// 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)
// __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)
// 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)
@@ -342,7 +357,6 @@ export fn renderParticleSprites_SSE(emitter: u32, particle_data: u32, vertex_buf
const b1: u8 = @truncate(color_value >> 8);
const b2: u8 = @truncate(color_value >> 16);
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);
}
@@ -432,40 +446,23 @@ export fn renderParticleSprites_SSE(emitter: u32, particle_data: u32, vertex_buf
}
} else {
// --- 3D billboard (asm 0x7B2C25-0x7B2D04) ---
// 4 vertices, using 3D offset table
const table_base: u32 = G.billboard_3d;
const ref_base: u32 = G.billboard_3d_base;
var vert: u32 = 0;
while (vert < 4) : (vert += 1) {
const tbl = ref_base + vert * 12; // stride 0xC per vertex in ref table
const ox = sprite_scale * rf32(tbl - 4);
const oy = sprite_scale * rf32(tbl);
const oz = sprite_scale * rf32(tbl + 4);
const vx = ox + world_pos[0];
const vy = oy + world_pos[1];
const vz = oz + world_pos[2];
const pos_ptr = ru32(vb + VB.pos);
wf32(pos_ptr, vx);
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 = 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));
{
var vs = VBState.load(vb);
const table_base: u32 = G.billboard_3d;
const ref_base: u32 = G.billboard_3d_base;
var vert: u32 = 0;
while (vert < 4) : (vert += 1) {
const tbl = ref_base + vert * 12;
vs.emit(
@mulAdd(f32, sprite_scale, rf32(tbl - 4), world_pos[0]),
@mulAdd(f32, sprite_scale, rf32(tbl), world_pos[1]),
@mulAdd(f32, sprite_scale, rf32(tbl + 4), world_pos[2]),
color_value,
@mulAdd(f32, rf32(table_base + vert * 8 - 4), tex_scale_u, tex_u_base),
@mulAdd(f32, rf32(table_base + vert * 8), tex_scale_v, tex_v_base),
);
}
vs.writeback();
}
}
} else {
@@ -513,46 +510,27 @@ export fn renderParticleSprites_SSE(emitter: u32, particle_data: u32, vertex_buf
_ = createRotMat(@intFromPtr(&rot_mat), emitter + E.rotation_axis,
@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 vert: u32 = 0;
while (vert < 4) : (vert += 1) {
const tbl = ref_base + vert * 12;
const ix = rf32(tbl - 4);
const iy = rf32(tbl);
const iz = rf32(tbl + 4);
// mat3x3 * vec3
const rx = (rot_mat[0] * ix + rot_mat[1] * iy + rot_mat[2] * iz) * sprite_scale;
const ry = (rot_mat[3] * ix + rot_mat[4] * iy + rot_mat[5] * iz) * sprite_scale;
const rz = (rot_mat[6] * ix + rot_mat[7] * iy + rot_mat[8] * iz) * sprite_scale;
const vx = rx + world_pos[0];
const vy = ry + world_pos[1];
const vz = rz + world_pos[2];
const pos_ptr = ru32(vb + VB.pos);
wf32(pos_ptr, vx);
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));
{
var vs = VBState.load(vb);
const ref_base: u32 = G.billboard_3d_base;
const tex_off_base: u32 = G.billboard_3d;
var vert: u32 = 0;
while (vert < 4) : (vert += 1) {
const tbl = ref_base + vert * 12;
const ix = rf32(tbl - 4);
const iy = rf32(tbl);
const iz = rf32(tbl + 4);
// mat3x3 * vec3, scaled, + worldPos
vs.emit(
@mulAdd(f32, rot_mat[2], iz, @mulAdd(f32, rot_mat[1], iy, rot_mat[0] * ix)) * sprite_scale + world_pos[0],
@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],
color_value,
@mulAdd(f32, rf32(tex_off_base + vert * 8 - 4), tex_scale_u, tex_u_base),
@mulAdd(f32, rf32(tex_off_base + vert * 8), tex_scale_v, tex_v_base),
);
}
vs.writeback();
}
}
}
@@ -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
const vel_z = tail_dist * transformed_vel[2] + world_pos[2];
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 tex_su = rf32(emitter + E.texScaleU);
const tex_sv = rf32(emitter + E.texScaleV);
// Vertex 0: worldPos - perp
emitVertex(vb, 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,
rf32(G.sprite_tex_v) * tex_sv + tail_tex_v);
// Vertex 1: worldPos + perp
emitVertex(vb, world_pos[0] + perp_y, world_pos[1] - perp_x, world_pos[2], color_value,
rf32(G.sprite_tex_u) * tex_su + tail_tex_u,
rf32(G.sprite_tex_v) * tex_sv + tail_tex_v);
// Vertex 2: worldPos + vel - perp
emitVertex(vb, tx + world_pos[0] - perp_y, ty + world_pos[1] + perp_x, vel_z, color_value,
rf32(G.tail_tex_u0) * tex_su + tail_tex_u,
rf32(G.tail_tex_v0) * tex_sv + tail_tex_v);
// Vertex 3: worldPos + vel + perp
emitVertex(vb, tx + world_pos[0] + perp_y, ty + world_pos[1] - perp_x, vel_z, color_value,
rf32(G.tail_tex_u1) * tex_su + tail_tex_u,
rf32(G.tail_tex_v1) * tex_sv + tail_tex_v);
var vs = VBState.load(vb);
vs.emit(world_pos[0] - perp_y, perp_x + world_pos[1], world_pos[2], color_value,
@mulAdd(f32, rf32(G.sprite_tex_u), tex_su, tail_tex_u),
@mulAdd(f32, rf32(G.sprite_tex_v), tex_sv, tail_tex_v));
vs.emit(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),
@mulAdd(f32, 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,
@mulAdd(f32, rf32(G.tail_tex_u0), tex_su, tail_tex_u),
@mulAdd(f32, rf32(G.tail_tex_v0), tex_sv, tail_tex_v));
vs.emit(tx + world_pos[0] + perp_y, ty + world_pos[1] - perp_x, vel_z, color_value,
@mulAdd(f32, rf32(G.tail_tex_u1), tex_su, tail_tex_u),
@mulAdd(f32, rf32(G.tail_tex_v1), tex_sv, tail_tex_v));
vs.writeback();
return 1;
}
// Fallback: velocity too small for trail, render as flat billboard
var loop_off: u32 = 0;
const tex_su = rf32(emitter + E.texScaleU);
const tex_sv = rf32(emitter + E.texScaleV);
while (loop_off < 0x20) : (loop_off += 8) {
const ox = rf32(G.billboard_offsets_x + loop_off);
const oy = rf32(G.billboard_offsets_y + loop_off);
const vx = sprite_scale * ox + world_pos[0];
const vy = sprite_scale * oy + world_pos[1];
const tu = rf32(G.sprite_tex_u + loop_off + 8) * tex_su + tail_tex_u;
const tv = rf32(G.sprite_tex_v + loop_off + 8) * tex_sv + tail_tex_v;
emitVertex(vb, vx, vy, world_pos[2], color_value, tu, tv);
{
var vs = VBState.load(vb);
const tex_su = rf32(emitter + E.texScaleU);
const tex_sv = rf32(emitter + E.texScaleV);
var loop_off: u32 = 0;
while (loop_off < 0x20) : (loop_off += 8) {
vs.emit(
@mulAdd(f32, sprite_scale, rf32(G.billboard_offsets_x + loop_off), world_pos[0]),
@mulAdd(f32, sprite_scale, rf32(G.billboard_offsets_y + loop_off), world_pos[1]),
world_pos[2],
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 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 resetParticleCache() void;
/// Thiscall wrapper for the SSE implementation. Lives here (baseline SSE2 unit)
/// 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) = .{};
fn worldUpdateDetour(frame_count: u32) callconv(hook.cc.fastcall) void {
resetParticleCache(); // Clear per-frame caches before rendering
const now = rdtsc();
if (last_frame_tsc != 0) {
const delta = now - last_frame_tsc;