particle: Ghidra decompilations, particle_sse.zig scaffold, bench

- Ghidra C decompilation of RenderParticleSprites (422 lines) and 5
  helper functions (calculateColorValues, matVec3Transform, etc.)
- particle_sse.zig with calcColorValues_SSE (10.7x bench but cache-miss
  bound in-game — needs inlining into full function replacement)
- Bench harness for calcColorValues with correctness check
- build.zig: particle_sse as separate ReleaseFast compilation unit
- colorDetour reverted to pass-through (SSE has no in-game effect due
  to L1 cache misses on scattered ColorCtx structs)
This commit is contained in:
MarcelineVQ
2026-03-23 21:56:57 -07:00
parent fec9ac9952
commit d205693dbb
6 changed files with 962 additions and 1 deletions
+97
View File
@@ -51,6 +51,7 @@ extern fn si_vec3Dot(u32, u32) callconv(cc_fc) f64;
extern fn si_translateBoundingVol(u32, u32) callconv(cc_tc) void;
extern fn si_processLinkedListCollision(u32, u32, u32, u32) callconv(cc_fc) u32;
extern fn si_frustumCullBBox(u32, u32, u32) callconv(cc_fc) u32;
extern fn calcColorValues_SSE(u32, u32, u32, u32, u32, u32, u32) callconv(cc_tc) void;
extern fn si_addVec3ToAccumulator(u32, u32) callconv(cc_tc) void;
extern fn si_addToColorAccumulator(u32, u32) callconv(cc_tc) void;
extern fn si_packParticleColor(u32, u32, u32, u32) callconv(cc_tc) void;
@@ -1769,6 +1770,9 @@ pub fn main() void {
}
}
// calcColorValues_SSE -- thiscall(ctx_ECX, time, scale, outColor, outAlpha1, outAlpha2, outFloat)
bench_calcColorValues();
// si_frustumCullBBox -- fastcall(bbox_ECX, flags_EDX, radius_stack) -> u32
bench_frustumCullBBox();
@@ -1779,6 +1783,99 @@ pub fn main() void {
print("\n", .{});
}
fn bench_calcColorValues() void {
// Map pages for global constants used by calculateColorValues
// 0x808AAC and 0x807A3C are in .rdata range (already mapped)
// 0x8029CC is in .rdata range (already mapped)
// 0x8015B8 is in .rdata range (already mapped) — pow exponent constant
// Build fake ColorCtx struct
// Layout: +0x00..0x03 = base bytes [B,G,R,A], +0x04..0x10 = deltas (4×i32),
// +0x14..0x20 = alpha base/delta pairs (4×i32), +0x24 = float_base(f32),
// +0x28 = float_scale(f32), +0x2C = time_base(f32), +0x30 = time_scale(f32),
// +0x50 = alpha_power(f32)
var ctx: [0x54]u8 align(4) = std.mem.zeroes([0x54]u8);
// Base color: BGRA = {100, 150, 200, 220}
ctx[0] = 100; ctx[1] = 150; ctx[2] = 200; ctx[3] = 220;
// Deltas (i32): small values
@as(*align(1) i32, @ptrCast(ctx[0x04..0x08])).* = 10;
@as(*align(1) i32, @ptrCast(ctx[0x08..0x0C])).* = -5;
@as(*align(1) i32, @ptrCast(ctx[0x0C..0x10])).* = 8;
@as(*align(1) i32, @ptrCast(ctx[0x10..0x14])).* = -3;
// Alpha base/delta
@as(*align(1) i32, @ptrCast(ctx[0x14..0x18])).* = 200;
@as(*align(1) i32, @ptrCast(ctx[0x18..0x1C])).* = 20;
@as(*align(1) i32, @ptrCast(ctx[0x1C..0x20])).* = 180;
@as(*align(1) i32, @ptrCast(ctx[0x20..0x24])).* = 15;
// Float base/scale
@as(*align(1) f32, @ptrCast(ctx[0x24..0x28])).* = 1.0;
@as(*align(1) f32, @ptrCast(ctx[0x28..0x2C])).* = 0.5;
// Time base/scale
@as(*align(1) f32, @ptrCast(ctx[0x2C..0x30])).* = 0.0;
@as(*align(1) f32, @ptrCast(ctx[0x30..0x34])).* = 1.0;
// Alpha power = 1.0 (linear, fast path)
@as(*align(1) f32, @ptrCast(ctx[0x50..0x54])).* = 1.0;
const time: f32 = 0.5;
const scale: f32 = 1.0;
var out_color_o: [4]u8 = .{0} ** 4;
var out_color_s: [4]u8 = .{0} ** 4;
var out_alpha1_o: u32 = 0;
var out_alpha1_s: u32 = 0;
var out_alpha2_o: u32 = 0;
var out_alpha2_s: u32 = 0;
var out_float_o: f32 = 0;
var out_float_s: f32 = 0;
// Original: __thiscall(ECX=ctx, stack: time, scale, outColor, outAlpha1, outAlpha2, outFloat), RET 0x18
const of = origFn(fn (u32, u32, u32, u32, u32, u32, u32) callconv(cc_tc) void, 0x7B9B10);
of(a(&ctx), @bitCast(time), @bitCast(scale), a(&out_color_o), a(&out_alpha1_o), a(&out_alpha2_o), a(&out_float_o));
calcColorValues_SSE(a(&ctx), @bitCast(time), @bitCast(scale), a(&out_color_s), a(&out_alpha1_s), a(&out_alpha2_s), a(&out_float_s));
// The original returns float in ST(0) which we need to pop to avoid FPU stack leak
// Pop it after each call in the bench loop too
const ok = out_color_o[0] == out_color_s[0] and out_color_o[1] == out_color_s[1] and
out_color_o[2] == out_color_s[2] and out_color_o[3] == out_color_s[3] and
out_alpha1_o == out_alpha1_s and out_alpha2_o == out_alpha2_s and
compareF32(out_float_o, out_float_s);
if (!ok) {
print(" color bytes: orig=[{d},{d},{d},{d}] sse=[{d},{d},{d},{d}]\n", .{
out_color_o[0], out_color_o[1], out_color_o[2], out_color_o[3],
out_color_s[0], out_color_s[1], out_color_s[2], out_color_s[3],
});
print(" alpha1: orig={d} sse={d} alpha2: orig={d} sse={d}\n", .{
out_alpha1_o, out_alpha1_s, out_alpha2_o, out_alpha2_s,
});
print(" float: orig=0x{x} sse=0x{x}\n", .{
@as(u32, @bitCast(out_float_o)), @as(u32, @bitCast(out_float_s)),
});
}
// Original returns float in ST(0) — must pop to avoid FPU stack overflow in bench loop
var t: u64 = std.math.maxInt(u64);
for (0..5) |_| {
const _t0 = rdtsc();
for (0..ITERS) |_| {
of(a(&ctx), @bitCast(time), @bitCast(scale), a(&out_color_o), a(&out_alpha1_o), a(&out_alpha2_o), a(&out_float_o));
// Pop ST(0) to prevent FPU stack overflow
asm volatile ("fstp %%st(0)" ::: "st");
}
const _te = rdtsc() - _t0;
if (_te < t) t = _te;
}
var s: u64 = std.math.maxInt(u64);
for (0..5) |_| {
const _t0 = rdtsc();
for (0..ITERS) |_| {
calcColorValues_SSE(a(&ctx), @bitCast(time), @bitCast(scale), a(&out_color_s), a(&out_alpha1_s), a(&out_alpha2_s), a(&out_float_s));
}
const _te = rdtsc() - _t0;
if (_te < s) s = _te;
}
report("calcColorValues", t, s, ok);
}
fn bench_frustumCullBBox() void {
// Map runtime global pages for view-proj matrices, occlusion buffer, and flags
_ = mapZeroed(0xC7B000, 0x20000); // covers 0xC7B000-0xC7D000+ (matrices, horizon buffer, globals)