diff --git a/build.zig b/build.zig index df78f2c..dd0e8b9 100644 --- a/build.zig +++ b/build.zig @@ -8,6 +8,7 @@ pub fn build(b: *std.Build) void { .target = target, .optimize = optimize, .linux_display_backend = .X11, + .opengl_version = .gl_4_3, }); // sandbox executable diff --git a/src/sandbox.zig b/src/sandbox.zig index f28a1c4..382dffa 100644 --- a/src/sandbox.zig +++ b/src/sandbox.zig @@ -286,3 +286,35 @@ 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) +pub const GpuEntity = extern struct { + x: f32, + y: f32, + color: u32, +}; + +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)); +} + +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 + .color = 0x00FFFF, + }; + + const gpu_entity = GpuEntity{ + .x = entity.x, + .y = entity.y, + .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); +} diff --git a/src/sandbox_main.zig b/src/sandbox_main.zig index f6fc53f..310ff5a 100644 --- a/src/sandbox_main.zig +++ b/src/sandbox_main.zig @@ -5,6 +5,7 @@ const std = @import("std"); const rl = @import("raylib"); const sandbox = @import("sandbox.zig"); const ui = @import("ui.zig"); +const SsboRenderer = @import("ssbo_renderer.zig").SsboRenderer; const SCREEN_WIDTH = sandbox.SCREEN_WIDTH; const SCREEN_HEIGHT = sandbox.SCREEN_HEIGHT; @@ -26,7 +27,7 @@ const HEARTBEAT_INTERVAL: f32 = 10.0; // seconds between periodic logs // auto-benchmark settings const BENCH_RAMP_INTERVAL: f32 = 2.0; // seconds between entity ramps -const BENCH_RAMP_AMOUNT: usize = 10_000; // entities added per ramp +const BENCH_RAMP_AMOUNT: usize = 50_000; // entities added per ramp const BENCH_EXIT_THRESHOLD_MS: f32 = 25.0; // exit when frame time exceeds this const BENCH_EXIT_SUSTAIN: f32 = 1.0; // must stay above threshold for this long @@ -154,14 +155,19 @@ pub fn main() !void { // parse args var bench_mode = false; var use_instancing = false; + var use_ssbo = false; var args = try std.process.argsWithAllocator(std.heap.page_allocator); defer args.deinit(); _ = args.skip(); // skip program name while (args.next()) |arg| { if (std.mem.eql(u8, arg, "--bench")) { bench_mode = true; + use_ssbo = true; // bench mode uses SSBO by default } else if (std.mem.eql(u8, arg, "--gpu")) { use_instancing = true; + use_ssbo = false; // explicit --gpu overrides SSBO + } else if (std.mem.eql(u8, arg, "--ssbo")) { + use_ssbo = true; } } @@ -221,6 +227,21 @@ pub fn main() !void { if (transforms) |t| std.heap.page_allocator.free(t); } + // SSBO rendering setup (only if --ssbo flag) + var ssbo_renderer: ?SsboRenderer = null; + + if (use_ssbo) { + ssbo_renderer = SsboRenderer.init(circle_texture) orelse { + std.debug.print("failed to initialize SSBO renderer\n", .{}); + return; + }; + std.debug.print("SSBO instancing mode enabled\n", .{}); + } + + defer { + if (ssbo_renderer) |*r| r.deinit(); + } + // load UI font (embedded) const font_data = @embedFile("verdanab.ttf"); const ui_font = rl.loadFontFromMemory(".ttf", font_data, 32, null) catch { @@ -301,8 +322,11 @@ pub fn main() !void { rl.beginDrawing(); rl.clearBackground(BG_COLOR); - if (use_instancing) { - // GPU instancing path + if (use_ssbo) { + // SSBO instanced rendering path (12 bytes per entity) + ssbo_renderer.?.render(&entities); + } else if (use_instancing) { + // GPU instancing path (64 bytes per entity) const xforms = transforms.?; // fill transforms array with entity positions for (entities.items[0..entities.count], 0..) |entity, i| {