diff --git a/TODO.md b/TODO.md index 6bd412e..4960b57 100644 --- a/TODO.md +++ b/TODO.md @@ -29,9 +29,15 @@ findings (AMD Radeon test): based on phase 2 results: -- [ ] batch rendering, instancing (render-bound confirmed) -- [ ] ~~if cpu-bound: SIMD, struct-of-arrays, multithreading~~ (not needed) -- [ ] re-test after each change +- [x] batch rendering via texture blitting (10x improvement) +- [x] ~~if cpu-bound: SIMD, struct-of-arrays, multithreading~~ (not needed) +- [x] re-test after each change + +findings: +- texture blitting: pre-render circle to texture, drawTexture() per entity +- baseline: 60fps @ ~5k entities +- optimized: 60fps @ ~50k entities, 30fps @ 100k entities +- see journal.txt for detailed benchmarks ## phase 4: add collision diff --git a/build.zig.zon b/build.zig.zon index 05b879b..830a9fb 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,7 +1,7 @@ .{ .name = .lofivor, .version = "0.0.1", - .fingerprint = 0x9de74c19d031cdc9, + .fingerprint = 0x74960d9e2a8050e2, .dependencies = .{ .raylib_zig = .{ .url = "git+https://github.com/raylib-zig/raylib-zig?ref=devel#a4d18b2d1cf8fdddec68b5b084535fca0475f466", diff --git a/journal.txt b/journal.txt index b3c3105..f2825f3 100644 --- a/journal.txt +++ b/journal.txt @@ -20,7 +20,40 @@ analysis: linear scaling, each drawCircle = separate GPU draw call --- -optimization 1: [pending] +optimization 1: texture blitting +-------------------------------- +technique: pre-render circle to 16x16 texture, drawTexture() per entity +code: sandbox_main.zig:109-124, 170-177 + +benchmark2.log results: +- 60fps stable: ~50,000 entities +- 60fps breaks: ~52,000-55,000 entities (18-21ms frame) +- 23k entities: 16.7ms frame (still vsync-locked) +- 59k entities: 20.6ms frame + +extended benchmark (benchmark3): +- 50k entities: 16.7ms (vsync-locked, briefly touches 19ms) +- 60k entities: 20.7ms +- 70k entities: 23.7ms +- 80k entities: 30.1ms +- 100k entities: 33-37ms (~30fps) + +comparison to baseline: +- baseline broke 60fps at ~5,000 entities +- texture blitting breaks at ~50,000 entities +- ~10x improvement in entity ceiling + +analysis: raylib batches texture draws internally when using same texture. +individual drawCircle() = separate draw call each. drawTexture() with same +texture = batched into fewer GPU calls. + +notes: render_ms stays ~16-18ms up to ~50k, then scales roughly linearly. +at 100k entities we're at ~30fps which is still playable. update loop +remains negligible (<0.6ms even at 100k). + +--- + +optimization 2: [pending] ------------------------- technique: results: diff --git a/src/sandbox_main.zig b/src/sandbox_main.zig index 04d684a..14e3849 100644 --- a/src/sandbox_main.zig +++ b/src/sandbox_main.zig @@ -12,6 +12,10 @@ const SCREEN_HEIGHT = sandbox.SCREEN_HEIGHT; const BG_COLOR = rl.Color{ .r = 10, .g = 10, .b = 18, .a = 255 }; const CYAN = rl.Color{ .r = 0, .g = 255, .b = 255, .a = 255 }; +// entity rendering +const ENTITY_RADIUS: f32 = 4.0; +const TEXTURE_SIZE: i32 = 16; // must be >= 2 * radius + // logging thresholds const TARGET_FRAME_MS: f32 = 16.7; // 60fps const THRESHOLD_MARGIN: f32 = 2.0; // hysteresis margin to avoid bounce @@ -102,11 +106,35 @@ const BenchmarkLogger = struct { } }; +fn createCircleTexture() ?rl.Texture2D { + // create a render texture to draw circle into + const target = rl.loadRenderTexture(TEXTURE_SIZE, TEXTURE_SIZE) catch return null; + + rl.beginTextureMode(target); + rl.clearBackground(rl.Color{ .r = 0, .g = 0, .b = 0, .a = 0 }); // transparent + rl.drawCircle( + @divTrunc(TEXTURE_SIZE, 2), + @divTrunc(TEXTURE_SIZE, 2), + ENTITY_RADIUS, + CYAN, + ); + rl.endTextureMode(); + + return target.texture; +} + pub fn main() !void { rl.initWindow(@intCast(SCREEN_WIDTH), @intCast(SCREEN_HEIGHT), "lofivor sandbox"); defer rl.closeWindow(); rl.setTargetFPS(60); + // create circle texture for batched rendering + const circle_texture = createCircleTexture() orelse { + std.debug.print("failed to create circle texture\n", .{}); + return; + }; + defer rl.unloadTexture(circle_texture); + var entities = sandbox.Entities.init(); var prng = std.Random.DefaultPrng.init(@intCast(std.time.timestamp())); var rng = prng.random(); @@ -140,13 +168,14 @@ pub fn main() !void { rl.beginDrawing(); rl.clearBackground(BG_COLOR); - // draw entities as filled circles + // draw entities using pre-rendered circle texture + const half_size = @as(f32, @floatFromInt(TEXTURE_SIZE)) / 2.0; for (entities.items[0..entities.count]) |entity| { - rl.drawCircle( - @intFromFloat(entity.x), - @intFromFloat(entity.y), - 4, - CYAN, + rl.drawTexture( + circle_texture, + @intFromFloat(entity.x - half_size), + @intFromFloat(entity.y - half_size), + rl.Color.white, // tint (white = use original colors) ); }