From c029f40bc79e8d892006b53358655b3dc7a38749 Mon Sep 17 00:00:00 2001 From: catangent Date: Wed, 13 Aug 2025 00:10:26 +0100 Subject: [PATCH] feat: parallelise chunkgen, model generation and uploading --- build.zig | 2 + raylib | 2 +- src/main.zig | 125 ++++++++++++++++++++++++++++++-------- src/world/chunk.zig | 5 +- src/world/world_state.zig | 60 ++++++++++++++---- znoise | 2 +- 6 files changed, 150 insertions(+), 46 deletions(-) diff --git a/build.zig b/build.zig index 417eb5e..5ae6581 100644 --- a/build.zig +++ b/build.zig @@ -8,6 +8,8 @@ pub fn build(b: *std.Build) void { const rl = b.dependency("raylib", .{ .target = target, .optimize = optimize, + //.config = "-DSUPPORT_CUSTOM_FRAME_CONTROL", + .config = @as([]const u8, "-DSUPPORT_CUSTOM_FRAME_CONTROL"), }); break :raylib rl; }; diff --git a/raylib b/raylib index 77e6260..f740d09 160000 --- a/raylib +++ b/raylib @@ -1 +1 @@ -Subproject commit 77e626060d3c5419c539edfa4fe4f268f9015ade +Subproject commit f740d0941f1bf73077bb369c161bc673e27b735a diff --git a/src/main.zig b/src/main.zig index 3c247b0..5f756f1 100644 --- a/src/main.zig +++ b/src/main.zig @@ -14,6 +14,43 @@ const TILE_TEXTURE_RESOLUTION = 16; const debug = true; +const FPS_HISTORY_LENGTH = 30; +var time: struct { + current: f64 = 0.0, + previous: f64 = 0.0, + target: f64 = 1.0/165.0, + frameCounter: u64 = 0, + fpsHistory: [FPS_HISTORY_LENGTH]f64 = .{0.0} ** FPS_HISTORY_LENGTH, + + pub fn deltaTime(self: @This()) f64 { + return self.current - self.previous; + } + + pub fn fps(self: *@This()) f64 { + self.fpsHistory[self.frameCounter % FPS_HISTORY_LENGTH] = 1/(self.current - self.previous); + if(self.frameCounter < FPS_HISTORY_LENGTH) return 0; + + var result: f64 = 0.0; + for(self.fpsHistory) |entry| { + result += entry; + } + result /= FPS_HISTORY_LENGTH; + return result; + } + + pub fn update(self: *@This()) void { + self.previous = self.current; + self.current = raylib.GetTime(); + + if (self.deltaTime() < self.target){ + raylib.WaitTime(self.target - self.deltaTime()); + self.current = raylib.GetTime(); + } + + self.frameCounter += 1; + } +} = .{}; + pub fn drawCameraPosition(camera: raylib.Camera3D, x: i32, y: i32) !void { var buf: [256:0]u8 = undefined; @@ -25,6 +62,17 @@ pub fn drawCameraPosition(camera: raylib.Camera3D, x: i32, y: i32) !void { raylib.DrawText(slice, x, y, 20, raylib.YELLOW); } +pub fn drawFmtText(comptime fmt: []const u8, args: anytype, x: i32, y: i32) !void { + var buf: [256:0]u8 = undefined; + + const slice = try std.fmt.bufPrintZ( + &buf, + fmt, + args, + ); + + raylib.DrawText(slice, x, y, 20, raylib.BLUE); +} pub fn moveCamera(camera: *raylib.Camera3D, vec: raylib.Vector3) void { camera.position = v3.add(camera.position, vec); @@ -34,17 +82,13 @@ pub fn moveCamera(camera: *raylib.Camera3D, vec: raylib.Vector3) void { pub fn main() !void { if (!debug) raylib.SetTraceLogLevel(raylib.LOG_ERROR); - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var gpa = std.heap.GeneralPurposeAllocator(.{.thread_safe = true}){}; const allocator = gpa.allocator(); defer { const status = gpa.deinit(); if (status == .leak) std.debug.print("MEMORY LEAK DETECTED!!!!!!!!!!!!!!!!!!!!!!\n", .{}) else std.debug.print("no leaks detected.\n", .{}); } - var thread_pool: std.Thread.Pool = undefined; - try thread_pool.init(.{.allocator = allocator}); - defer thread_pool.deinit(); - raylib.SetConfigFlags(raylib.FLAG_WINDOW_RESIZABLE | raylib.FLAG_FULLSCREEN_MODE); const display = raylib.GetCurrentMonitor(); @@ -79,21 +123,26 @@ pub fn main() !void { defer raylib.UnloadShader(shader); raylib.SetShaderValue(shader, raylib.GetShaderLocation(shader, "textureTiling"), &.{ @as(f32, @floatFromInt(tile_columns)), @as(f32, @floatFromInt(tile_rows)) }, raylib.SHADER_UNIFORM_VEC2); - var world_state: WorldState = WorldState.init(thread_pool, allocator); + var thread_pool: std.Thread.Pool = undefined; + try thread_pool.init(.{.allocator = allocator}); + defer thread_pool.deinit(); + + var global_graphics_mutex: std.Thread.Mutex = undefined; + + var world_state: WorldState = WorldState.init(&thread_pool, allocator); defer world_state.deinit(); - for (0..10) |x| for (0..10) |z| { - _ = try world_state.generateChunk(.{@intCast(x), 0, @intCast(z)}); - _ = try world_state.generateChunkModel(.{@intCast(x), 0, @intCast(z)}, tile_rows, tile_columns, texture, ambient_occlusion_texture, shader); + for (0..50) |x| for (0..50) |z| { + try world_state.queueLoadChunk(.{@intCast(x), 0, @intCast(z)}, tile_rows, tile_columns, texture, ambient_occlusion_texture, shader, &global_graphics_mutex); }; while (!raylib.WindowShouldClose()) { - raylib.ClearBackground(raylib.BLACK); + raylib.PollInputEvents(); const right = v3.neg(v3.nor(v3.cross(camera.up, v3.sub(camera.target, camera.position)))); const forward = v3.cross(right, v3.neg(camera.up)); - const speed = @as(f32, if (raylib.IsKeyDown(raylib.KEY_LEFT_CONTROL)) 25 else 5) * raylib.GetFrameTime(); + const speed = @as(f32, if (raylib.IsKeyDown(raylib.KEY_LEFT_CONTROL)) 25 else 5) * time.deltaTime(); var movement = v3.new(0, 0, 0); if (raylib.IsKeyDown(raylib.KEY_SPACE)) movement.y += 1; @@ -104,7 +153,7 @@ pub fn main() !void { if (raylib.IsKeyDown(raylib.KEY_D)) movement = v3.add(movement, right); if (raylib.IsKeyDown(raylib.KEY_A)) movement = v3.sub(movement, right); - moveCamera(&camera, v3.scl(v3.nor(movement), speed)); + moveCamera(&camera, v3.scl(v3.nor(movement), @floatCast(speed))); const delta = raylib.GetMouseDelta(); // on the first mouse movement, for some reason mouse delta is way too large, so we just ignore too large deltas @@ -120,26 +169,48 @@ pub fn main() !void { } } - raylib.BeginDrawing(); - defer raylib.EndDrawing(); - { - raylib.BeginMode3D(camera); - defer raylib.EndMode3D(); + global_graphics_mutex.lock(); + defer global_graphics_mutex.unlock(); + raylib.MakeContextCurrent(); + defer raylib.DropContextCurrent(); + + raylib.ClearBackground(raylib.BLACK); - world_state.chunks_access_mutex.lock(); - var chunks_iterator = world_state.chunks.valueIterator(); - while (chunks_iterator.next()) |entry| { - if (entry.*.model) |model| { - const model_position = v3.new(@floatFromInt(entry.position[0]*16), @floatFromInt(entry.position[1]*16), @floatFromInt(entry.position[2]*16)); - raylib.DrawChunkModel(model, model_position, 0.5, raylib.WHITE); + { + raylib.BeginDrawing(); + defer raylib.EndDrawing(); + + var chunk_count: u32 = 0; + var shown_count: u32 = 0; + + { + raylib.BeginMode3D(camera); + defer raylib.EndMode3D(); + + world_state.chunks_access_mutex.lock(); + defer world_state.chunks_access_mutex.unlock(); + + var chunks_iterator = world_state.chunks.valueIterator(); + while (chunks_iterator.next()) |entry_ptr| { + const entry = entry_ptr.*; + if (entry.*.model) |model| { + const model_position = v3.new(@floatFromInt(entry.position[0]*16), @floatFromInt(entry.position[1]*16), @floatFromInt(entry.position[2]*16)); + raylib.DrawChunkModel(model, model_position, 0.5, raylib.WHITE); + shown_count += 1; + } + chunk_count += 1; + } } + + try drawFmtText("fps: {d:.0}", .{time.fps()}, 10, 10); + try drawCameraPosition(camera, 10, 30); + try drawFmtText("chunks shown: {}, chunks total: {}", .{shown_count, chunk_count}, 10, 50); } - world_state.chunks_access_mutex.unlock(); + + raylib.SwapScreenBuffer(); } - - raylib.DrawFPS(10, 10); - try drawCameraPosition(camera, 10, 30); + time.update(); } } diff --git a/src/world/chunk.zig b/src/world/chunk.zig index d7bf7a0..2f63290 100644 --- a/src/world/chunk.zig +++ b/src/world/chunk.zig @@ -387,10 +387,7 @@ pub const Chunk = struct { var raw_quads = try scanForRawQuads(chunk); defer raw_quads.deinit(); - var mesh = packMeshFromRawQuads(raw_quads, tile_columns, tile_rows); - - raylib.UploadChunkMesh(@ptrCast(&mesh), false); - + const mesh = packMeshFromRawQuads(raw_quads, tile_columns, tile_rows); return mesh; } }; diff --git a/src/world/world_state.zig b/src/world/world_state.zig index 3fa1b33..0fab3b9 100644 --- a/src/world/world_state.zig +++ b/src/world/world_state.zig @@ -2,6 +2,7 @@ const std = @import("std"); const Thread = std.Thread; const Allocator = std.mem.Allocator; const AutoHashMap = std.AutoHashMap; +const ArrayList = std.ArrayList; const Chunk = @import("chunk.zig").Chunk; const chunk_generators = @import("chunk_generators.zig"); @@ -10,28 +11,29 @@ const raylib_helper = @import("../lib_helpers/raylib_helper.zig"); const raylib = raylib_helper.raylib; pub const WorldStateError = error{ - ChunkNotGeneratedError, +ChunkNotGeneratedError, }; pub const ChunkEntry = struct { + mutex: Thread.Mutex = .{}, chunk: ?Chunk = null, model: ?raylib.ChunkModel = null, position: @Vector(3, i64), }; pub const WorldState = struct { - pool: Thread.Pool = undefined, + pool: *Thread.Pool = undefined, allocator: Allocator = undefined, // TODO: we can do better than a hashmap and a mutex - chunks: AutoHashMap(@Vector(3, i64), ChunkEntry) = undefined, + chunks: AutoHashMap(@Vector(3, i64), *ChunkEntry) = undefined, chunks_access_mutex: Thread.Mutex = .{}, - pub fn init(global_pool: Thread.Pool, allocator: Allocator) WorldState { + pub fn init(global_pool: *Thread.Pool, allocator: Allocator) WorldState { var world_state: WorldState = undefined; world_state.pool = global_pool; world_state.allocator = allocator; - world_state.chunks = AutoHashMap(@Vector(3, i64), ChunkEntry).init(allocator); + world_state.chunks = AutoHashMap(@Vector(3, i64), *ChunkEntry).init(allocator); return world_state; } @@ -44,33 +46,65 @@ pub const WorldState = struct { if(entry.*.model) |model| { raylib.UnloadChunkModel(model); } + self.allocator.destroy(entry.*); } self.chunks.deinit(); + self.* = undefined; + } + + pub fn queueLoadChunk(self: *WorldState, pos: @Vector(3, i64), tile_rows: u32, tile_columns: u32, texture: raylib.Texture, ambient_occlusion_texture: raylib.Texture, shader: raylib.Shader, global_graphics_mutex: *Thread.Mutex) !void { + try self.pool.spawn(completeChunk, .{self, pos, tile_rows, tile_columns, texture, ambient_occlusion_texture, shader, global_graphics_mutex}); + } + + pub fn completeChunk(self: *WorldState, pos: @Vector(3, i64), tile_rows: u32, tile_columns: u32, texture: raylib.Texture, ambient_occlusion_texture: raylib.Texture, shader: raylib.Shader, global_graphics_mutex: *Thread.Mutex) void { + _ = generateChunk(self, pos) catch |err| std.debug.print("error while generating chunk: {}", .{err}); + _ = generateChunkModel(self, pos, tile_rows, tile_columns, texture, ambient_occlusion_texture, shader, global_graphics_mutex) catch |err| std.debug.print("error while generating chunk model: {}", .{err}); } pub fn generateChunk(self: *WorldState, pos: @Vector(3, i64)) !Chunk { const chunk = try chunk_generators.createChunk(self.allocator, pos); + const entry_ptr = try self.allocator.create(ChunkEntry); + entry_ptr.* = .{.chunk = chunk, .position = pos}; self.chunks_access_mutex.lock(); - try self.chunks.put(pos, .{.chunk = chunk, .position = pos}); + try self.chunks.put(pos, entry_ptr); self.chunks_access_mutex.unlock(); return chunk; } - pub fn generateChunkModel(self: *WorldState, pos: @Vector(3, i64), tile_rows: u32, tile_columns: u32, texture: raylib.Texture, ambient_occlusion_texture: raylib.Texture, shader: raylib.Shader) !raylib.ChunkModel { + pub fn generateChunkModel(self: *WorldState, pos: @Vector(3, i64), tile_rows: u32, tile_columns: u32, texture: raylib.Texture, ambient_occlusion_texture: raylib.Texture, shader: raylib.Shader, global_graphics_mutex: *Thread.Mutex) !raylib.ChunkModel { self.chunks_access_mutex.lock(); - const chunk_entry = (try self.chunks.getOrPutValue(pos, .{.position = pos})).value_ptr; + const chunk_entry = blk: { + const result = try self.chunks.getOrPut(pos); + if(!result.found_existing) { + const entry_ptr = try self.allocator.create(ChunkEntry); + entry_ptr.* = .{.position = pos}; + result.value_ptr.* = entry_ptr; + } + break :blk result.value_ptr.*; + }; self.chunks_access_mutex.unlock(); const chunk = chunk_entry.chunk; if(chunk == null) return WorldStateError.ChunkNotGeneratedError; - const model = raylib.LoadChunkModelFromMesh(try chunk.?.createMesh(tile_rows, tile_columns)); - model.materials[0].maps[raylib.MATERIAL_MAP_DIFFUSE].texture = texture; - model.materials[0].shader = shader; + var mesh = try chunk.?.createMesh(tile_rows, tile_columns); + const model = blk: { + global_graphics_mutex.lock(); + defer global_graphics_mutex.unlock(); + raylib.MakeContextCurrent(); + defer raylib.DropContextCurrent(); - model.materials[0].shader.locs[raylib.SHADER_LOC_MAP_DIFFUSE+1] = raylib.GetShaderLocation(shader, "occlusionMap"); - raylib.SetShaderValueTexture(shader, model.materials[0].shader.locs[raylib.SHADER_LOC_MAP_DIFFUSE+1], ambient_occlusion_texture); model.materials[0].maps[raylib.MATERIAL_MAP_DIFFUSE+1].texture = ambient_occlusion_texture; + raylib.UploadChunkMesh(@ptrCast(&mesh), false); + const model = raylib.LoadChunkModelFromMesh(mesh); + model.materials[0].maps[raylib.MATERIAL_MAP_DIFFUSE].texture = texture; + model.materials[0].shader = shader; + + model.materials[0].shader.locs[raylib.SHADER_LOC_MAP_DIFFUSE+1] = raylib.GetShaderLocation(shader, "occlusionMap"); + raylib.SetShaderValueTexture(shader, model.materials[0].shader.locs[raylib.SHADER_LOC_MAP_DIFFUSE+1], ambient_occlusion_texture); + model.materials[0].maps[raylib.MATERIAL_MAP_DIFFUSE+1].texture = ambient_occlusion_texture; + break :blk model; + }; chunk_entry.model = model; return model; } diff --git a/znoise b/znoise index 96f9458..7672458 160000 --- a/znoise +++ b/znoise @@ -1 +1 @@ -Subproject commit 96f9458c2da975a8bf1cdf95e819c7b070965198 +Subproject commit 76724581c99be0b2f6aa43eb8b63d6f27bada27e