From 5fd82000cfa901223166e6befeda19d6f138a674 Mon Sep 17 00:00:00 2001 From: Jared Miller Date: Wed, 17 Dec 2025 09:43:11 -0500 Subject: [PATCH] Add GpuEntity struct expansion --- src/sandbox.zig | 45 ++++++++++++++++++++++++++++++++++++----- src/shaders/entity.vert | 3 ++- src/ssbo_renderer.zig | 5 +++-- 3 files changed, 45 insertions(+), 8 deletions(-) diff --git a/src/sandbox.zig b/src/sandbox.zig index bb1a4e0..1509a3d 100644 --- a/src/sandbox.zig +++ b/src/sandbox.zig @@ -287,34 +287,69 @@ test "update respawns entity at edge when reaching center" { try std.testing.expect(on_left or on_right or on_top or on_bottom); } -// GPU entity for SSBO rendering (position + color only, no velocity) +// GPU entity for SSBO rendering (16 bytes, matches compute shader layout) pub const GpuEntity = extern struct { x: f32, y: f32, + packed_vel: i32, // vx high 16 bits, vy low 16 bits (fixed-point 8.8) color: u32, }; +// pack two f32 velocities into a single i32 (fixed-point 8.8 format) +pub fn packVelocity(vx: f32, vy: f32) i32 { + const vx_fixed: i16 = @intFromFloat(std.math.clamp(vx * 256.0, -32768.0, 32767.0)); + const vy_fixed: i16 = @intFromFloat(std.math.clamp(vy * 256.0, -32768.0, 32767.0)); + return (@as(i32, vx_fixed) << 16) | (@as(i32, vy_fixed) & 0xFFFF); +} + test "GpuEntity struct has correct size for SSBO" { - // SSBO layout: x(4) + y(4) + color(4) = 12 bytes - try std.testing.expectEqual(@as(usize, 12), @sizeOf(GpuEntity)); + // SSBO layout: x(4) + y(4) + packed_vel(4) + color(4) = 16 bytes + try std.testing.expectEqual(@as(usize, 16), @sizeOf(GpuEntity)); } test "GpuEntity can be created from Entity" { const entity = Entity{ .x = 100.0, .y = 200.0, - .vx = 1.5, // ignored for GPU - .vy = -0.5, // ignored for GPU + .vx = 1.5, + .vy = -0.5, .color = 0x00FFFF, }; const gpu_entity = GpuEntity{ .x = entity.x, .y = entity.y, + .packed_vel = packVelocity(entity.vx, entity.vy), .color = entity.color, }; try std.testing.expectEqual(@as(f32, 100.0), gpu_entity.x); try std.testing.expectEqual(@as(f32, 200.0), gpu_entity.y); try std.testing.expectEqual(@as(u32, 0x00FFFF), gpu_entity.color); + + // unpack and verify velocity (should round-trip within precision) + const vx_unpacked = @as(f32, @floatFromInt(@as(i16, @truncate(gpu_entity.packed_vel >> 16)))) / 256.0; + const vy_unpacked = @as(f32, @floatFromInt(@as(i16, @truncate(gpu_entity.packed_vel)))) / 256.0; + try std.testing.expectApproxEqAbs(@as(f32, 1.5), vx_unpacked, 0.004); + try std.testing.expectApproxEqAbs(@as(f32, -0.5), vy_unpacked, 0.004); +} + +test "packVelocity round-trips correctly" { + // test positive values + const packed1 = packVelocity(2.0, 1.5); + const vx1 = @as(f32, @floatFromInt(@as(i16, @truncate(packed1 >> 16)))) / 256.0; + const vy1 = @as(f32, @floatFromInt(@as(i16, @truncate(packed1)))) / 256.0; + try std.testing.expectApproxEqAbs(@as(f32, 2.0), vx1, 0.004); + try std.testing.expectApproxEqAbs(@as(f32, 1.5), vy1, 0.004); + + // test negative values + const packed2 = packVelocity(-1.0, -2.5); + const vx2 = @as(f32, @floatFromInt(@as(i16, @truncate(packed2 >> 16)))) / 256.0; + const vy2 = @as(f32, @floatFromInt(@as(i16, @truncate(packed2)))) / 256.0; + try std.testing.expectApproxEqAbs(@as(f32, -1.0), vx2, 0.004); + try std.testing.expectApproxEqAbs(@as(f32, -2.5), vy2, 0.004); + + // test zero + const packed3 = packVelocity(0.0, 0.0); + try std.testing.expectEqual(@as(i32, 0), packed3); } diff --git a/src/shaders/entity.vert b/src/shaders/entity.vert index 590882d..8778466 100644 --- a/src/shaders/entity.vert +++ b/src/shaders/entity.vert @@ -4,10 +4,11 @@ layout(location = 0) in vec2 position; layout(location = 1) in vec2 texCoord; -// entity data from SSBO +// entity data from SSBO (16 bytes, matches compute shader layout) struct Entity { float x; float y; + int packedVel; // vx high 16 bits, vy low 16 bits (fixed-point 8.8), unused in vertex shader uint color; }; diff --git a/src/ssbo_renderer.zig b/src/ssbo_renderer.zig index 49c697e..ee10337 100644 --- a/src/ssbo_renderer.zig +++ b/src/ssbo_renderer.zig @@ -99,7 +99,7 @@ pub const SsboRenderer = struct { rl.gl.rlSetVertexAttribute(1, 2, rl.gl.rl_float, false, 4 * @sizeOf(f32), 2 * @sizeOf(f32)); rl.gl.rlEnableVertexAttribute(1); - // create SSBO for entity data (12 bytes per entity, 1M entities = 12MB) + // create SSBO for entity data (16 bytes per entity, 1M entities = 16MB) const ssbo_size: u32 = @intCast(sandbox.MAX_ENTITIES * @sizeOf(sandbox.GpuEntity)); const ssbo_id = rl.gl.rlLoadShaderBuffer(ssbo_size, null, rl.gl.rl_dynamic_draw); if (ssbo_id == 0) { @@ -142,7 +142,7 @@ pub const SsboRenderer = struct { // flush raylib's internal render batch before our custom GL calls rl.gl.rlDrawRenderBatchActive(); - // copy entity data to GPU buffer (position + color only) + // copy entity data to GPU buffer (position + packed velocity + color) { const zone = ztracy.ZoneN(@src(), "ssbo_copy"); defer zone.End(); @@ -150,6 +150,7 @@ pub const SsboRenderer = struct { self.gpu_buffer[i] = .{ .x = entity.x, .y = entity.y, + .packed_vel = sandbox.packVelocity(entity.vx, entity.vy), .color = entity.color, }; }