Add texture blitting optimization

This commit is contained in:
Jared Tyler Miller 2025-12-14 23:10:30 -05:00 committed by Jared Miller
parent f12e67346d
commit c010746897
4 changed files with 79 additions and 11 deletions

12
TODO.md
View file

@ -29,9 +29,15 @@ findings (AMD Radeon test):
based on phase 2 results: based on phase 2 results:
- [ ] batch rendering, instancing (render-bound confirmed) - [x] batch rendering via texture blitting (10x improvement)
- [ ] ~~if cpu-bound: SIMD, struct-of-arrays, multithreading~~ (not needed) - [x] ~~if cpu-bound: SIMD, struct-of-arrays, multithreading~~ (not needed)
- [ ] re-test after each change - [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 ## phase 4: add collision

View file

@ -1,7 +1,7 @@
.{ .{
.name = .lofivor, .name = .lofivor,
.version = "0.0.1", .version = "0.0.1",
.fingerprint = 0x9de74c19d031cdc9, .fingerprint = 0x74960d9e2a8050e2,
.dependencies = .{ .dependencies = .{
.raylib_zig = .{ .raylib_zig = .{
.url = "git+https://github.com/raylib-zig/raylib-zig?ref=devel#a4d18b2d1cf8fdddec68b5b084535fca0475f466", .url = "git+https://github.com/raylib-zig/raylib-zig?ref=devel#a4d18b2d1cf8fdddec68b5b084535fca0475f466",

View file

@ -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: technique:
results: results:

View file

@ -12,6 +12,10 @@ const SCREEN_HEIGHT = sandbox.SCREEN_HEIGHT;
const BG_COLOR = rl.Color{ .r = 10, .g = 10, .b = 18, .a = 255 }; 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 }; 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 // logging thresholds
const TARGET_FRAME_MS: f32 = 16.7; // 60fps const TARGET_FRAME_MS: f32 = 16.7; // 60fps
const THRESHOLD_MARGIN: f32 = 2.0; // hysteresis margin to avoid bounce 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 { pub fn main() !void {
rl.initWindow(@intCast(SCREEN_WIDTH), @intCast(SCREEN_HEIGHT), "lofivor sandbox"); rl.initWindow(@intCast(SCREEN_WIDTH), @intCast(SCREEN_HEIGHT), "lofivor sandbox");
defer rl.closeWindow(); defer rl.closeWindow();
rl.setTargetFPS(60); 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 entities = sandbox.Entities.init();
var prng = std.Random.DefaultPrng.init(@intCast(std.time.timestamp())); var prng = std.Random.DefaultPrng.init(@intCast(std.time.timestamp()));
var rng = prng.random(); var rng = prng.random();
@ -140,13 +168,14 @@ pub fn main() !void {
rl.beginDrawing(); rl.beginDrawing();
rl.clearBackground(BG_COLOR); 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| { for (entities.items[0..entities.count]) |entity| {
rl.drawCircle( rl.drawTexture(
@intFromFloat(entity.x), circle_texture,
@intFromFloat(entity.y), @intFromFloat(entity.x - half_size),
4, @intFromFloat(entity.y - half_size),
CYAN, rl.Color.white, // tint (white = use original colors)
); );
} }