particle: V4 vertex store, inline for unroll — ~42% total reduction

- V4 store (vmovups) writes xyz+color as one 16-byte op instead of 4
  scalar stores. Unaligned but still 1 μop on modern CPUs.
- inline for unrolls 4-vertex loops, letting LLVM schedule stores
  across vertices and fill pipeline bubbles.
- Hoisted world_pos to locals to prevent array re-reads.
- A/B: BASELINE ~433ms → CUSTOM ~299ms (~31% per-period reduction).
  Total from original: 520ms → 299ms = 42% reduction.
This commit is contained in:
MarcelineVQ
2026-03-23 22:47:50 -07:00
parent 379fcb62eb
commit 5373686bd4
+29 -25
View File
@@ -189,14 +189,12 @@ const VBState = struct {
// All strides are 24 except normal (0, shared global).
// Write contiguously when stride == 24 and layout matches.
if (s.pos_stride == 24 and s.color_ptr == s.pos + 12 and s.texcoord == s.pos + 16) {
// Fast path: single contiguous 24-byte vertex write
const p: [*]u32 = @ptrFromInt(s.pos);
p[0] = @bitCast(px); // x
p[1] = @bitCast(py); // y
p[2] = @bitCast(pz); // z
p[3] = color; // color at +12
p[4] = @bitCast(tu); // u at +16
p[5] = @bitCast(tv); // v at +20
// Fast path: contiguous 24-byte vertex. V4 store for xyz+color (16 bytes),
// then 2 scalar stores for uv (8 bytes). Unaligned V4 store via vmovups.
const xyzc = V4{ px, py, pz, @bitCast(color) };
@as(*align(1) V4, @ptrFromInt(s.pos)).* = xyzc;
wf32(s.pos + 16, tu);
wf32(s.pos + 20, tv);
s.pos += 24;
s.color_ptr += 24;
s.texcoord += 24;
@@ -478,18 +476,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)
// 4 vertices with cached VB state to avoid pointer re-reads.
// Unrolled — inline for lets LLVM schedule stores across vertices.
{
var vs = VBState.load(vb);
var loop_off: u32 = 0;
while (loop_off < 0x20) : (loop_off += 8) {
const wpx = world_pos[0];
const wpy = world_pos[1];
const wpz = world_pos[2];
inline for (0..4) |i| {
const off: u32 = @intCast(i * 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],
@mulAdd(f32, sprite_scale, rf32(G.billboard_offsets_x + off), wpx),
@mulAdd(f32, sprite_scale, rf32(G.billboard_offsets_y + off), wpy),
wpz,
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),
@mulAdd(f32, rf32(G.sprite_tex_u + off + 8), tex_scale_u, tex_u_base),
@mulAdd(f32, rf32(G.sprite_tex_v + off + 8), tex_scale_v, tex_v_base),
);
}
vs.writeback();
@@ -538,17 +539,20 @@ export fn renderParticleSprites_SSE(emitter: u32, particle_data: u32, vertex_buf
{
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);
const wpx = world_pos[0];
const wpy = world_pos[1];
const wpz = world_pos[2];
inline for (0..4) |i| {
const off: u32 = @intCast(i * 8);
const ox = rf32(G.billboard_offsets_x + off);
const oy = rf32(G.billboard_offsets_y + 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],
@mulAdd(f32, ox, scaled_cos, wpx) - oy * scaled_sin,
@mulAdd(f32, oy, scaled_cos, @mulAdd(f32, ox, scaled_sin, wpy)),
wpz,
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),
@mulAdd(f32, rf32(G.sprite_tex_u + off + 8), tex_scale_u, tex_u_base),
@mulAdd(f32, rf32(G.sprite_tex_v + off + 8), tex_scale_v, tex_v_base),
);
}
vs.writeback();