Skip to content

Commit 30c90f0

Browse files
committed
Add upload thread and non-blocking upload pipeline
1 parent 4320868 commit 30c90f0

7 files changed

Lines changed: 1296 additions & 190 deletions

File tree

src/client/FarHorizonsClient.zig

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -392,12 +392,14 @@ pub const FarHorizonsClient = struct {
392392
}
393393
}
394394

395-
// Main thread only: process completed chunk meshes
395+
// AAA pattern: minimal main thread work
396+
// Heavy upload work (staging, allocations) is done by dedicated upload thread
397+
// tick() only processes ready uploads from upload thread (non-blocking)
396398
if (self.chunk_manager) |*cm| {
397-
cm.beginFrame(self.render_system.getCurrentFrameFence());
399+
cm.beginFrame();
398400
// C2ME-style: flush load queue once per frame, not per tick
399401
cm.flushLoadQueue();
400-
cm.tick();
402+
cm.tick(); // Now minimal: just applies ready uploads
401403
}
402404

403405
const partial_tick: f32 = @floatCast(tick_accumulator / MS_PER_TICK);

src/client/renderer/RenderSystem.zig

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,11 @@ pub const RenderSystem = struct {
563563
return self.graphics_queue;
564564
}
565565

566+
/// Get graphics queue family index (for command pool creation)
567+
pub fn getGraphicsFamily(self: *const Self) u32 {
568+
return self.graphics_family;
569+
}
570+
566571
/// Get the GPU device abstraction for resource creation
567572
pub fn getGpuDevice(self: *Self) ?*GpuDevice {
568573
if (self.gpu_device) |*dev| {

src/client/renderer/buffer/ChunkBufferManager.zig

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,13 @@ pub const ChunkBufferManager = struct {
232232
};
233233
}
234234

235+
/// Advance frame and process deferred frees (for upload thread use)
236+
/// Does NOT touch staging ring - use when only deferred free processing is needed
237+
pub fn advanceFrameAndProcessFrees(self: *Self) void {
238+
self.frame_counter += 1;
239+
self.processDeferredFrees();
240+
}
241+
235242
/// Process deferred frees - frees allocations that are old enough
236243
/// Call this at the start of each frame
237244
fn processDeferredFrees(self: *Self) void {

src/client/renderer/buffer/StagingRing.zig

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,60 @@ pub const StagingRing = struct {
207207
};
208208
}
209209

210+
/// Non-blocking check if a frame's fence is signaled
211+
/// Returns true if fence is signaled (GPU work complete) or no fence exists
212+
pub fn isFenceReady(self: *const Self, frame_index: usize) bool {
213+
const vkGetFenceStatus = vk.vkGetFenceStatus orelse return true;
214+
215+
if (frame_index >= MAX_FRAMES_IN_FLIGHT) return true;
216+
217+
if (self.frame_allocations[frame_index]) |alloc| {
218+
if (alloc.fence != null) {
219+
return vkGetFenceStatus(self.device, alloc.fence) == vk.VK_SUCCESS;
220+
}
221+
}
222+
return true; // No fence means ready
223+
}
224+
225+
/// Non-blocking check if any fence is signaled
226+
pub fn checkFence(self: *const Self, fence: vk.VkFence) bool {
227+
const vkGetFenceStatus = vk.vkGetFenceStatus orelse return true;
228+
if (fence == null) return true;
229+
return vkGetFenceStatus(self.device, fence) == vk.VK_SUCCESS;
230+
}
231+
232+
/// Try to begin a new frame without blocking
233+
/// Returns false if previous frame's fence is not ready (caller should skip)
234+
/// Returns true and advances frame if ready
235+
pub fn tryBeginFrame(self: *Self, frame_fence: vk.VkFence) !bool {
236+
const vkGetFenceStatus = vk.vkGetFenceStatus orelse return error.VulkanFunctionNotLoaded;
237+
238+
const next_frame = (self.current_frame + 1) % MAX_FRAMES_IN_FLIGHT;
239+
240+
// Check if next frame slot's fence is ready (non-blocking)
241+
if (self.frame_allocations[next_frame]) |old_alloc| {
242+
if (old_alloc.fence != null) {
243+
if (vkGetFenceStatus(self.device, old_alloc.fence) != vk.VK_SUCCESS) {
244+
// Fence not ready, caller should skip this frame's staging
245+
return false;
246+
}
247+
}
248+
self.frame_allocations[next_frame] = null;
249+
}
250+
251+
// Fence ready, advance frame
252+
self.current_frame = next_frame;
253+
254+
// Record start of this frame's allocations
255+
self.frame_allocations[self.current_frame] = FrameAllocation{
256+
.start = self.write_pos,
257+
.end = self.write_pos,
258+
.fence = frame_fence,
259+
};
260+
261+
return true;
262+
}
263+
210264
/// Stage data for upload to a destination buffer
211265
/// Returns the offset in the staging buffer where data was written
212266
pub fn stage(
@@ -299,6 +353,19 @@ pub const StagingRing = struct {
299353
self.pending_copies.clearRetainingCapacity();
300354
}
301355

356+
/// Get the current number of pending copies (for tracking before staging)
357+
pub fn getPendingCount(self: *const Self) usize {
358+
return self.pending_copies.items.len;
359+
}
360+
361+
/// Cancel pending copies added after a certain point (used when staging fails partway)
362+
/// This prevents copy commands referencing freed buffer regions
363+
pub fn cancelPendingCopiesAfter(self: *Self, count: usize) void {
364+
if (count < self.pending_copies.items.len) {
365+
self.pending_copies.shrinkRetainingCapacity(count);
366+
}
367+
}
368+
302369
fn findMemoryType(
303370
physical_device: vk.VkPhysicalDevice,
304371
type_filter: u32,

0 commit comments

Comments
 (0)