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:
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user