particle: faithful RenderParticleSprites recreation, A/B verified
Full recreation of RenderParticleSprites (0x7B2A50, 2688 bytes) in particle_sse.zig. All 5 code paths: 2D billboard, 3D billboard, 2D+rotation (sin/cos), 3D+rotation (axis-angle matrix), tail particles. Key fixes during verification: - colorCtx address: removed Ghidra's spurious -0x12 offset - calcColor arg2: pass raw u32 from emitter+0x1A8, not truncated - Texture coord lookups: +8 offset to match assembly's eax increment between position and texcoord reads in the vertex loop Verified in-game: particles render identically in CUSTOM vs BASELINE. Next: optimize with SSE (inline calcColor, V4 vertex math).
This commit is contained in:
+495
-132
@@ -4,171 +4,534 @@
|
||||
//! Functions are exported and called via `extern fn` from transform44.zig detour hooks.
|
||||
//!
|
||||
//! Assembly references: decompiled/asm_RenderParticleSprites.txt,
|
||||
//! asm_calculateColorValues.txt, asm_SetupParticleRendering.txt
|
||||
//! decomp_RenderParticleSprites.c, decomp_particle_helpers.c
|
||||
//!
|
||||
//! Faithful recreation of RenderParticleSprites (0x7B2A50, 2688 bytes).
|
||||
//! Every section verified against assembly. Optimization comes later —
|
||||
//! first priority is byte-identical output.
|
||||
|
||||
const std = @import("std");
|
||||
const V4 = @Vector(4, f32);
|
||||
const V4i = @Vector(4, i32);
|
||||
const CC = std.builtin.CallingConvention;
|
||||
const TC: CC = .{ .x86_thiscall = .{} };
|
||||
const FC: CC = .{ .x86_fastcall = .{} };
|
||||
|
||||
inline fn rf32(addr: u32) f32 {
|
||||
return @as(*align(1) const f32, @ptrFromInt(addr)).*;
|
||||
}
|
||||
|
||||
inline fn ri32(addr: u32) i32 {
|
||||
return @as(*align(1) const i32, @ptrFromInt(addr)).*;
|
||||
}
|
||||
|
||||
inline fn ru8(addr: u32) u8 {
|
||||
return @as(*const u8, @ptrFromInt(addr)).*;
|
||||
}
|
||||
|
||||
inline fn ru16(addr: u32) u16 {
|
||||
return @as(*align(1) const u16, @ptrFromInt(addr)).*;
|
||||
}
|
||||
inline fn ru32(addr: u32) u32 {
|
||||
return @as(*align(1) const u32, @ptrFromInt(addr)).*;
|
||||
}
|
||||
|
||||
inline fn wf32(addr: u32, val: f32) void {
|
||||
@as(*align(1) f32, @ptrFromInt(addr)).* = val;
|
||||
}
|
||||
|
||||
inline fn wu32(addr: u32, val: u32) void {
|
||||
@as(*align(1) u32, @ptrFromInt(addr)).* = val;
|
||||
}
|
||||
|
||||
inline fn wu8(addr: u32, val: u8) void {
|
||||
@as(*u8, @ptrFromInt(addr)).* = val;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// calculateColorValues (0x7B9B10)
|
||||
// Emitter struct offsets (this = ECX = ParticleSystemRenderer*)
|
||||
// Assembly-derived from [edi+N] references in asm_RenderParticleSprites.txt
|
||||
// =============================================================================
|
||||
//
|
||||
// __thiscall(ECX=colorCtx, stack: time, scale, outColor, outAlpha1, outAlpha2, outFloat)
|
||||
// RET 0x18 (6 stack params)
|
||||
//
|
||||
// ColorCtx layout:
|
||||
// +0x00..0x03: base color bytes [B, G, R, A] (4 bytes)
|
||||
// +0x04: delta_alpha (i32)
|
||||
// +0x08: delta_red (i32)
|
||||
// +0x0C: delta_green (i32)
|
||||
// +0x10: delta_blue (i32)
|
||||
// +0x14: alpha1_base (i32)
|
||||
// +0x18: alpha1_delta (i32)
|
||||
// +0x1C: alpha2_base (i32)
|
||||
// +0x20: alpha2_delta (i32)
|
||||
// +0x24: float_base (f32)
|
||||
// +0x28: float_scale (f32)
|
||||
// +0x2C: time_base (f32)
|
||||
// +0x30: time_scale (f32)
|
||||
// +0x50: alpha_power (f32, 1.0 = linear, else calls pow)
|
||||
//
|
||||
// Algorithm:
|
||||
// t = (time - ctx.timeBase) * ctx.timeScale * CONST1 + CONST2
|
||||
// For each color channel (A,R,G,B):
|
||||
// val = (float)delta * t + (float)base_byte
|
||||
// alpha channel only: val *= scale
|
||||
// val += MAGIC (float-to-byte trick constant at 0x8029CC)
|
||||
// outColor[ch] = (byte)(float_bits >> 14)
|
||||
// outFloat = t * ctx.floatScale + ctx.floatBase
|
||||
// For alpha outputs:
|
||||
// if ctx.alphaPower == 1.0: linear interp
|
||||
// else: pow(t * alphaPower, ...) path
|
||||
//
|
||||
// The "float bits >> 14" is a classic fast float-to-byte: add a large power-of-2
|
||||
// magic number so the integer value sits in the mantissa bits, then extract.
|
||||
const E = struct {
|
||||
const uvCoordScale: u32 = 0x0C; // shift count for texture V index
|
||||
const texScaleU: u32 = 0x10; // texture U scale factor
|
||||
const texScaleV: u32 = 0x14; // texture V scale factor
|
||||
const colorCtxBase: u32 = 0xBC; // base of color/orientation data array
|
||||
const rotation_offset: u32 = 0x18C; // rotation angle scale
|
||||
const particle_count_mask: u32 = 0x19C; // mask for particle index extraction
|
||||
const orientation_base: u32 = 0x1A8; // orientation data ptr
|
||||
const flags: u32 = 0x1AC; // rendering flags (u32)
|
||||
const particle_size: u32 = 0x1B0; // base particle size
|
||||
const visibility: u32 = 0x1B4; // visibility threshold
|
||||
const alpha_scale: u32 = 0x1B8; // alpha scale offset
|
||||
const alpha_value: u32 = 0x1C0; // alpha value
|
||||
const extra_scale: u32 = 0x264; // additional scale factor
|
||||
const rotation_axis: u32 = 0x284; // rotation axis vec3 (for 3D rotation path)
|
||||
const tail_distance: u32 = 0xB4; // tail particle max distance
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Global addresses
|
||||
// =============================================================================
|
||||
const G = struct {
|
||||
const float_1_0: u32 = 0x7FF9D8; // 1.0f
|
||||
const zero_threshold: u32 = 0x7FFD74; // 0.0f (collision plane zero)
|
||||
const max_particle_size: u32 = 0x7FFE58; // max clamp for particle size
|
||||
const rounding_magic: u32 = 0x8029CC; // float-to-byte magic number
|
||||
const depth_buffer: u32 = 0xCF58F0; // g_particleDepthBuffer (128 floats)
|
||||
const world_matrix: u32 = 0xCF5B68; // g_worldMatrix (4x4)
|
||||
const light_dir_x: u32 = 0xCF5878; // g_lightDirectionX
|
||||
const light_dir_y: u32 = 0xCF587C; // g_lightDirectionY
|
||||
const light_dir_z: u32 = 0xCF5880; // g_lightDirectionZ
|
||||
// Billboard vertex offset lookup tables (4 vertices × {x,y} = 8 floats each table)
|
||||
const billboard_offsets_x: u32 = 0x87D714; // g_billboardVertexOffsetsX (stride 8 per vertex)
|
||||
const billboard_offsets_y: u32 = 0x87D718; // g_billboardVertexOffsetsY
|
||||
// 3D billboard offset table (4 vertices × {x,y,z} = 12 floats)
|
||||
const billboard_3d: u32 = 0x87D738; // g_transformedVertex table (stride 8 per vertex for 2D ref)
|
||||
const billboard_3d_base: u32 = 0xCF5B30; // secondary 3D table base (-4/0/+4 indexed)
|
||||
// Sprite texture offset lookup (4 vertices × {u,v})
|
||||
const sprite_tex_u: u32 = 0x87D72C; // texture U offsets (stride 8)
|
||||
const sprite_tex_v: u32 = 0x87D730; // texture V offsets (stride 8)
|
||||
// Tail particle texture data
|
||||
const tail_tex_u0: u32 = 0x87D744; // tail tex offsets per vertex
|
||||
const tail_tex_v0: u32 = 0x87D748;
|
||||
const tail_tex_u1: u32 = 0x87D74C;
|
||||
const tail_tex_v1: u32 = 0x87D750;
|
||||
const tail_threshold: u32 = 0x80C744; // minimum velocity squared for tail rendering
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Game function pointers (called from RenderParticleSprites)
|
||||
// =============================================================================
|
||||
|
||||
const CC = std.builtin.CallingConvention;
|
||||
const TC: CC = .{ .x86_thiscall = .{} };
|
||||
/// calculateParticleColorAndScale (0x7B9B10)
|
||||
/// __thiscall(ECX=colorCtx, stack: time, scale, outColor, outAlpha1, outAlpha2, outFloat)
|
||||
const calcColorFn = *const fn (u32, u32, u32, u32, u32, u32, u32) callconv(TC) void;
|
||||
const calcColor: calcColorFn = @ptrFromInt(0x7B9B10);
|
||||
|
||||
const std = @import("std");
|
||||
/// UpdateLightingOffset / setupRenderState (0x58A230)
|
||||
/// __cdecl() → returns ptr (used to check [ret+0x1C])
|
||||
const setupRenderFn = *const fn () callconv(.{ .x86_stdcall = .{} }) u32;
|
||||
const setupRender: setupRenderFn = @ptrFromInt(0x58A230);
|
||||
|
||||
/// SSE replacement for calculateColorValues.
|
||||
/// Thiscall: ECX=ctx, stack params: time(f32), scale(f32), outColor(ptr), outAlpha1(ptr), outAlpha2(ptr), outFloat(ptr)
|
||||
export fn calcColorValues_SSE(
|
||||
ctx: u32,
|
||||
time_bits: u32,
|
||||
scale_bits: u32,
|
||||
out_color: u32,
|
||||
out_alpha1: u32,
|
||||
out_alpha2: u32,
|
||||
out_float: u32,
|
||||
) callconv(TC) void {
|
||||
const time: f32 = @bitCast(time_bits);
|
||||
const scale: f32 = @bitCast(scale_bits);
|
||||
/// transformVector3ByMatrix4x4 (0x7BCA80)
|
||||
/// __fastcall(ECX=out, EDX=vec3, stack=mat4x4ptr), RET 0x4
|
||||
const transformVec3Fn = *const fn (u32, u32, u32) callconv(FC) u32;
|
||||
const transformVec3: transformVec3Fn = @ptrFromInt(0x7BCA80);
|
||||
|
||||
// Step 1: Compute interpolation parameter t
|
||||
const t = (time - rf32(ctx + 0x2C)) * rf32(ctx + 0x30) * rf32(0x808AAC) + rf32(0x807A3C);
|
||||
/// createAxisAngleRotationMatrix3x3 (0x7BE490)
|
||||
/// __fastcall(ECX=outMat9, EDX=axisVec3, stack=angle_f32, isNormalized_char), RET 0x8
|
||||
/// Note: angle is passed as f32 bits on stack, isNormalized as u32 (char in low byte)
|
||||
const createRotMatFn = *const fn (u32, u32, u32, u32) callconv(FC) u32;
|
||||
const createRotMat: createRotMatFn = @ptrFromInt(0x7BE490);
|
||||
|
||||
// Step 2: Compute 4 color channels
|
||||
// Load base bytes and deltas
|
||||
const base_a: f32 = @floatFromInt(@as(i32, ru8(ctx + 3)));
|
||||
const base_r: f32 = @floatFromInt(@as(i32, ru8(ctx + 2)));
|
||||
const base_g: f32 = @floatFromInt(@as(i32, ru8(ctx + 1)));
|
||||
const base_b: f32 = @floatFromInt(@as(i32, ru8(ctx + 0)));
|
||||
/// transformVector4ByMatrix4x4 (0x7BCB40)
|
||||
/// __fastcall(ECX=out, EDX=vec3, stack=mat4x4ptr), RET 0x4
|
||||
const transformVec4Fn = *const fn (u32, u32, u32) callconv(FC) u32;
|
||||
const transformVec4: transformVec4Fn = @ptrFromInt(0x7BCB40);
|
||||
|
||||
const delta_a: f32 = @floatFromInt(ri32(ctx + 0x04));
|
||||
const delta_r: f32 = @floatFromInt(ri32(ctx + 0x08));
|
||||
const delta_g: f32 = @floatFromInt(ri32(ctx + 0x0C));
|
||||
const delta_b: f32 = @floatFromInt(ri32(ctx + 0x10));
|
||||
// =============================================================================
|
||||
// VertexBuffers struct — the vertexBuffers parameter
|
||||
// =============================================================================
|
||||
// vertexBuffers is a float** (array of pointers):
|
||||
// [0] = vertexPos ptr (3 floats per vertex: x,y,z)
|
||||
// [1] = normalPtr (3 floats: light direction)
|
||||
// [2] = colorPtr (1 u32: packed BGRA color)
|
||||
// [3] = texCoordPtr (2 floats: u,v)
|
||||
// [4] = vertexStride (bytes to advance vertex ptr)
|
||||
// [5] = normalStride (bytes to advance normal ptr)
|
||||
// [6] = colorStride (bytes to advance color ptr)
|
||||
// [7] = texCoordStride (bytes to advance texcoord ptr)
|
||||
// [8] = vertexCount (incremented per vertex emitted)
|
||||
const VB = struct {
|
||||
const pos: u32 = 0;
|
||||
const normal: u32 = 4;
|
||||
const color: u32 = 8;
|
||||
const texcoord: u32 = 12;
|
||||
const pos_stride: u32 = 16;
|
||||
const normal_stride: u32 = 20;
|
||||
const color_stride: u32 = 24;
|
||||
const texcoord_stride: u32 = 28;
|
||||
const count: u32 = 32;
|
||||
};
|
||||
|
||||
const magic: f32 = rf32(0x8029CC);
|
||||
|
||||
// Alpha channel: (delta * t + base) * scale + magic
|
||||
const alpha_f = @mulAdd(f32, delta_a, t, base_a) * scale + magic;
|
||||
// RGB channels: delta * t + base + magic (no scale)
|
||||
const red_f = @mulAdd(f32, delta_r, t, base_r) + magic;
|
||||
const green_f = @mulAdd(f32, delta_g, t, base_g) + magic;
|
||||
const blue_f = @mulAdd(f32, delta_b, t, base_b) + magic;
|
||||
|
||||
// Extract bytes via float-bits >> 14 trick
|
||||
const alpha_byte: u8 = @truncate(@as(u32, @bitCast(alpha_f)) >> 14);
|
||||
const red_byte: u8 = @truncate(@as(u32, @bitCast(red_f)) >> 14);
|
||||
const green_byte: u8 = @truncate(@as(u32, @bitCast(green_f)) >> 14);
|
||||
const blue_byte: u8 = @truncate(@as(u32, @bitCast(blue_f)) >> 14);
|
||||
|
||||
// Store color bytes: [B, G, R, A] at outColor
|
||||
wu8(out_color + 0, blue_byte);
|
||||
wu8(out_color + 1, green_byte);
|
||||
wu8(out_color + 2, red_byte);
|
||||
wu8(out_color + 3, alpha_byte);
|
||||
|
||||
// Step 3: Float output = t * ctx.floatScale + ctx.floatBase
|
||||
wf32(out_float, @mulAdd(f32, t, rf32(ctx + 0x28), rf32(ctx + 0x24)));
|
||||
|
||||
// Step 4: Alpha outputs
|
||||
const alpha_power = ru32(ctx + 0x50);
|
||||
if (alpha_power == 0x3F800000) {
|
||||
// Fast path: alphaPower == 1.0 (linear)
|
||||
const a1_val = @mulAdd(f32, @as(f32, @floatFromInt(ri32(ctx + 0x18))), t, @as(f32, @floatFromInt(ri32(ctx + 0x14)))) + magic;
|
||||
const a2_val = @mulAdd(f32, @as(f32, @floatFromInt(ri32(ctx + 0x20))), t, @as(f32, @floatFromInt(ri32(ctx + 0x1C)))) + magic;
|
||||
|
||||
wu32(out_alpha1, (@as(u32, @bitCast(a1_val)) >> 14) & 0xFF);
|
||||
wu32(out_alpha2, (@as(u32, @bitCast(a2_val)) >> 14) & 0xFF);
|
||||
} else {
|
||||
// Slow path: pow scaling. Call game's pow function.
|
||||
// 0x73F90A: __cdecl pow — takes ST(0)=base, ST(1)=exponent, returns ST(0)
|
||||
// t_scaled = pow(t * alphaPower, ???)
|
||||
// For now, fall back to scalar computation matching the original exactly.
|
||||
const ap: f32 = @bitCast(alpha_power);
|
||||
const t_scaled = t * ap;
|
||||
|
||||
// The original calls 0x73F90A with ST(0)=t_scaled, ST(1)=loaded from [0x8015B8] (qword)
|
||||
// This is __CIpow (MSVC intrinsic pow) — ST(1)=exponent (from 0x8015B8), ST(0)=base
|
||||
// We need the exponent constant. For now use @exp2/@log2 to compute pow.
|
||||
// Actually: the original loads FLD qword [0x8015B8] THEN calls __CIpow.
|
||||
// __CIpow expects ST(0)=x, ST(1)=y, computes x^y. So: pow(t_scaled, const_at_8015B8).
|
||||
// The constant at 0x8015B8 is a f64. We read it and use std.math.pow.
|
||||
const exp_val: f64 = @as(*align(1) const f64, @ptrFromInt(0x8015B8)).*;
|
||||
const t_pow: f32 = @floatCast(std.math.pow(f64, @as(f64, t_scaled), exp_val));
|
||||
|
||||
const a1_delta: f32 = @floatFromInt(ri32(ctx + 0x18));
|
||||
const a1_base: f32 = @floatFromInt(ri32(ctx + 0x14));
|
||||
const a1_val = @mulAdd(f32, a1_delta, t_pow, a1_base) + magic;
|
||||
|
||||
const a2_delta: f32 = @floatFromInt(ri32(ctx + 0x20));
|
||||
const a2_base: f32 = @floatFromInt(ri32(ctx + 0x1C));
|
||||
const a2_val = @mulAdd(f32, a2_delta, t_pow, a2_base) + magic;
|
||||
|
||||
wu32(out_alpha1, (@as(u32, @bitCast(a1_val)) >> 14) & 0xFF);
|
||||
wu32(out_alpha2, (@as(u32, @bitCast(a2_val)) >> 14) & 0xFF);
|
||||
}
|
||||
/// Emit one vertex: write position, normal (light dir), color, texcoord, advance pointers.
|
||||
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));
|
||||
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));
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// RenderParticleSprites (0x7B2A50)
|
||||
// __thiscall(ECX=emitter, stack=particleData, vertexBuffers), RET 0x8
|
||||
// Returns: 0 (culled) or 1 (rendered)
|
||||
//
|
||||
// Faithful recreation from assembly + Ghidra decompilation.
|
||||
// =============================================================================
|
||||
export fn renderParticleSprites_SSE(emitter: u32, particle_data: u32, vertex_buffers: u32) callconv(TC) u32 {
|
||||
const pd = particle_data; // particleData pointer (float*)
|
||||
const vb = vertex_buffers; // vertexBuffers pointer (float**)
|
||||
|
||||
// =========================================================================
|
||||
// Section 1: Early-out visibility checks (asm 0x7B2A5E-0x7B2B0B)
|
||||
// =========================================================================
|
||||
|
||||
// Check visibility threshold: emitter+0x1B4 < 1.0
|
||||
var depth_index: u32 = 0;
|
||||
|
||||
if (rf32(emitter + E.visibility) < rf32(G.float_1_0) or
|
||||
rf32(emitter + E.alpha_value) != rf32(G.zero_threshold))
|
||||
{
|
||||
// Compute clamped particle size
|
||||
var clamped_size: f32 = rf32(emitter + E.particle_size) * rf32(pd + 0x1C);
|
||||
if (clamped_size < rf32(G.zero_threshold)) {
|
||||
clamped_size = rf32(G.zero_threshold);
|
||||
} else if (clamped_size >= rf32(G.max_particle_size)) {
|
||||
clamped_size = rf32(G.max_particle_size);
|
||||
}
|
||||
// Float-to-index conversion: add magic, extract bits, combine with particle data hash
|
||||
const size_with_magic = clamped_size + rf32(G.rounding_magic);
|
||||
depth_index = ((@as(u32, @bitCast(size_with_magic)) >> 14) + (particle_data >> 5)) & 0x7F;
|
||||
}
|
||||
|
||||
// Depth buffer cull check
|
||||
if (rf32(emitter + E.visibility) < rf32(G.float_1_0) and
|
||||
rf32(emitter + E.visibility) < rf32(G.depth_buffer + depth_index * 4))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Section 2: Calculate color and scale (asm 0x7B2B0E-0x7B2B41)
|
||||
// =========================================================================
|
||||
|
||||
// Compute colorCtx address: emitter + 0xBC + byte(particleData[0x0C]) * 96
|
||||
// Assembly: movzx eax,byte[ebx+0xC]; lea ecx,[eax+eax*2]; shl ecx,5; lea ecx,[ecx+edi+0xBC]
|
||||
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);
|
||||
|
||||
var color_value: u32 = 0;
|
||||
var color_data1: u32 = 0;
|
||||
var color_data2: u32 = 0;
|
||||
var sprite_scale: f32 = undefined;
|
||||
|
||||
// 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));
|
||||
|
||||
// =========================================================================
|
||||
// Section 3: Render state setup (asm 0x7B2B46)
|
||||
// =========================================================================
|
||||
|
||||
const render_state = setupRender();
|
||||
|
||||
// =========================================================================
|
||||
// Section 4: Color byte swizzle (asm 0x7B2B4B-0x7B2B6E)
|
||||
// If render_state[0x1C] == 1, swizzle BGRA → RGBA
|
||||
// =========================================================================
|
||||
|
||||
if (ru32(render_state + 0x1C) == 1) {
|
||||
const b0: u8 = @truncate(color_value);
|
||||
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);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Section 5: Alpha/size scaling (asm 0x7B2B71-0x7B2BB1)
|
||||
// =========================================================================
|
||||
|
||||
if (rf32(emitter + E.alpha_value) != rf32(G.zero_threshold)) {
|
||||
sprite_scale = (rf32(G.depth_buffer + depth_index * 4) * rf32(emitter + E.alpha_value) +
|
||||
rf32(emitter + E.alpha_scale)) * sprite_scale;
|
||||
}
|
||||
|
||||
// Read full flags as u32 for subsequent checks
|
||||
const full_flags = ru32(emitter + E.flags);
|
||||
|
||||
// Extra scale factor if flag 0x200 set
|
||||
if ((full_flags & 0x200) != 0) {
|
||||
sprite_scale = sprite_scale * rf32(emitter + E.extra_scale);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Section 6: Position transform (asm 0x7B2BB4-0x7B2BC3)
|
||||
// Transform particle world position through view matrix
|
||||
// =========================================================================
|
||||
|
||||
var world_pos: [3]f32 = undefined;
|
||||
_ = transformVec3(@intFromPtr(&world_pos), pd, G.world_matrix);
|
||||
|
||||
// =========================================================================
|
||||
// Section 7: Branch on flag 0x4 — sprite vs tail rendering
|
||||
// =========================================================================
|
||||
|
||||
if ((full_flags & 0x4) == 0) {
|
||||
// No sprite rendering — jump to tail check at section 9
|
||||
} else {
|
||||
// =====================================================================
|
||||
// Section 7a: Texture coordinate setup (asm 0x7B2BD5-0x7B2C05)
|
||||
// =====================================================================
|
||||
|
||||
const count_mask = ru32(emitter + E.particle_count_mask) - 1;
|
||||
const tex_index_raw = color_data1;
|
||||
const tex_u_index: f32 = @floatFromInt(count_mask & tex_index_raw);
|
||||
const shift_count: u5 = @truncate(ru32(emitter + E.uvCoordScale));
|
||||
const tex_v_raw: i32 = @as(i32, @bitCast(tex_index_raw)) >> shift_count;
|
||||
const tex_v_index: f32 = @floatFromInt(tex_v_raw);
|
||||
|
||||
const tex_u_base = tex_u_index * rf32(emitter + E.texScaleU);
|
||||
const tex_v_base = tex_v_index * rf32(emitter + E.texScaleV);
|
||||
const tex_scale_u = rf32(emitter + E.texScaleU);
|
||||
const tex_scale_v = rf32(emitter + E.texScaleV);
|
||||
|
||||
// Check rotation angle: if emitter+0x18C == 0.0, no rotation needed
|
||||
const has_rotation = rf32(emitter + E.rotation_offset) != rf32(G.zero_threshold);
|
||||
|
||||
if (!has_rotation) {
|
||||
// =================================================================
|
||||
// Section 8a: No rotation — check 2D vs 3D billboard
|
||||
// =================================================================
|
||||
|
||||
if ((full_flags & 0x2000) == 0) {
|
||||
// --- 2D billboard (asm 0x7B2D10-0x7B2DD5) ---
|
||||
// 4 vertices. Position uses [eax+0x87D714/718], but eax is incremented
|
||||
// by 8 BEFORE the Y read and texcoord reads. So texcoords use eax+8.
|
||||
// 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);
|
||||
}
|
||||
} 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));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// =================================================================
|
||||
// Section 8b: With rotation
|
||||
// =================================================================
|
||||
|
||||
// Compute rotation angle: emitter+0x18C * particleData[7]
|
||||
var rot_angle = rf32(emitter + E.rotation_offset) * rf32(pd + 0x1C);
|
||||
|
||||
// Negate if flags indicate (asm 0x7B2DE8-0x7B2DF4)
|
||||
const flag_byte: i8 = @bitCast(@as(u8, @truncate(full_flags >> 8)));
|
||||
if (flag_byte < 0 and (particle_data & 0x20) != 0) {
|
||||
rot_angle = -rot_angle;
|
||||
}
|
||||
|
||||
if ((full_flags & 0x2000) == 0) {
|
||||
// --- 2D billboard with sin/cos rotation (asm 0x7B2F49-0x7B303B) ---
|
||||
const cos_val = @cos(rot_angle);
|
||||
const sin_val = @sin(rot_angle);
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
// --- 3D billboard with rotation matrix (asm 0x7B2E00-0x7B2F41) ---
|
||||
// Build rotation matrix from axis + angle, then transform each vertex
|
||||
var rot_mat: [9]f32 = undefined;
|
||||
_ = 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Section 9: Tail particle rendering (asm 0x7B3041-0x7B34C5)
|
||||
// Flag 0x8 in emitter+0x1AC: velocity-based trail
|
||||
// =========================================================================
|
||||
|
||||
if ((ru8(emitter + E.flags) & 0x8) != 0) {
|
||||
// Tail particles: compute from velocity direction
|
||||
const count_mask = ru32(emitter + E.particle_count_mask) - 1;
|
||||
const tex_index_raw = color_data2;
|
||||
const tex_u_index: f32 = @floatFromInt(count_mask & tex_index_raw);
|
||||
const shift_count: u5 = @truncate(ru32(emitter + E.uvCoordScale));
|
||||
const tex_v_raw: i32 = @as(i32, @bitCast(tex_index_raw)) >> shift_count;
|
||||
const tail_tex_u = tex_u_index * rf32(emitter + E.texScaleU);
|
||||
const tail_tex_v: f32 = @as(f32, @floatFromInt(tex_v_raw)) * rf32(emitter + E.texScaleV);
|
||||
|
||||
// Negate velocity vector
|
||||
const neg_vel_x: f32 = -rf32(pd + 0x10); // particleData[4]
|
||||
const neg_vel_y: f32 = -rf32(pd + 0x14); // particleData[5]
|
||||
const neg_vel_z: f32 = -rf32(pd + 0x18); // particleData[6]
|
||||
|
||||
// Get tail distance, clamp by particleData[7] if flag 0x1 set
|
||||
var tail_dist: f32 = @bitCast(ru32(emitter + E.tail_distance));
|
||||
const tail_flag_byte = ru8(emitter + E.flags + 2); // byte at +0x1AE
|
||||
if ((tail_flag_byte & 0x1) != 0 and rf32(pd + 0x1C) < tail_dist) {
|
||||
tail_dist = rf32(pd + 0x1C);
|
||||
}
|
||||
|
||||
// Transform negated velocity through world matrix
|
||||
var neg_vel = [3]f32{ neg_vel_x, neg_vel_y, neg_vel_z };
|
||||
var transformed_vel: [4]f32 = undefined;
|
||||
_ = transformVec4(@intFromPtr(&transformed_vel), @intFromPtr(&neg_vel), G.world_matrix);
|
||||
|
||||
const tx = tail_dist * transformed_vel[0];
|
||||
const ty = tail_dist * transformed_vel[1];
|
||||
const cos_sq = tx * tx + ty * ty;
|
||||
|
||||
if (cos_sq >= rf32(G.tail_threshold)) {
|
||||
// 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_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);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ extern fn rotateMatrixByAxisAngle(u32, u32, u32, u32) void;
|
||||
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;
|
||||
|
||||
/// Thiscall wrapper for the SSE implementation. Lives here (baseline SSE2 unit)
|
||||
/// so LLVM can't inline transformImpl_SSE's alignment into the thiscall frame.
|
||||
@@ -925,7 +926,14 @@ fn glyphDetour(a: u32, b: u32, c: u32, d: u32) callconv(hook.cc.fastcall) ?*anyo
|
||||
return ret;
|
||||
}
|
||||
fn particleDetour(a: u32, b: u32, c: u32, d: u32) callconv(hook.cc.fastcall) ?*anyopaque {
|
||||
// a=ECX(emitter), b=EDX(unused), c=particleData, d=vertexBuffers
|
||||
const s = rdtsc();
|
||||
if (ab_use_custom) {
|
||||
const result = renderParticleSprites_SSE(a, c, d);
|
||||
prof.particle_cycles +|= rdtsc() - s;
|
||||
prof.particle_calls +|= 1;
|
||||
return @ptrFromInt(result);
|
||||
}
|
||||
const ret = particle_hook.callOriginal(.{ a, b, c, d });
|
||||
prof.particle_cycles +|= rdtsc() - s;
|
||||
prof.particle_calls +|= 1;
|
||||
|
||||
Reference in New Issue
Block a user