From 9913bba2e805cd6647bf050feeabea95b90e4f25 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 23 Oct 2022 20:18:10 -0700 Subject: [PATCH] introduce renderer thread logic (not starting it yet) --- src/renderer.zig | 1 + src/renderer/OpenGL.zig | 33 ++++++++++++ src/renderer/Thread.zig | 113 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 147 insertions(+) create mode 100644 src/renderer/Thread.zig diff --git a/src/renderer.zig b/src/renderer.zig index d55b2565f..5696a853d 100644 --- a/src/renderer.zig +++ b/src/renderer.zig @@ -8,6 +8,7 @@ //! setup (OpenGL has a context, Vulkan has a surface, etc.) pub const OpenGL = @import("renderer/OpenGL.zig"); +pub const Thread = @import("renderer/Thread.zig"); test { @import("std").testing.refAllDecls(@This()); diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index e56f832a0..5d5a4cb1f 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -2,6 +2,8 @@ pub const OpenGL = @This(); const std = @import("std"); +const builtin = @import("builtin"); +const glfw = @import("glfw"); const assert = std.debug.assert; const testing = std.testing; const Allocator = std.mem.Allocator; @@ -320,6 +322,37 @@ pub fn deinit(self: *OpenGL) void { self.* = undefined; } +/// Callback called by renderer.Thread when it begins. +pub fn threadEnter(window: glfw.Window) !void { + // We need to make the OpenGL context current. OpenGL requires + // that a single thread own the a single OpenGL context (if any). This + // ensures that the context switches over to our thread. Important: + // the prior thread MUST have detached the context prior to calling + // this entrypoint. + try glfw.makeContextCurrent(window); + errdefer glfw.makeContextCurrent(null) catch |err| + log.warn("failed to cleanup OpenGL context err={}", .{err}); + try glfw.swapInterval(1); + + // Load OpenGL bindings. This API is context-aware so this sets + // a threadlocal context for these pointers. + const version = try gl.glad.load(switch (builtin.zig_backend) { + .stage1 => glfw.getProcAddress, + else => &glfw.getProcAddress, + }); + errdefer gl.glad.unload(); + log.info("loaded OpenGL {}.{}", .{ + gl.glad.versionMajor(version), + gl.glad.versionMinor(version), + }); +} + +/// Callback called by renderer.Thread when it exits. +pub fn threadExit() void { + gl.glad.unload(); + glfw.makeContextCurrent(null) catch {}; +} + /// rebuildCells rebuilds all the GPU cells from our CPU state. This is a /// slow operation but ensures that the GPU state exactly matches the CPU state. /// In steady-state operation, we use some GPU tricks to send down stale data diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig new file mode 100644 index 000000000..729198fcc --- /dev/null +++ b/src/renderer/Thread.zig @@ -0,0 +1,113 @@ +//! Represents the renderer thread logic. The renderer thread is able to +//! be woken up to render. +pub const Thread = @This(); + +const std = @import("std"); +const builtin = @import("builtin"); +const glfw = @import("glfw"); +const libuv = @import("libuv"); +const renderer = @import("../renderer.zig"); +const gl = @import("../opengl.zig"); + +const Allocator = std.mem.Allocator; +const log = std.log.named(.renderer_thread); + +/// The main event loop for the application. The user data of this loop +/// is always the allocator used to create the loop. This is a convenience +/// so that users of the loop always have an allocator. +loop: libuv.Loop, + +/// This can be used to wake up the renderer and force a render safely from +/// any thread. +wakeup: libuv.Async, + +/// Initialize the thread. This does not START the thread. This only sets +/// up all the internal state necessary prior to starting the thread. It +/// is up to the caller to start the thread with the threadMain entrypoint. +pub fn init(alloc: Allocator) !Thread { + // We always store allocator pointer on the loop data so that + // handles can use our global allocator. + const allocPtr = try alloc.create(Allocator); + errdefer alloc.destroy(allocPtr); + allocPtr.* = alloc; + + // Create our event loop. + var loop = try libuv.Loop.init(alloc); + errdefer loop.deinit(alloc); + loop.setData(allocPtr); + + // This async handle is used to "wake up" the renderer and force a render. + var async_h = try libuv.Async.init(alloc, loop, (struct { + fn callback(_: *libuv.Async) void {} + }).callback); + errdefer async_h.close((struct { + fn callback(h: *libuv.Async) void { + const loop_alloc = h.loop().getData(Allocator).?.*; + h.deinit(loop_alloc); + } + }).callback); + + return Thread{ + .alloc = alloc, + .loop = loop, + .notifier = async_h, + }; +} + +/// Clean up the thread. This is only safe to call once the thread +/// completes executing; the caller must join prior to this. +pub fn deinit(self: *Thread) void { + // Get a copy to our allocator + const alloc_ptr = self.loop.getData(Allocator).?; + const alloc = alloc_ptr.*; + + // Run the loop one more time, because destroying our other things + // like windows usually cancel all our event loop stuff and we need + // one more run through to finalize all the closes. + _ = self.loop.run(.default) catch |err| + log.err("error finalizing event loop: {}", .{err}); + + // Dealloc our allocator copy + alloc.destroy(alloc_ptr); + + self.loop.deinit(alloc); +} + +/// The main entrypoint for the thread. +pub fn threadMain( + window: glfw.Window, + renderer_impl: *const renderer.OpenGL, +) void { + // Call child function so we can use errors... + threadMain_( + window, + renderer_impl, + ) catch |err| { + // In the future, we should expose this on the thread struct. + log.warn("error in renderer err={}", .{err}); + }; +} + +fn threadMain_( + self: *const Thread, + window: glfw.Window, + renderer_impl: *const renderer.OpenGL, +) !void { + const Renderer = switch (@TypeOf(renderer_impl)) { + .Pointer => |p| p.child, + .Struct => |s| s, + }; + + // Run our thread start/end callbacks. This is important because some + // renderers have to do per-thread setup. For example, OpenGL has to set + // some thread-local state since that is how it works. + if (@hasDecl(Renderer, "threadEnter")) try renderer_impl.threadEnter(window); + defer if (@hasDecl(Renderer, "threadExit")) renderer_impl.threadExit(); + + // Setup our timer handle which is used to perform the actual render. + // TODO + + // Run + log.debug("starting renderer thread", .{}); + try self.loop.run(.default); +}