particle: inline calcColor + mat*vec3, VBState caching — ~20% speedup

- Inline calcColor: eliminates function call, allows OoO overlap of
  cache misses on colorCtx with vertex math. Pow path falls back to
  game function.
- Inline mat*vec3 transform: V4 FMA chain replaces call to 0x7BCA80.
- VBState: cache VB pointers/strides in locals, write back once after
  4 vertices. Eliminates ~80 pointer re-reads per particle.
- @mulAdd throughout vertex loops for FMA codegen.
- A/B verified: BASELINE ~470ms → CUSTOM ~382ms (~20% reduction).
This commit is contained in:
MarcelineVQ
2026-03-23 22:22:31 -07:00
parent 574f96f661
commit 1dc1350645
+145 -39
View File
@@ -40,6 +40,9 @@ inline fn wu32(addr: u32, val: u32) void {
inline fn wu8(addr: u32, val: u8) void {
@as(*u8, @ptrFromInt(addr)).* = val;
}
inline fn loadV4(ptr: u32) V4 {
return @as(*align(1) const V4, @ptrFromInt(ptr)).*;
}
// =============================================================================
// Emitter struct offsets (this = ECX = ParticleSystemRenderer*)
@@ -148,25 +151,78 @@ const VB = struct {
const count: u32 = 32;
};
/// Emit one vertex: write position, normal (light dir), color, texcoord, advance pointers.
/// Cached vertex buffer state — avoids re-reading pointer array per vertex.
/// Load once at start, emit vertices via direct pointer math, write back at end.
const VBState = struct {
pos: u32,
normal: u32,
color_ptr: u32,
texcoord: u32,
pos_stride: u32,
normal_stride: u32,
color_stride: u32,
texcoord_stride: u32,
count: u32,
vb: u32, // base pointer for writeback
// Cached light direction (same for all vertices)
light: [3]u32,
fn load(vb: u32) VBState {
return .{
.pos = ru32(vb + VB.pos),
.normal = ru32(vb + VB.normal),
.color_ptr = ru32(vb + VB.color),
.texcoord = ru32(vb + VB.texcoord),
.pos_stride = ru32(vb + VB.pos_stride),
.normal_stride = ru32(vb + VB.normal_stride),
.color_stride = ru32(vb + VB.color_stride),
.texcoord_stride = ru32(vb + VB.texcoord_stride),
.count = ru32(vb + VB.count),
.vb = vb,
.light = .{ ru32(G.light_dir_x), ru32(G.light_dir_y), ru32(G.light_dir_z) },
};
}
fn emit(s: *VBState, px: f32, py: f32, pz: f32, color: u32, tu: f32, tv: f32) void {
wf32(s.pos, px);
wf32(s.pos + 4, py);
wf32(s.pos + 8, pz);
wu32(s.normal, s.light[0]);
wu32(s.normal + 4, s.light[1]);
wu32(s.normal + 8, s.light[2]);
wu32(s.color_ptr, color);
wf32(s.texcoord, tu);
wf32(s.texcoord + 4, tv);
s.pos += s.pos_stride;
s.normal += s.normal_stride;
s.color_ptr += s.color_stride;
s.texcoord += s.texcoord_stride;
s.count += 1;
}
fn writeback(s: *const VBState) void {
wu32(s.vb + VB.pos, s.pos);
wu32(s.vb + VB.normal, s.normal);
wu32(s.vb + VB.color, s.color_ptr);
wu32(s.vb + VB.texcoord, s.texcoord);
wu32(s.vb + VB.count, s.count);
}
};
/// Emit one vertex using the old pointer-chasing path (for code paths not yet converted to VBState).
inline fn emitVertex(vb: u32, px: f32, py: f32, pz: f32, color: u32, tu: f32, tv: f32) void {
// Position
const pos_ptr = ru32(vb + VB.pos);
wf32(pos_ptr, px);
wf32(pos_ptr + 4, py);
wf32(pos_ptr + 8, pz);
// Normal (light direction — global, same for all particles)
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));
// Color
wu32(ru32(vb + VB.color), color);
// Texcoords
const tc_ptr = ru32(vb + VB.texcoord);
wf32(tc_ptr, tu);
wf32(tc_ptr + 4, tv);
// Advance pointers and increment count
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));
@@ -223,17 +279,52 @@ export fn renderParticleSprites_SSE(emitter: u32, particle_data: u32, vertex_buf
const color_ctx_offset: u32 = @as(u32, ru8(pd + 0x0C)) * 96;
const color_ctx = emitter + E.colorCtxBase + color_ctx_offset;
// Read orientation/scale data from emitter+0x1A8 — passed directly as arg2 to calcColor
const orientation_data = ru32(emitter + E.orientation_base);
// Inline calcColor: compute color, alpha, and sprite scale from colorCtx
// Original at 0x7B9B10, assembly-verified. Inlined to allow OoO overlap with cache misses.
const scale_param: f32 = @bitCast(ru32(emitter + E.orientation_base)); // arg2: float scale for alpha
const time_val: f32 = rf32(pd + 0x1C);
var color_value: u32 = 0;
var color_data1: u32 = 0;
var color_data2: u32 = 0;
var sprite_scale: f32 = undefined;
// t = (time - ctx.timeBase) * ctx.timeScale * CONST1 + CONST2
const t = (time_val - rf32(color_ctx + 0x2C)) * rf32(color_ctx + 0x30) * rf32(0x808AAC) + rf32(0x807A3C);
const magic: f32 = rf32(G.rounding_magic);
// calcColor: __thiscall(ECX=colorCtx, stack: time, orientData, outColor, outAlpha1, outAlpha2, outFloat)
calcColor(color_ctx, @bitCast(rf32(pd + 0x1C)), orientation_data,
@intFromPtr(&color_value), @intFromPtr(&color_data1), @intFromPtr(&color_data2), @intFromPtr(&sprite_scale));
// Color channels: (float)delta * t + (float)base [+ magic], extract byte via >>14
// Alpha (byte 3): scaled by scale_param
const alpha_f = @mulAdd(f32, @as(f32, @floatFromInt(ri32(color_ctx + 0x04))), t,
@as(f32, @floatFromInt(@as(i32, ru8(color_ctx + 3))))) * scale_param + magic;
// Red (byte 2): no scale
const red_f = @mulAdd(f32, @as(f32, @floatFromInt(ri32(color_ctx + 0x08))), t,
@as(f32, @floatFromInt(@as(i32, ru8(color_ctx + 2))))) + magic;
// Green (byte 1):
const green_f = @mulAdd(f32, @as(f32, @floatFromInt(ri32(color_ctx + 0x0C))), t,
@as(f32, @floatFromInt(@as(i32, ru8(color_ctx + 1))))) + magic;
// Blue (byte 0):
const blue_f = @mulAdd(f32, @as(f32, @floatFromInt(ri32(color_ctx + 0x10))), t,
@as(f32, @floatFromInt(@as(i32, ru8(color_ctx + 0))))) + magic;
var color_value: u32 = @as(u32, @truncate(@as(u32, @bitCast(blue_f)) >> 14)) |
(@as(u32, @truncate(@as(u32, @bitCast(green_f)) >> 14)) << 8) |
(@as(u32, @truncate(@as(u32, @bitCast(red_f)) >> 14)) << 16) |
(@as(u32, @truncate(@as(u32, @bitCast(alpha_f)) >> 14)) << 24);
// Sprite scale: t * ctx.scaleDelta + ctx.scaleBase
var sprite_scale: f32 = @mulAdd(f32, t, rf32(color_ctx + 0x28), rf32(color_ctx + 0x24));
// Alpha outputs (color_data1, color_data2) — used for texture index
var color_data1: u32 = undefined;
var color_data2: u32 = undefined;
const alpha_power = ru32(color_ctx + 0x50);
if (alpha_power == 0x3F800000) {
// Fast path: alphaPower == 1.0 (linear)
color_data1 = (@as(u32, @bitCast(@mulAdd(f32, @as(f32, @floatFromInt(ri32(color_ctx + 0x18))), t,
@as(f32, @floatFromInt(ri32(color_ctx + 0x14)))) + magic)) >> 14) & 0xFF;
color_data2 = (@as(u32, @bitCast(@mulAdd(f32, @as(f32, @floatFromInt(ri32(color_ctx + 0x20))), t,
@as(f32, @floatFromInt(ri32(color_ctx + 0x1C)))) + magic)) >> 14) & 0xFF;
} else {
// Slow path: pow scaling — fall back to game function call
calcColor(color_ctx, @bitCast(time_val), @bitCast(ru32(emitter + E.orientation_base)),
@intFromPtr(&color_value), @intFromPtr(&color_data1), @intFromPtr(&color_data2), @intFromPtr(&sprite_scale));
}
// =========================================================================
// Section 3: Render state setup (asm 0x7B2B46)
@@ -274,11 +365,16 @@ export fn renderParticleSprites_SSE(emitter: u32, particle_data: u32, vertex_buf
// =========================================================================
// Section 6: Position transform (asm 0x7B2BB4-0x7B2BC3)
// Transform particle world position through view matrix
// Inline V4 mat*vec3: result = col0*v.x + col1*v.y + col2*v.z + col3
// =========================================================================
var world_pos: [3]f32 = undefined;
_ = transformVec3(@intFromPtr(&world_pos), pd, G.world_matrix);
const pp: [*]const f32 = @ptrFromInt(pd);
const pvx: V4 = @splat(pp[0]);
const pvy: V4 = @splat(pp[1]);
const pvz: V4 = @splat(pp[2]);
const m: u32 = G.world_matrix;
const wp = @mulAdd(V4, pvz, loadV4(m + 32), @mulAdd(V4, pvy, loadV4(m + 16), @mulAdd(V4, pvx, loadV4(m), loadV4(m + 48))));
const world_pos = [3]f32{ wp[0], wp[1], wp[2] };
// =========================================================================
// Section 7: Branch on flag 0x4 — sprite vs tail rendering
@@ -318,16 +414,21 @@ export fn renderParticleSprites_SSE(emitter: u32, particle_data: u32, vertex_buf
// Assembly: eax starts at 0, adds 8 between X and Y reads.
// X: [eax+0x87D714], eax+=8, Y: [eax+0x87D710]=[eax_new+0x87D710]
// texU: [eax+0x87D72C], texV: [eax+0x87D730] (eax already incremented)
var loop_off: u32 = 0;
while (loop_off < 0x20) : (loop_off += 8) {
const ox = rf32(G.billboard_offsets_x + loop_off); // [eax+0x87D714]
const oy = rf32(G.billboard_offsets_y + loop_off); // [eax+8+0x87D710]
const vx = sprite_scale * ox + world_pos[0];
const vy = sprite_scale * oy + world_pos[1];
// Texcoords use eax+8 offset (eax already incremented in original)
const tu = rf32(G.sprite_tex_u + loop_off + 8) * tex_scale_u + tex_u_base;
const tv = rf32(G.sprite_tex_v + loop_off + 8) * tex_scale_v + tex_v_base;
emitVertex(vb, vx, vy, world_pos[2], color_value, tu, tv);
// 4 vertices with cached VB state to avoid pointer re-reads.
{
var vs = VBState.load(vb);
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_scale_u, tex_u_base),
@mulAdd(f32, rf32(G.sprite_tex_v + loop_off + 8), tex_scale_v, tex_v_base),
);
}
vs.writeback();
}
} else {
// --- 3D billboard (asm 0x7B2C25-0x7B2D04) ---
@@ -388,17 +489,22 @@ export fn renderParticleSprites_SSE(emitter: u32, particle_data: u32, vertex_buf
const scaled_sin = sin_val * sprite_scale;
const scaled_cos = cos_val * sprite_scale;
var loop_off: u32 = 0;
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);
// Rotated billboard: x' = ox*cos - oy*sin, y' = oy*cos + ox*sin
const vx = @mulAdd(f32, ox, scaled_cos, world_pos[0]) - oy * scaled_sin;
const vy = @mulAdd(f32, oy, scaled_cos, @mulAdd(f32, ox, scaled_sin, world_pos[1]));
// Texcoords use eax+8 offset (eax incremented before tex reads in original)
const tu = rf32(G.sprite_tex_u + loop_off + 8) * tex_scale_u + tex_u_base;
const tv = rf32(G.sprite_tex_v + loop_off + 8) * tex_scale_v + tex_v_base;
emitVertex(vb, vx, vy, world_pos[2], color_value, tu, tv);
{
var vs = VBState.load(vb);
var loop_off: u32 = 0;
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);
vs.emit(
@mulAdd(f32, ox, scaled_cos, world_pos[0]) - oy * scaled_sin,
@mulAdd(f32, oy, scaled_cos, @mulAdd(f32, ox, scaled_sin, world_pos[1])),
world_pos[2],
color_value,
@mulAdd(f32, rf32(G.sprite_tex_u + loop_off + 8), tex_scale_u, tex_u_base),
@mulAdd(f32, rf32(G.sprite_tex_v + loop_off + 8), tex_scale_v, tex_v_base),
);
}
vs.writeback();
}
} else {
// --- 3D billboard with rotation matrix (asm 0x7B2E00-0x7B2F41) ---