From cca32c4d1c5fe337f60e8045b164b16f0e0e66e7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 22 Apr 2022 10:01:52 -0700 Subject: [PATCH] embedded libuv loop. still some issues: 1. 100% CPU if no handles/requests 2. slow to exit cause it waits for the next tick --- build.zig | 1 + src/App.zig | 50 +++++++++++++++++++- src/Window.zig | 21 +++++---- src/libuv/Embed.zig | 109 ++++++++++++++++++++++++++++++++++++++++++++ src/libuv/Loop.zig | 2 +- src/libuv/main.zig | 14 ++++-- 6 files changed, 179 insertions(+), 18 deletions(-) create mode 100644 src/libuv/Embed.zig diff --git a/build.zig b/build.zig index 2de72a81e..f8922712d 100644 --- a/build.zig +++ b/build.zig @@ -26,6 +26,7 @@ pub fn build(b: *std.build.Builder) !void { ftlib.link(exe); const libuv = try uv.create(b, target, mode); + libuv.link(exe); // stb if we need it // exe.addIncludeDir("vendor/stb"); diff --git a/src/App.zig b/src/App.zig index 3394c5c71..7adae511b 100644 --- a/src/App.zig +++ b/src/App.zig @@ -5,7 +5,11 @@ const App = @This(); const std = @import("std"); const Allocator = std.mem.Allocator; +const glfw = @import("glfw"); const Window = @import("Window.zig"); +const libuv = @import("libuv/main.zig"); + +const log = std.log.scoped(.app); /// General purpose allocator alloc: Allocator, @@ -14,24 +18,66 @@ alloc: Allocator, /// single window operations. window: *Window, +// The main event loop for the application. +loop: libuv.Loop, + /// Initialize the main app instance. This creates the main window, sets /// up the renderer state, compiles the shaders, etc. This is the primary /// "startup" logic. pub fn init(alloc: Allocator) !App { // Create the window - const window = try Window.create(alloc); + var window = try Window.create(alloc); + errdefer window.destroy(); + + // Create the event loop + var loop = try libuv.Loop.init(alloc); + errdefer loop.deinit(alloc); return App{ .alloc = alloc, .window = window, + .loop = loop, }; } pub fn deinit(self: *App) void { self.window.destroy(); + self.loop.deinit(self.alloc); self.* = undefined; } pub fn run(self: App) !void { - try self.window.run(); + // We are embedding two event loops: glfw and libuv. To do this, we + // create a separate thread that watches for libuv events and notifies + // glfw to wake up so we can run the libuv tick. + var embed = try libuv.Embed.init(self.alloc, self.loop, (struct { + fn callback() void { + glfw.postEmptyEvent() catch unreachable; + } + }).callback); + defer embed.deinit(self.alloc); + try embed.start(); + errdefer embed.stop() catch unreachable; + + // We need at least one handle in the event loop at all times + var timer = try libuv.Timer.init(self.alloc, self.loop); + defer timer.deinit(self.alloc); + try timer.start((struct { + fn callback(_: libuv.Timer) void { + log.info("timer tick", .{}); + } + }).callback, 5000, 5000); + + while (!self.window.shouldClose()) { + try self.window.run(); + + // Block for any glfw events. This may also be an "empty" event + // posted by the libuv watcher so that we trigger a libuv loop tick. + try glfw.waitEvents(); + + // Run the libuv loop + try embed.loopRun(); + } + + try embed.stop(); } diff --git a/src/Window.zig b/src/Window.zig index 7d2c9b274..b93ab4b59 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -119,19 +119,20 @@ pub fn destroy(self: *Window) void { self.alloc.destroy(self); } +pub fn shouldClose(self: Window) bool { + return self.window.shouldClose(); +} + pub fn run(self: Window) !void { - while (!self.window.shouldClose()) { - // Set our background - gl.clearColor(0.2, 0.3, 0.3, 1.0); - gl.clear(gl.c.GL_COLOR_BUFFER_BIT); + // Set our background + gl.clearColor(0.2, 0.3, 0.3, 1.0); + gl.clear(gl.c.GL_COLOR_BUFFER_BIT); - // Render the grid - try self.grid.render(); + // Render the grid + try self.grid.render(); - // Swap - try self.window.swapBuffers(); - try glfw.waitEvents(); - } + // Swap + try self.window.swapBuffers(); } fn sizeCallback(window: glfw.Window, width: i32, height: i32) void { diff --git a/src/libuv/Embed.zig b/src/libuv/Embed.zig new file mode 100644 index 000000000..5ac618658 --- /dev/null +++ b/src/libuv/Embed.zig @@ -0,0 +1,109 @@ +//! This has a helper for embedding libuv in another event loop. +//! This is an extension of libuv and not a helper built-in to libuv +//! itself, although it uses official APIs of libuv to enable the +//! functionality. + +const Embed = @This(); + +const std = @import("std"); +const builtin = @import("builtin"); +const testing = std.testing; +const Allocator = std.mem.Allocator; +const Loop = @import("Loop.zig"); +const Sem = @import("Sem.zig"); +const Thread = @import("Thread.zig"); + +const TerminateAtomic = std.atomic.Atomic(bool); + +loop: Loop, +sem: Sem, +terminate: TerminateAtomic, +callback: fn () void, +thread: ?Thread, + +/// Initialize a new embedder. The callback is called when libuv should +/// tick. The callback should be as fast as possible. +pub fn init(alloc: Allocator, loop: Loop, callback: fn () void) !Embed { + return Embed{ + .loop = loop, + .sem = try Sem.init(alloc, 0), + .terminate = TerminateAtomic.init(false), + .callback = callback, + .thread = null, + }; +} + +/// Deinit the embed struct. This will not automatically terminate +/// the embed thread. You must call stop manually. +pub fn deinit(self: *Embed, alloc: Allocator) void { + std.debug.assert(self.thread == null); + self.sem.deinit(alloc); + self.* = undefined; +} + +/// Start the thread that runs the embed logic and calls callback +/// when the libuv loop should tick. This must only be called once. +pub fn start(self: *Embed) !void { + self.thread = try Thread.initData(self, Embed.threadMain); +} + +/// Stop stops the embed thread and blocks until the thread joins. +pub fn stop(self: *Embed) !void { + var thread = self.thread orelse return; + + // Mark that we want to terminate + self.terminate.store(true, .SeqCst); + + // Post to the semaphore to ensure that any waits are processed. + self.sem.post(); + + // Wait + try thread.join(); + self.thread = null; +} + +/// loopRun runs the next tick of the libuv event loop. This should be +/// called by the main loop thread as a result of callback making some +/// signal. This should NOT be called from callback. +pub fn loopRun(self: Embed) !void { + _ = try self.loop.run(.nowait); + self.sem.post(); +} + +fn threadMain(self: *Embed) void { + while (self.terminate.load(.SeqCst) == false) { + const fd = self.loop.backendFd() catch unreachable; + const timeout = self.loop.backendTimeout(); + switch (builtin.os.tag) { + // epoll + .linux => { + std.log.info("FOO {} {}", .{ fd, timeout }); + var ev: [1]std.os.linux.epoll_event = undefined; + while (std.os.epoll_wait(fd, &ev, timeout) == -1) {} + }, + + else => @compileError("unsupported libuv Embed platform"), + } + + // Call our trigger + self.callback(); + + // Wait for libuv to run a tick + self.sem.wait(); + } +} + +test "Embed" { + var loop = try Loop.init(testing.allocator); + defer loop.deinit(testing.allocator); + + var embed = try init(testing.allocator, loop, (struct { + fn callback() void {} + }).callback); + defer embed.deinit(testing.allocator); + + // This just tests that the thread can start and then stop. + // It doesn't do much else at the moment + try embed.start(); + try embed.stop(); +} diff --git a/src/libuv/Loop.zig b/src/libuv/Loop.zig index 184fcb5f3..30f22bd77 100644 --- a/src/libuv/Loop.zig +++ b/src/libuv/Loop.zig @@ -40,7 +40,7 @@ pub fn alive(self: Loop) !bool { /// This function runs the event loop. See RunMode for mode documentation. /// /// This is not reentrant. It must not be called from a callback. -pub fn run(self: *Loop, mode: RunMode) !u32 { +pub fn run(self: Loop, mode: RunMode) !u32 { const res = c.uv_run(self.loop, @enumToInt(mode)); try errors.convertError(res); return @intCast(u32, res); diff --git a/src/libuv/main.zig b/src/libuv/main.zig index 66d24d352..f64d19c46 100644 --- a/src/libuv/main.zig +++ b/src/libuv/main.zig @@ -1,8 +1,10 @@ -const Loop = @import("Loop.zig"); -const Timer = @import("Timer.zig"); -const Sem = @import("Sem.zig"); -const Thread = @import("Thread.zig"); -const Error = @import("error.zig").Error; +pub const Loop = @import("Loop.zig"); +pub const Timer = @import("Timer.zig"); +pub const Sem = @import("Sem.zig"); +pub const Thread = @import("Thread.zig"); +pub const Error = @import("error.zig").Error; + +pub const Embed = @import("Embed.zig"); test { _ = Loop; @@ -10,4 +12,6 @@ test { _ = Sem; _ = Thread; _ = Error; + + _ = Embed; }