// sandbox stress test // measures entity count ceiling on weak hardware const std = @import("std"); pub const SCREEN_WIDTH: u32 = 1280; pub const SCREEN_HEIGHT: u32 = 1024; const CENTER_X: f32 = @as(f32, @floatFromInt(SCREEN_WIDTH)) / 2.0; const CENTER_Y: f32 = @as(f32, @floatFromInt(SCREEN_HEIGHT)) / 2.0; const RESPAWN_THRESHOLD: f32 = 10.0; const ENTITY_SPEED: f32 = 2.0; pub const Entity = struct { x: f32, y: f32, vx: f32, vy: f32, color: u32, }; pub const MAX_ENTITIES: usize = 10_000_000; pub const Entities = struct { items: []Entity, count: usize, var backing: [MAX_ENTITIES]Entity = undefined; pub fn init() Entities { return .{ .items = &backing, .count = 0, }; } pub fn add(self: *Entities, rng: *std.Random) void { if (self.count >= MAX_ENTITIES) return; self.items[self.count] = spawnAtEdge(rng); self.count += 1; } pub fn remove(self: *Entities, n: usize) void { if (n >= self.count) { self.count = 0; } else { self.count -= n; } } pub fn reset(self: *Entities) void { self.count = 0; } }; pub fn update(entities: *Entities, rng: *std.Random) void { for (entities.items[0..entities.count]) |*entity| { // apply velocity entity.x += entity.vx; entity.y += entity.vy; // check if reached center const dx = entity.x - CENTER_X; const dy = entity.y - CENTER_Y; const dist = @sqrt(dx * dx + dy * dy); if (dist < RESPAWN_THRESHOLD) { // respawn at random edge entity.* = spawnAtEdge(rng); } } } pub fn spawnAtEdge(rng: *std.Random) Entity { // pick random edge: 0=top, 1=bottom, 2=left, 3=right const edge = rng.intRangeAtMost(u8, 0, 3); const screen_w = @as(f32, @floatFromInt(SCREEN_WIDTH)); const screen_h = @as(f32, @floatFromInt(SCREEN_HEIGHT)); var x: f32 = undefined; var y: f32 = undefined; switch (edge) { 0 => { // top x = rng.float(f32) * screen_w; y = 0; }, 1 => { // bottom x = rng.float(f32) * screen_w; y = screen_h; }, 2 => { // left x = 0; y = rng.float(f32) * screen_h; }, 3 => { // right x = screen_w; y = rng.float(f32) * screen_h; }, else => unreachable, } // velocity toward center const dx = CENTER_X - x; const dy = CENTER_Y - y; const dist = @sqrt(dx * dx + dy * dy); const vx = (dx / dist) * ENTITY_SPEED; const vy = (dy / dist) * ENTITY_SPEED; // random RGB color const r = rng.int(u8); const g = rng.int(u8); const b = rng.int(u8); const color: u32 = (@as(u32, r) << 16) | (@as(u32, g) << 8) | @as(u32, b); return .{ .x = x, .y = y, .vx = vx, .vy = vy, .color = color, }; } // tests test "Entity struct has correct size" { // 5 fields: x, y, vx, vy (f32 = 4 bytes each) + color (u32 = 4 bytes) // 20 bytes total as per plan try std.testing.expectEqual(@as(usize, 20), @sizeOf(Entity)); } test "Entity can be created with initial values" { const entity = Entity{ .x = 100.0, .y = 200.0, .vx = 1.5, .vy = -0.5, .color = 0x00FFFF, }; try std.testing.expectEqual(@as(f32, 100.0), entity.x); try std.testing.expectEqual(@as(f32, 200.0), entity.y); try std.testing.expectEqual(@as(f32, 1.5), entity.vx); try std.testing.expectEqual(@as(f32, -0.5), entity.vy); try std.testing.expectEqual(@as(u32, 0x00FFFF), entity.color); } test "Entities init starts empty" { const entities = Entities.init(); try std.testing.expectEqual(@as(usize, 0), entities.count); } test "Entities add increases count" { var entities = Entities.init(); var prng = std.Random.DefaultPrng.init(12345); var rng = prng.random(); entities.add(&rng); try std.testing.expectEqual(@as(usize, 1), entities.count); entities.add(&rng); try std.testing.expectEqual(@as(usize, 2), entities.count); } test "Entities remove decreases count" { var entities = Entities.init(); var prng = std.Random.DefaultPrng.init(12345); var rng = prng.random(); // add 5 for (0..5) |_| entities.add(&rng); try std.testing.expectEqual(@as(usize, 5), entities.count); // remove 2 entities.remove(2); try std.testing.expectEqual(@as(usize, 3), entities.count); // remove more than count entities.remove(100); try std.testing.expectEqual(@as(usize, 0), entities.count); } test "Entities reset clears all" { var entities = Entities.init(); var prng = std.Random.DefaultPrng.init(12345); var rng = prng.random(); for (0..10) |_| entities.add(&rng); try std.testing.expectEqual(@as(usize, 10), entities.count); entities.reset(); try std.testing.expectEqual(@as(usize, 0), entities.count); } test "spawnAtEdge creates entity on screen edge" { var prng = std.Random.DefaultPrng.init(12345); var rng = prng.random(); // spawn several and check they're on edges for (0..20) |_| { const entity = spawnAtEdge(&rng); const on_left = entity.x == 0; const on_right = entity.x == @as(f32, @floatFromInt(SCREEN_WIDTH)); const on_top = entity.y == 0; const on_bottom = entity.y == @as(f32, @floatFromInt(SCREEN_HEIGHT)); try std.testing.expect(on_left or on_right or on_top or on_bottom); } } test "spawnAtEdge velocity points toward center" { var prng = std.Random.DefaultPrng.init(12345); var rng = prng.random(); for (0..20) |_| { const entity = spawnAtEdge(&rng); // after one step, should be closer to center const dist_before = @sqrt((entity.x - CENTER_X) * (entity.x - CENTER_X) + (entity.y - CENTER_Y) * (entity.y - CENTER_Y)); const new_x = entity.x + entity.vx; const new_y = entity.y + entity.vy; const dist_after = @sqrt((new_x - CENTER_X) * (new_x - CENTER_X) + (new_y - CENTER_Y) * (new_y - CENTER_Y)); try std.testing.expect(dist_after < dist_before); } } test "spawnAtEdge velocity has correct speed" { var prng = std.Random.DefaultPrng.init(12345); var rng = prng.random(); const entity = spawnAtEdge(&rng); const speed = @sqrt(entity.vx * entity.vx + entity.vy * entity.vy); try std.testing.expectApproxEqAbs(ENTITY_SPEED, speed, 0.001); } test "update moves entities by velocity" { var entities = Entities.init(); // manually place an entity entities.items[0] = .{ .x = 100.0, .y = 100.0, .vx = 2.0, .vy = 3.0, .color = 0x00FFFF, }; entities.count = 1; var prng = std.Random.DefaultPrng.init(12345); var rng = prng.random(); update(&entities, &rng); try std.testing.expectApproxEqAbs(@as(f32, 102.0), entities.items[0].x, 0.001); try std.testing.expectApproxEqAbs(@as(f32, 103.0), entities.items[0].y, 0.001); } test "update respawns entity at edge when reaching center" { var entities = Entities.init(); // place entity very close to center (within threshold) entities.items[0] = .{ .x = CENTER_X + 1.0, .y = CENTER_Y + 1.0, .vx = -1.0, .vy = -1.0, .color = 0x00FFFF, }; entities.count = 1; var prng = std.Random.DefaultPrng.init(12345); var rng = prng.random(); // after update, entity moves to center and should respawn update(&entities, &rng); // should now be on an edge const entity = entities.items[0]; const on_left = entity.x == 0; const on_right = entity.x == @as(f32, @floatFromInt(SCREEN_WIDTH)); const on_top = entity.y == 0; const on_bottom = entity.y == @as(f32, @floatFromInt(SCREEN_HEIGHT)); try std.testing.expect(on_left or on_right or on_top or on_bottom); } // 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) + 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, .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); }