Add texture blitting optimization
This commit is contained in:
parent
f12e67346d
commit
c010746897
4 changed files with 79 additions and 11 deletions
12
TODO.md
12
TODO.md
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
35
journal.txt
35
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:
|
technique:
|
||||||
results:
|
results:
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue