mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-21 11:16:08 +03:00
introduce renderer thread logic (not starting it yet)
This commit is contained in:
@ -8,6 +8,7 @@
|
|||||||
//! setup (OpenGL has a context, Vulkan has a surface, etc.)
|
//! setup (OpenGL has a context, Vulkan has a surface, etc.)
|
||||||
|
|
||||||
pub const OpenGL = @import("renderer/OpenGL.zig");
|
pub const OpenGL = @import("renderer/OpenGL.zig");
|
||||||
|
pub const Thread = @import("renderer/Thread.zig");
|
||||||
|
|
||||||
test {
|
test {
|
||||||
@import("std").testing.refAllDecls(@This());
|
@import("std").testing.refAllDecls(@This());
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
pub const OpenGL = @This();
|
pub const OpenGL = @This();
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
const glfw = @import("glfw");
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
@ -320,6 +322,37 @@ pub fn deinit(self: *OpenGL) void {
|
|||||||
self.* = undefined;
|
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
|
/// 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.
|
/// 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
|
/// In steady-state operation, we use some GPU tricks to send down stale data
|
||||||
|
113
src/renderer/Thread.zig
Normal file
113
src/renderer/Thread.zig
Normal file
@ -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);
|
||||||
|
}
|
Reference in New Issue
Block a user