mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-09-19 20:18:07 +00:00
gtk,opengl: free us from the clutches of GtkGLArea
GtkGLArea had numerous downsides that forced us to invent unsightly hacks in our renderer to work around them, most chiefly the fact that it holds its own GdkGLContext on the main thread (GL contexts are not at all thread-safe), forcing us to keep our GL calls on the main thread. It also does not interact well with triple-buffering and initialization is forced to be this sort of deferred song-and-dance since we need to wait for the GLArea to initialize its GL context before we can initialize the renderer, the core surface, and then most things in the GTK surface. We instead invent our own custom widget named RenderSurface that takes simple DMABUFs and displays them. The task of obtaining a GL context falls to manual EGL bindings, since we also need EGL to export OpenGL textures into DMABUFs. We keep the EGL context solely on the render thread meaning that the main thread never concerns itself with rendering except when being notified that the renderer has pushed a new frame. What makes this extra significant is that now the entire GTK apprt no longer depends on OpenGL in any way, shape or form. As long as it is being fed DMABUFs, it can render from whichever graphics API you want. This means we can add more backends based on OpenGL ES or more likely Vulkan rather painlessly in the future. **AI disclosure**: I came up with the idea and let Pi implement most of the nitty-gritty details around EGL, as well as replumbing the renderer and cleaning up all the GTK-specific workarounds there. I then carefully vetted every line of code and spent roughly as much time reviewing as coding. Most of the documentation and all commit messages are in my own words.
This commit is contained in:
@@ -39,6 +39,7 @@ pub const drawElementsInstanced = draw.drawElementsInstanced;
|
||||
pub const enable = draw.enable;
|
||||
pub const disable = draw.disable;
|
||||
pub const frontFace = draw.frontFace;
|
||||
pub const readPixels = draw.readPixels;
|
||||
pub const pixelStore = draw.pixelStore;
|
||||
pub const viewport = draw.viewport;
|
||||
pub const flush = draw.flush;
|
||||
|
||||
30
src/App.zig
30
src/App.zig
@@ -267,7 +267,6 @@ fn drainMailbox(self: *App, rt_app: *apprt.App) !void {
|
||||
if (comptime std.log.logEnabled(.debug, .app)) {
|
||||
switch (message) {
|
||||
// these tend to be way too verbose for normal debugging
|
||||
.redraw_surface => {},
|
||||
else => log.debug("mailbox message={t}", .{message}),
|
||||
}
|
||||
}
|
||||
@@ -284,7 +283,6 @@ fn drainMailbox(self: *App, rt_app: *apprt.App) !void {
|
||||
.new_window => |msg| try self.newWindow(rt_app, msg),
|
||||
.close => |surface| self.closeSurface(surface),
|
||||
.surface_message => |msg| try self.surfaceMessage(msg.surface, msg.message),
|
||||
.redraw_surface => |surface| try self.redrawSurface(rt_app, surface),
|
||||
|
||||
// If we're quitting, then we set the quit flag and stop
|
||||
// draining the mailbox immediately. This lets us defer
|
||||
@@ -309,20 +307,6 @@ pub fn focusSurface(self: *App, surface: *Surface) void {
|
||||
self.focused_surface = surface;
|
||||
}
|
||||
|
||||
fn redrawSurface(
|
||||
self: *App,
|
||||
rt_app: *apprt.App,
|
||||
surface: *apprt.Surface,
|
||||
) !void {
|
||||
if (!self.hasRtSurface(surface)) return;
|
||||
|
||||
_ = try rt_app.performAction(
|
||||
.{ .surface = surface.core() },
|
||||
.render,
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a new window
|
||||
pub fn newWindow(self: *App, rt_app: *apprt.App, msg: Message.NewWindow) !void {
|
||||
const target: apprt.Target = target: {
|
||||
@@ -557,14 +541,6 @@ pub fn findSurfaceByID(self: *const App, id: u64) ?*Surface {
|
||||
return null;
|
||||
}
|
||||
|
||||
fn hasRtSurface(self: *const App, surface: *apprt.Surface) bool {
|
||||
for (self.surfaces.items) |v| {
|
||||
if (v == surface) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// The message types that can be sent to the app thread.
|
||||
pub const Message = union(enum) {
|
||||
// Open the configuration file
|
||||
@@ -586,12 +562,6 @@ pub const Message = union(enum) {
|
||||
message: apprt.surface.Message,
|
||||
},
|
||||
|
||||
/// Redraw a surface. This only has an effect for runtimes that
|
||||
/// use single-threaded draws. To redraw a surface for all runtimes,
|
||||
/// wake up the renderer thread. The renderer thread will send this
|
||||
/// message if it needs to.
|
||||
redraw_surface: *apprt.Surface,
|
||||
|
||||
const NewWindow = struct {
|
||||
/// The parent surface
|
||||
parent: ?*Surface = null,
|
||||
|
||||
@@ -496,9 +496,6 @@ pub fn init(
|
||||
var derived_config = try DerivedConfig.init(alloc, config);
|
||||
errdefer derived_config.deinit();
|
||||
|
||||
// Initialize our renderer with our initialized surface.
|
||||
try Renderer.surfaceInit(rt_surface);
|
||||
|
||||
// Determine our DPI configurations so we can properly configure
|
||||
// font points to pixels and handle other high-DPI scaling factors.
|
||||
const content_scale = try rt_surface.getContentScale();
|
||||
@@ -577,7 +574,6 @@ pub fn init(
|
||||
rt_surface,
|
||||
&self.renderer,
|
||||
&self.renderer_state,
|
||||
app_mailbox,
|
||||
);
|
||||
errdefer render_thread.deinit();
|
||||
|
||||
@@ -717,10 +713,6 @@ pub fn init(
|
||||
// to duplicate.
|
||||
try self.resize(self.size.screen);
|
||||
|
||||
// Give the renderer one more opportunity to finalize any surface
|
||||
// setup on the main thread prior to spinning up the rendering thread.
|
||||
try renderer_impl.finalizeSurfaceInit(rt_surface);
|
||||
|
||||
// Start our renderer thread
|
||||
self.renderer_thr = try std.Thread.spawn(
|
||||
.{},
|
||||
@@ -806,9 +798,6 @@ pub fn deinit(self: *Surface) void {
|
||||
self.renderer_thread.stop.notify() catch |err|
|
||||
log.err("error notifying renderer thread to stop, may stall err={}", .{err});
|
||||
self.renderer_thr.join();
|
||||
|
||||
// We need to become the active rendering thread again
|
||||
self.renderer.threadEnter(self.rt_surface) catch unreachable;
|
||||
}
|
||||
|
||||
// Stop our IO thread
|
||||
@@ -1105,6 +1094,8 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
|
||||
try self.showDesktopNotification(title, body);
|
||||
},
|
||||
|
||||
.redraw => self.redraw(),
|
||||
|
||||
.renderer_health => |health| self.updateRendererHealth(health),
|
||||
|
||||
.scrollbar => |scrollbar| self.updateScrollbar(scrollbar),
|
||||
@@ -1726,6 +1717,18 @@ fn updateScrollbar(self: *Surface, scrollbar: terminal.Scrollbar) void {
|
||||
};
|
||||
}
|
||||
|
||||
/// Called when the render thread has pushed a new frame.
|
||||
/// Notifies the apprt to redraw this surface.
|
||||
fn redraw(self: *Surface) void {
|
||||
_ = self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.render,
|
||||
{},
|
||||
) catch |err| {
|
||||
log.warn("failed to notify app of frame present err={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
/// This should be called anytime `config_conditional_state` changes
|
||||
/// so that the apprt can reload the configuration.
|
||||
fn notifyConfigConditionalState(self: *Surface) void {
|
||||
@@ -2477,6 +2480,25 @@ fn queueRender(self: *Surface) !void {
|
||||
try self.renderer_thread.wakeup.notify();
|
||||
}
|
||||
|
||||
/// Called by the apprt when the surface's display is realized.
|
||||
/// Notifies the renderer so it can begin rendering.
|
||||
/// Safe to call from the main thread.
|
||||
pub fn displayRealized(self: *Surface) !void {
|
||||
try self.renderer.displayRealized();
|
||||
}
|
||||
|
||||
/// Called by the apprt when the surface's display is unrealized (the surface
|
||||
/// is being destroyed or reparented). Safe to call from the main thread.
|
||||
pub fn displayUnrealized(self: *Surface) void {
|
||||
self.renderer.displayUnrealized();
|
||||
|
||||
// Wake the render thread so it notices `display_realized` is now false
|
||||
// and releases GPU resources (swap chain and shaders).
|
||||
self.renderer_thread.wakeup.notify() catch |err| {
|
||||
log.warn("failed to notify renderer thread of unrealize err={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
pub fn sizeCallback(self: *Surface, size: apprt.SurfaceSize) !void {
|
||||
// Crash metadata in case we crash in here
|
||||
crash.sentry.thread_state = self.crashThreadState();
|
||||
@@ -2516,6 +2538,16 @@ fn resize(self: *Surface, size: rendererpkg.ScreenSize) !void {
|
||||
|
||||
// Mail the IO thread
|
||||
self.queueIo(.{ .resize = self.size }, .unlocked);
|
||||
|
||||
// Mail the render thread so it updates its padding and screen size.
|
||||
_ = self.renderer_thread.mailbox.push(
|
||||
global.io(),
|
||||
.{ .resize = self.size },
|
||||
.forever,
|
||||
);
|
||||
self.queueRender() catch |err| {
|
||||
log.warn("failed to notify renderer of resize err={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
/// Recalculate the balanced padding if needed.
|
||||
|
||||
@@ -52,15 +52,6 @@ pub const runtime = switch (build_config.artifact) {
|
||||
pub const App = runtime.App;
|
||||
pub const Surface = runtime.Surface;
|
||||
|
||||
/// True if all GPU operations (drawing, resource creation and
|
||||
/// destruction) must happen on the app thread. This is the case
|
||||
/// when the graphics context is owned by the app thread (GTK's
|
||||
/// GLArea). When false, the render thread performs GPU operations
|
||||
/// itself.
|
||||
pub const must_draw_from_app_thread =
|
||||
@hasDecl(App, "must_draw_from_app_thread") and
|
||||
App.must_draw_from_app_thread;
|
||||
|
||||
test {
|
||||
_ = Runtime;
|
||||
_ = runtime;
|
||||
|
||||
@@ -18,11 +18,6 @@ const ipcToggleQuickTerminal = @import("ipc/toggle_quick_terminal.zig").toggleQu
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
|
||||
/// This is detected by the Renderer, in which case it sends a `redraw_surface`
|
||||
/// message so that we can call `drawFrame` ourselves from the app thread,
|
||||
/// because GTK's `GLArea` does not support drawing from a different thread.
|
||||
pub const must_draw_from_app_thread = true;
|
||||
|
||||
/// GTK application ID
|
||||
pub const application_id = @import("build/info.zig").application_id;
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ pub const Application = @import("class/application.zig").Application;
|
||||
pub const Window = @import("class/window.zig").Window;
|
||||
pub const Config = @import("class/config.zig").Config;
|
||||
pub const Surface = @import("class/surface.zig").Surface;
|
||||
pub const RenderSurface = @import("class/render_surface.zig").RenderSurface;
|
||||
|
||||
/// Common methods for all GObject classes we create.
|
||||
pub fn Common(
|
||||
|
||||
349
src/apprt/gtk/class/render_surface.zig
Normal file
349
src/apprt/gtk/class/render_surface.zig
Normal file
@@ -0,0 +1,349 @@
|
||||
const std = @import("std");
|
||||
|
||||
const glib = @import("glib");
|
||||
const gobject = @import("gobject");
|
||||
const gdk = @import("gdk");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const global = @import("../../../global.zig");
|
||||
const Application = @import("application.zig").Application;
|
||||
const Common = @import("../class.zig").Common;
|
||||
const CoreSurface = @import("../../../Surface.zig");
|
||||
const rendererpkg = @import("../../../renderer.zig");
|
||||
const ExportedFrame = rendererpkg.Renderer.ExportedFrame;
|
||||
const Dmabuf = @import("../../../renderer/Dmabuf.zig");
|
||||
const Planes = Dmabuf.Planes;
|
||||
|
||||
const log = std.log.scoped(.gtk_render_surface);
|
||||
|
||||
/// A widget that displays the rendered output of the Ghostty renderer.
|
||||
///
|
||||
/// In the past, Ghostty relied on GTK's builtin `GLArea` to display the
|
||||
/// rendered output within the GTK-based UI, but this had numerous
|
||||
/// debilitating limitations.
|
||||
///
|
||||
/// Its most significant limitation is that GTK's render lifecycle lives
|
||||
/// **exclusively on the main UI thread**, from initialization to drawing the
|
||||
/// rendered framebuffers. This is not only inefficient but also error-prone,
|
||||
/// as we offload our render work to a dedicated render thread which has
|
||||
/// to be careful not to access the renderer at the same time as the main
|
||||
/// thread. Initialization is also far more complex as we had to wait for
|
||||
/// GTK's OpenGL context to initialize, then we can proceed with initializing
|
||||
/// the renderer and then the core surface. (The GTK surface widget could not
|
||||
/// assume it always has a valid core surface, which is obviously absurd!)
|
||||
/// It was a messy, chicken-and-egg relationship that held us back a lot.
|
||||
///
|
||||
/// With our custom `RenderSurface`, the renderer instead renders frames,
|
||||
/// exports them into DMABUFs, then pushes them into a queue that this widget
|
||||
/// can consume at its own pace on the main thread, whenever GTK calls for it
|
||||
/// to be snapshotted. This also allowed us to do triple-buffering and should
|
||||
/// greatly reduce tearing. Initialization is done on the render thread and
|
||||
/// always completes before the core surface is created. Furthermore, this
|
||||
/// surface is actually OpenGL-agnostic, since it operates exclusively on
|
||||
/// DMABUFs which can also be produced by other graphics APIs like Vulkan.
|
||||
pub const RenderSurface = extern struct {
|
||||
const Self = @This();
|
||||
parent_instance: Parent,
|
||||
pub const Parent = gtk.Widget;
|
||||
pub const getGObjectType = gobject.ext.defineClass(Self, .{
|
||||
.name = "GhosttyRenderSurface",
|
||||
.classInit = &Class.init,
|
||||
.parent_class = &Class.parent,
|
||||
.private = .{ .Type = Private, .offset = &Private.offset },
|
||||
});
|
||||
|
||||
const C = Common(Self, Private);
|
||||
|
||||
pub const Private = struct {
|
||||
/// The core surface, used to pull presents from the renderer.
|
||||
/// Set by the apprt Surface when it realizes the render surface.
|
||||
core_surface: ?*CoreSurface = null,
|
||||
|
||||
/// The GDK Texture currently being displayed.
|
||||
texture: ?*gdk.Texture = null,
|
||||
|
||||
pub var offset: c_int = 0;
|
||||
};
|
||||
|
||||
const private = C.private;
|
||||
pub const as = C.as;
|
||||
|
||||
pub const signals = struct {
|
||||
pub const resize = struct {
|
||||
pub const name = "resize";
|
||||
const impl = gobject.ext.defineSignal(
|
||||
name,
|
||||
Self,
|
||||
&.{ c_int, c_int },
|
||||
void,
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Virtual methods
|
||||
|
||||
fn realize(self: *Self) callconv(.c) void {
|
||||
// Call the parent realize.
|
||||
gtk.Widget.virtual_methods.realize.call(
|
||||
Class.parent,
|
||||
self.as(gtk.Widget),
|
||||
);
|
||||
|
||||
// Request a draw in case frames were produced before we
|
||||
// realized. The snapshot will pull from the renderer's queue.
|
||||
self.as(gtk.Widget).queueDraw();
|
||||
}
|
||||
|
||||
fn unrealize(self: *Self) callconv(.c) void {
|
||||
const priv = self.private();
|
||||
|
||||
if (priv.texture) |tex| {
|
||||
tex.as(gobject.Object).unref();
|
||||
priv.texture = null;
|
||||
}
|
||||
|
||||
gtk.Widget.virtual_methods.unrealize.call(
|
||||
Class.parent,
|
||||
self.as(gtk.Widget),
|
||||
);
|
||||
}
|
||||
|
||||
fn sizeAllocate(
|
||||
self: *Self,
|
||||
width: c_int,
|
||||
height: c_int,
|
||||
baseline: c_int,
|
||||
) callconv(.c) void {
|
||||
const scale = self.as(gtk.Widget).getScaleFactor();
|
||||
const device_width = width * scale;
|
||||
const device_height = height * scale;
|
||||
|
||||
// Emit resize so the surface's renderSurfaceResize callback
|
||||
// forwards size updates to the core surface/renderer. We emit
|
||||
// device pixels (width*scale) so that the renderer's size matches
|
||||
// what `surfaceSize` reports and what the render target is
|
||||
// allocated at.
|
||||
signals.resize.impl.emit(self, null, .{ device_width, device_height }, null);
|
||||
|
||||
gtk.Widget.virtual_methods.size_allocate.call(
|
||||
Class.parent,
|
||||
self.as(gtk.Widget),
|
||||
width,
|
||||
height,
|
||||
baseline,
|
||||
);
|
||||
}
|
||||
|
||||
fn snapshot(self: *Self, snap: *gtk.Snapshot) callconv(.c) void {
|
||||
const priv = self.private();
|
||||
|
||||
// Take the latest present from the renderer's queue, if any, and
|
||||
// build a texture from it. `take` closes any present that was
|
||||
// still unconsumed in the queue, but not the texture we're
|
||||
// currently displaying.
|
||||
if (priv.core_surface) |core| {
|
||||
if (core.renderer.takeFrame()) |frame| {
|
||||
self.rebuildTexture(frame) catch |err| {
|
||||
log.warn("error building texture from frame err={}", .{err});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a texture, draw it.
|
||||
const texture = priv.texture orelse return;
|
||||
|
||||
const widget = self.as(gtk.Widget);
|
||||
const w = widget.getWidth();
|
||||
const h = widget.getHeight();
|
||||
if (w == 0 or h == 0) return;
|
||||
|
||||
// In OpenGL +Y is up but in GSK (and DirectX, Metal, Vulkan, etc.)
|
||||
// +Y is down. We therefore might need to flip the rendered image.
|
||||
snap.save();
|
||||
defer snap.restore();
|
||||
if (comptime !rendererpkg.Renderer.API.custom_shader_y_is_down) {
|
||||
snap.translate(&.{ .f_x = 0, .f_y = @floatFromInt(h) });
|
||||
snap.scale(1, -1);
|
||||
}
|
||||
|
||||
snap.appendTexture(texture, &.{
|
||||
.f_origin = .{ .f_x = 0, .f_y = 0 },
|
||||
.f_size = .{ .f_width = @floatFromInt(w), .f_height = @floatFromInt(h) },
|
||||
});
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Presenting
|
||||
|
||||
/// Return the size of this surface in device pixels. Used by the
|
||||
/// apprt surface to report the size to the renderer.
|
||||
pub fn deviceSize(self: *Self) struct { width: u32, height: u32 } {
|
||||
const scale = @max(self.as(gtk.Widget).getScaleFactor(), 1);
|
||||
const width = self.as(gtk.Widget).getWidth();
|
||||
const height = self.as(gtk.Widget).getHeight();
|
||||
|
||||
return .{
|
||||
.width = @intCast(@max(width * scale, 0)),
|
||||
.height = @intCast(@max(height * scale, 0)),
|
||||
};
|
||||
}
|
||||
|
||||
/// Set the core surface to pull presents from. Nothing will
|
||||
/// render when this is unset.
|
||||
pub fn setCoreSurface(self: *Self, core: ?*CoreSurface) void {
|
||||
self.private().core_surface = core;
|
||||
}
|
||||
|
||||
/// Build a `GdkTexture` from the present and set it as our current
|
||||
/// texture, unrefing any previous texture.
|
||||
fn rebuildTexture(self: *Self, frame: ExportedFrame) !void {
|
||||
switch (frame) {
|
||||
.dmabuf => |dmabuf| try self.rebuildDmabufTexture(dmabuf),
|
||||
.memory => |memory| try self.rebuildMemoryTexture(memory),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a `GdkDmabufTexture` from the present and set it as our
|
||||
/// current texture, unrefing any previous texture.
|
||||
fn rebuildDmabufTexture(self: *Self, frame: Dmabuf) !void {
|
||||
const priv = self.private();
|
||||
const widget = self.as(gtk.Widget);
|
||||
const display = widget.getDisplay();
|
||||
|
||||
const builder = gdk.DmabufTextureBuilder.new();
|
||||
defer builder.unref();
|
||||
|
||||
builder.setDisplay(display);
|
||||
builder.setWidth(frame.width);
|
||||
builder.setHeight(frame.height);
|
||||
builder.setFourcc(frame.fourcc);
|
||||
builder.setModifier(frame.modifier);
|
||||
builder.setPremultiplied(@intFromBool(frame.premultiplied));
|
||||
builder.setNPlanes(@intCast(frame.planes.count));
|
||||
|
||||
for (0..frame.planes.count) |i| {
|
||||
builder.setFd(@intCast(i), frame.planes.fds[i]);
|
||||
builder.setOffset(@intCast(i), @intCast(frame.planes.offsets[i]));
|
||||
builder.setStride(@intCast(i), @intCast(frame.planes.strides[i]));
|
||||
}
|
||||
|
||||
// Build the texture. We retain ownership over the DMABUF FDs
|
||||
// until the asynchronous texture building process is complete,
|
||||
// so we have to make a heap copy of them, send them to GTK,
|
||||
// and then destroy them once successful.
|
||||
const app = Application.default();
|
||||
const alloc = app.allocator();
|
||||
|
||||
const planes = try alloc.create(Planes);
|
||||
errdefer alloc.destroy(planes);
|
||||
|
||||
planes.* = frame.planes;
|
||||
errdefer planes.deinit();
|
||||
|
||||
const texture = builder.build(
|
||||
dmabufDestroy,
|
||||
planes,
|
||||
null,
|
||||
) orelse return error.DmabufBuildFailed;
|
||||
|
||||
// Swap out the old texture.
|
||||
if (priv.texture) |old| old.as(gobject.Object).unref();
|
||||
priv.texture = texture;
|
||||
}
|
||||
|
||||
/// Destroy callback for `GdkDmabufTexture`. GDK calls this when the
|
||||
/// texture is released; we close the FDs and free the holder.
|
||||
fn dmabufDestroy(data: ?*anyopaque) callconv(.c) void {
|
||||
const app = Application.default();
|
||||
const alloc = app.allocator();
|
||||
|
||||
const planes: *Planes = @ptrCast(@alignCast(data orelse return));
|
||||
planes.deinit();
|
||||
alloc.destroy(planes);
|
||||
}
|
||||
|
||||
/// Holder for the pixel data backing a `GdkMemoryTexture`. GTK
|
||||
/// ref-counts the `GBytes` we hand it and calls `memoryDestroy`
|
||||
/// when it no longer needs the data, at which point we can return
|
||||
/// the pixels to the allocator.
|
||||
const MemoryHolder = struct {
|
||||
alloc: std.mem.Allocator,
|
||||
pixels: []u8,
|
||||
};
|
||||
|
||||
/// Build a `GdkMemoryTexture` from a CPU memory frame and set it as
|
||||
/// our current texture, unrefing any previous texture. This is the
|
||||
/// fallback path for drivers that can't export DMA-BUFs.
|
||||
fn rebuildMemoryTexture(self: *Self, frame: ExportedFrame.Memory) !void {
|
||||
const priv = self.private();
|
||||
|
||||
const app = Application.default();
|
||||
const alloc = app.allocator();
|
||||
|
||||
const holder = alloc.create(MemoryHolder) catch |err| {
|
||||
// We own the pixels; make sure they don't leak.
|
||||
frame.deinit();
|
||||
return err;
|
||||
};
|
||||
errdefer alloc.destroy(holder);
|
||||
holder.* = .{ .alloc = alloc, .pixels = frame.pixels };
|
||||
|
||||
// `GdkMemoryTexture` keeps a reference to the `GBytes` for as
|
||||
// long as it needs the pixel data, so we hand it our holder and
|
||||
// get the pixels freed via the destroy notify below.
|
||||
const bytes = glib.Bytes.newWithFreeFunc(
|
||||
holder.pixels.ptr,
|
||||
holder.pixels.len,
|
||||
&memoryDestroy,
|
||||
holder,
|
||||
);
|
||||
errdefer bytes.unref();
|
||||
|
||||
const texture = gdk.MemoryTexture.new(
|
||||
@intCast(frame.width),
|
||||
@intCast(frame.height),
|
||||
.r8g8b8a8_premultiplied,
|
||||
bytes,
|
||||
frame.width * 4, // stride
|
||||
);
|
||||
|
||||
// The texture holds its own reference to the bytes now.
|
||||
bytes.unref();
|
||||
|
||||
// Swap out the old texture.
|
||||
if (priv.texture) |old| old.as(gobject.Object).unref();
|
||||
priv.texture = texture.as(gdk.Texture);
|
||||
}
|
||||
|
||||
/// Destroy callback for the `GBytes` backing a `GdkMemoryTexture`.
|
||||
fn memoryDestroy(data: ?*anyopaque) callconv(.c) void {
|
||||
const holder: *MemoryHolder = @ptrCast(@alignCast(data orelse return));
|
||||
holder.alloc.free(holder.pixels);
|
||||
holder.alloc.destroy(holder);
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Class
|
||||
|
||||
pub const Class = extern struct {
|
||||
parent_class: Parent.Class,
|
||||
var parent: *Parent.Class = undefined;
|
||||
pub const Instance = Self;
|
||||
|
||||
fn init(class: *Class) callconv(.c) void {
|
||||
// Virtual methods
|
||||
gtk.Widget.virtual_methods.realize.implement(class, &realize);
|
||||
gtk.Widget.virtual_methods.unrealize.implement(class, &unrealize);
|
||||
gtk.Widget.virtual_methods.size_allocate.implement(class, &sizeAllocate);
|
||||
gtk.Widget.virtual_methods.snapshot.implement(class, &snapshot);
|
||||
|
||||
// Signals
|
||||
signals.resize.impl.register(.{});
|
||||
}
|
||||
|
||||
pub const as = C.Class.as;
|
||||
pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
|
||||
pub const bindTemplateCallback = C.Class.bindTemplateCallback;
|
||||
};
|
||||
};
|
||||
@@ -35,6 +35,7 @@ const TitleDialog = @import("title_dialog.zig").TitleDialog;
|
||||
const Window = @import("window.zig").Window;
|
||||
const InspectorWindow = @import("inspector_window.zig").InspectorWindow;
|
||||
const SplitTree = @import("split_tree.zig").SplitTree;
|
||||
const RenderSurface = @import("render_surface.zig").RenderSurface;
|
||||
const i18n = @import("../../../os/i18n.zig");
|
||||
const global = @import("../../../global.zig");
|
||||
const gtk_version = @import("../gtk_version.zig");
|
||||
@@ -612,17 +613,18 @@ pub const Surface = extern struct {
|
||||
/// focus events.
|
||||
focused: bool = true,
|
||||
|
||||
/// Whether the GLArea widget is mapped. Some operations like grabbing
|
||||
/// focus only work if a widget is mapped.
|
||||
/// Whether the RenderSurface widget is mapped. Some operations like
|
||||
/// grabbing focus only work if a widget is mapped.
|
||||
mapped: bool = false,
|
||||
|
||||
/// Whether this surface is "zoomed" or not. A zoomed surface
|
||||
/// shows up taking the full bounds of a split view.
|
||||
zoom: bool = false,
|
||||
|
||||
/// The GLArea that renders the actual surface. This is a binding
|
||||
/// to the template so it doesn't have to be unrefed manually.
|
||||
gl_area: *gtk.GLArea,
|
||||
/// The RenderSurface that displays the rendered output of the
|
||||
/// surface. This is a binding to the template so it doesn't have
|
||||
/// to be unrefed manually.
|
||||
render_surface: *RenderSurface,
|
||||
|
||||
/// The labels for the left/right sides of the URL hover tooltip.
|
||||
url_left: *gtk.Label,
|
||||
@@ -824,7 +826,7 @@ pub const Surface = extern struct {
|
||||
/// then we should force a redraw.
|
||||
pub fn redraw(self: *Self) void {
|
||||
const priv = self.private();
|
||||
priv.gl_area.queueRender();
|
||||
priv.render_surface.as(gtk.Widget).queueDraw();
|
||||
}
|
||||
|
||||
/// Callback used to determine whether border should be shown around the
|
||||
@@ -1376,7 +1378,7 @@ pub const Surface = extern struct {
|
||||
// Get the keyvals for this event.
|
||||
const keyval_unicode = gdk.keyvalToUnicode(keyval);
|
||||
const keyval_unicode_unshifted: u21 = gtk_key.keyvalUnicodeUnshifted(
|
||||
priv.gl_area.as(gtk.Widget),
|
||||
priv.render_surface.as(gtk.Widget),
|
||||
key_event,
|
||||
keycode,
|
||||
);
|
||||
@@ -1500,9 +1502,9 @@ pub const Surface = extern struct {
|
||||
x: f64,
|
||||
y: f64,
|
||||
) struct { x: f64, y: f64 } {
|
||||
const gl_area = self.private().gl_area;
|
||||
const widget = self.private().render_surface;
|
||||
const scale_factor: f64 = @floatFromInt(
|
||||
gl_area.as(gtk.Widget).getScaleFactor(),
|
||||
widget.as(gtk.Widget).getScaleFactor(),
|
||||
);
|
||||
|
||||
return .{
|
||||
@@ -1548,10 +1550,9 @@ pub const Surface = extern struct {
|
||||
|
||||
pub fn getContentScale(self: *Self) apprt.ContentScale {
|
||||
const priv = self.private();
|
||||
const gl_area = priv.gl_area;
|
||||
const widget = priv.render_surface.as(gtk.Widget);
|
||||
|
||||
const gtk_scale: f32 = scale: {
|
||||
const widget = gl_area.as(gtk.Widget);
|
||||
// Future: detect GTK version 4.12+ and use gdk_surface_get_scale so we
|
||||
// can support fractional scaling.
|
||||
const scale = widget.getScaleFactor();
|
||||
@@ -1765,7 +1766,7 @@ pub const Surface = extern struct {
|
||||
/// our surface.
|
||||
pub fn grabFocus(self: *Self) void {
|
||||
const priv = self.private();
|
||||
_ = priv.gl_area.as(gtk.Widget).grabFocus();
|
||||
_ = priv.render_surface.as(gtk.Widget).grabFocus();
|
||||
}
|
||||
|
||||
pub fn sendDesktopNotification(self: *Self, title: [:0]const u8, body: [:0]const u8) void {
|
||||
@@ -2864,10 +2865,10 @@ pub const Surface = extern struct {
|
||||
const core_surface = priv.core_surface orelse return;
|
||||
|
||||
// If we don't have focus, grab it.
|
||||
const gl_area_widget = priv.gl_area.as(gtk.Widget);
|
||||
const had_focus = gl_area_widget.hasFocus() != 0;
|
||||
const widget = priv.render_surface.as(gtk.Widget);
|
||||
const had_focus = widget.hasFocus() != 0;
|
||||
if (!had_focus) {
|
||||
_ = gl_area_widget.grabFocus();
|
||||
_ = widget.grabFocus();
|
||||
}
|
||||
|
||||
// Report the event
|
||||
@@ -3002,11 +3003,11 @@ pub const Surface = extern struct {
|
||||
|
||||
// If we don't have focus, and we want it, grab it.
|
||||
if (priv.config) |config| {
|
||||
const gl_area_widget = priv.gl_area.as(gtk.Widget);
|
||||
if (gl_area_widget.hasFocus() == 0 and
|
||||
const widget = priv.render_surface.as(gtk.Widget);
|
||||
if (widget.hasFocus() == 0 and
|
||||
config.get().@"focus-follows-mouse")
|
||||
{
|
||||
_ = gl_area_widget.grabFocus();
|
||||
_ = widget.grabFocus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3315,35 +3316,31 @@ pub const Surface = extern struct {
|
||||
}
|
||||
}
|
||||
|
||||
fn glareaRealize(
|
||||
_: *gtk.GLArea,
|
||||
fn renderSurfaceRealize(
|
||||
_: *RenderSurface,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
log.debug("realize", .{});
|
||||
log.debug("render surface realize", .{});
|
||||
|
||||
// Make the GL area current so we can detect any OpenGL errors. If
|
||||
// we have errors here we can't render and we switch to the error
|
||||
// state.
|
||||
// The RenderSurface has no GL context; the renderer's EGL context
|
||||
// manages all GPU resources. We just notify the core surface that
|
||||
// the widget is realized (has a valid GDK surface with a real size)
|
||||
// so it can begin rendering.
|
||||
const priv = self.private();
|
||||
priv.gl_area.makeCurrent();
|
||||
if (priv.gl_area.getError()) |err| {
|
||||
log.warn("failed to make GL context current: {s}", .{err.f_message orelse "(no message)"});
|
||||
log.warn("this error is almost always due to a library, driver, or GTK issue", .{});
|
||||
log.warn("this is a common cause of this issue: https://ghostty.org/docs/help/gtk-opengl-context", .{});
|
||||
self.setError(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// If we already have an initialized surface then we notify it.
|
||||
// If we don't, we'll initialize it on the first resize so we have
|
||||
// our proper initial dimensions.
|
||||
if (priv.core_surface) |v| realize: {
|
||||
v.renderer.displayRealized() catch |err| {
|
||||
v.displayRealized() catch |err| {
|
||||
log.warn("core displayRealized failed err={}", .{err});
|
||||
break :realize;
|
||||
};
|
||||
|
||||
self.redraw();
|
||||
} else {
|
||||
// Lazily initialize the core surface and renderer on first
|
||||
// realization. This must happen here (not in size_allocate)
|
||||
// because we need a valid GDK surface to create the EGL context.
|
||||
self.initSurface() catch |err| {
|
||||
log.warn("surface failed to initialize err={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
// Setup our input method. We do this here because this will
|
||||
@@ -3352,50 +3349,31 @@ pub const Surface = extern struct {
|
||||
priv.im_context.as(gtk.IMContext).setClientWidget(self.as(gtk.Widget));
|
||||
}
|
||||
|
||||
fn glareaUnrealize(
|
||||
gl_area: *gtk.GLArea,
|
||||
fn renderSurfaceUnrealize(
|
||||
_: *RenderSurface,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
log.debug("unrealize", .{});
|
||||
log.debug("render surface unrealize", .{});
|
||||
|
||||
// Notify our core surface
|
||||
const priv = self.private();
|
||||
if (priv.core_surface) |surface| {
|
||||
// There is no guarantee that our GLArea context is current
|
||||
// when unrealize is emitted, so we need to make it current.
|
||||
gl_area.makeCurrent();
|
||||
if (gl_area.getError()) |err| {
|
||||
// I don't know a scenario this can happen, but it means
|
||||
// we probably leaked memory because displayUnrealized
|
||||
// below frees resources that aren't specifically OpenGL
|
||||
// related. I didn't make the OpenGL renderer handle this
|
||||
// scenario because I don't know if its even possible
|
||||
// under valid circumstances, so let's log.
|
||||
log.warn(
|
||||
"gl_area_make_current failed in unrealize msg={s}",
|
||||
.{err.f_message orelse "(no message)"},
|
||||
);
|
||||
log.warn("OpenGL resources and memory likely leaked", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
surface.renderer.displayUnrealized();
|
||||
surface.displayUnrealized();
|
||||
}
|
||||
|
||||
// Unset our input method
|
||||
priv.im_context.as(gtk.IMContext).setClientWidget(null);
|
||||
}
|
||||
|
||||
fn glareaMap(
|
||||
_: *gtk.GLArea,
|
||||
fn renderSurfaceMap(
|
||||
_: *RenderSurface,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
self.updateMapped(true);
|
||||
self.updateOcclusion();
|
||||
}
|
||||
|
||||
fn glareaUnmap(
|
||||
_: *gtk.GLArea,
|
||||
fn renderSurfaceUnmap(
|
||||
_: *RenderSurface,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
self.updateMapped(false);
|
||||
@@ -3425,33 +3403,15 @@ pub const Surface = extern struct {
|
||||
return window.isSuspended() != 0;
|
||||
}
|
||||
|
||||
fn glareaRender(
|
||||
_: *gtk.GLArea,
|
||||
_: *gdk.GLContext,
|
||||
self: *Self,
|
||||
) callconv(.c) c_int {
|
||||
// If we don't have a surface then we failed to initialize for
|
||||
// some reason and there's nothing to draw to the GLArea.
|
||||
const priv = self.private();
|
||||
const surface = priv.core_surface orelse return 1;
|
||||
|
||||
surface.renderer.drawFrame(true) catch |err| {
|
||||
log.warn("failed to draw frame err={}", .{err});
|
||||
return 0;
|
||||
};
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
fn glareaResize(
|
||||
gl_area: *gtk.GLArea,
|
||||
fn renderSurfaceResize(
|
||||
_: *RenderSurface,
|
||||
width: c_int,
|
||||
height: c_int,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
// Some debug output to help understand what GTK is telling us.
|
||||
{
|
||||
const widget = gl_area.as(gtk.Widget);
|
||||
const widget = self.private().render_surface.as(gtk.Widget);
|
||||
const scale_factor = widget.getScaleFactor();
|
||||
const window_scale_factor = scale: {
|
||||
const root = widget.getRoot() orelse break :scale 0;
|
||||
@@ -3504,24 +3464,12 @@ pub const Surface = extern struct {
|
||||
}
|
||||
|
||||
const InitError = Allocator.Error || error{
|
||||
GLAreaError,
|
||||
SurfaceError,
|
||||
};
|
||||
|
||||
fn initSurface(self: *Self) InitError!void {
|
||||
const priv: *Private = self.private();
|
||||
assert(priv.core_surface == null);
|
||||
const gl_area = priv.gl_area;
|
||||
|
||||
// We need to make the context current so we can call GL functions.
|
||||
// This is required for all surface operations.
|
||||
gl_area.makeCurrent();
|
||||
if (gl_area.getError()) |err| {
|
||||
log.warn("failed to make GL context current: {s}", .{err.f_message orelse "(no message)"});
|
||||
log.warn("this error is usually due to a driver or gtk bug", .{});
|
||||
log.warn("this is a common cause of this issue: https://gitlab.gnome.org/GNOME/gtk/-/issues/4950", .{});
|
||||
return error.GLAreaError;
|
||||
}
|
||||
|
||||
const app = Application.default();
|
||||
const alloc = app.allocator();
|
||||
@@ -3579,6 +3527,10 @@ pub const Surface = extern struct {
|
||||
// Store it!
|
||||
priv.core_surface = surface;
|
||||
|
||||
// Give the render surface a pointer to the core surface so it
|
||||
// can pull presents from the renderer in its snapshot handler.
|
||||
priv.render_surface.setCoreSurface(surface);
|
||||
|
||||
// Emit the signal that we initialized the surface.
|
||||
Surface.signals.init.impl.emit(
|
||||
self,
|
||||
@@ -3656,7 +3608,7 @@ pub const Surface = extern struct {
|
||||
_ = surface.performBindingAction(.end_search) catch |err| {
|
||||
log.warn("unable to perform end_search action err={}", .{err});
|
||||
};
|
||||
_ = self.private().gl_area.as(gtk.Widget).grabFocus();
|
||||
_ = self.private().render_surface.as(gtk.Widget).grabFocus();
|
||||
}
|
||||
|
||||
fn searchChanged(_: *SearchOverlay, needle: ?[*:0]const u8, self: *Self) callconv(.c) void {
|
||||
@@ -3879,7 +3831,7 @@ pub const Surface = extern struct {
|
||||
);
|
||||
|
||||
// Bindings
|
||||
class.bindTemplateChildPrivate("gl_area", .{});
|
||||
class.bindTemplateChildPrivate("render_surface", .{});
|
||||
class.bindTemplateChildPrivate("url_left", .{});
|
||||
class.bindTemplateChildPrivate("url_right", .{});
|
||||
class.bindTemplateChildPrivate("child_exited_overlay", .{});
|
||||
@@ -3910,12 +3862,11 @@ pub const Surface = extern struct {
|
||||
class.bindTemplateCallback("scroll_vertical_end", &ecMouseScrollVerticalPrecisionEnd);
|
||||
class.bindTemplateCallback("scroll_horizontal", &ecMouseScrollHorizontal);
|
||||
class.bindTemplateCallback("drop", &dtDrop);
|
||||
class.bindTemplateCallback("gl_realize", &glareaRealize);
|
||||
class.bindTemplateCallback("gl_unrealize", &glareaUnrealize);
|
||||
class.bindTemplateCallback("gl_map", &glareaMap);
|
||||
class.bindTemplateCallback("gl_unmap", &glareaUnmap);
|
||||
class.bindTemplateCallback("gl_render", &glareaRender);
|
||||
class.bindTemplateCallback("gl_resize", &glareaResize);
|
||||
class.bindTemplateCallback("render_surface_realize", &renderSurfaceRealize);
|
||||
class.bindTemplateCallback("render_surface_unrealize", &renderSurfaceUnrealize);
|
||||
class.bindTemplateCallback("render_surface_map", &renderSurfaceMap);
|
||||
class.bindTemplateCallback("render_surface_unmap", &renderSurfaceUnmap);
|
||||
class.bindTemplateCallback("render_surface_resize", &renderSurfaceResize);
|
||||
class.bindTemplateCallback("im_preedit_start", &imPreeditStart);
|
||||
class.bindTemplateCallback("im_preedit_changed", &imPreeditChanged);
|
||||
class.bindTemplateCallback("im_preedit_end", &imPreeditEnd);
|
||||
@@ -4061,7 +4012,7 @@ const Clipboard = struct {
|
||||
// If no confirmation is necessary, set the clipboard.
|
||||
if (!confirm) {
|
||||
const clipboard = get(
|
||||
priv.gl_area.as(gtk.Widget),
|
||||
priv.render_surface.as(gtk.Widget),
|
||||
clipboard_type,
|
||||
) orelse return;
|
||||
|
||||
@@ -4147,7 +4098,7 @@ const Clipboard = struct {
|
||||
|
||||
// Get our requested clipboard
|
||||
const clipboard = get(
|
||||
self.private().gl_area.as(gtk.Widget),
|
||||
self.private().render_surface.as(gtk.Widget),
|
||||
clipboard_type,
|
||||
) orelse return .unsupported;
|
||||
|
||||
|
||||
@@ -21,20 +21,16 @@ Overlay terminal_page {
|
||||
hexpand: true;
|
||||
vexpand: true;
|
||||
|
||||
GLArea gl_area {
|
||||
realize => $gl_realize();
|
||||
unrealize => $gl_unrealize();
|
||||
map => $gl_map();
|
||||
unmap => $gl_unmap();
|
||||
render => $gl_render();
|
||||
resize => $gl_resize();
|
||||
$GhosttyRenderSurface render_surface {
|
||||
realize => $render_surface_realize();
|
||||
unrealize => $render_surface_unrealize();
|
||||
map => $render_surface_map();
|
||||
unmap => $render_surface_unmap();
|
||||
resize => $render_surface_resize();
|
||||
hexpand: true;
|
||||
vexpand: true;
|
||||
focusable: true;
|
||||
focus-on-click: true;
|
||||
has-stencil-buffer: false;
|
||||
has-depth-buffer: false;
|
||||
allowed-apis: gl;
|
||||
}
|
||||
|
||||
PopoverMenu context_menu {
|
||||
|
||||
@@ -153,6 +153,9 @@ pub const Message = union(enum) {
|
||||
/// Selected search index change
|
||||
search_selected: ?usize,
|
||||
|
||||
/// Renderer pushed a new frame, redraw this surface.
|
||||
redraw,
|
||||
|
||||
pub const ReportTitleStyle = enum {
|
||||
csi_21_t,
|
||||
|
||||
|
||||
75
src/renderer/Dmabuf.zig
Normal file
75
src/renderer/Dmabuf.zig
Normal file
@@ -0,0 +1,75 @@
|
||||
//! A DMABUF frame produced by exporting a GPU texture.
|
||||
//! This may be used on apprts like GTK that require us to manually
|
||||
//! export each frame and import them as textures in their UI scene graphs.
|
||||
//!
|
||||
//! Note that DMABUFs are independent of the graphics API used:
|
||||
//! the OpenGL renderer allocates them with GBM, and Vulkan can
|
||||
//! export them with a KHR external memory implementation. Therefore
|
||||
//! this struct has to be placed parallel to the renderer
|
||||
//! implementations.
|
||||
pub const Dmabuf = @This();
|
||||
pub const std = @import("std");
|
||||
|
||||
/// The maximum number of planes in a DMABUF that we support.
|
||||
/// This matches the maximum found in apprts such as GTK.
|
||||
pub const max_planes = 4;
|
||||
|
||||
/// Width of the texture in device pixels.
|
||||
width: u32,
|
||||
|
||||
/// Height of the texture in device pixels.
|
||||
height: u32,
|
||||
|
||||
/// DRM fourcc of the pixel format.
|
||||
/// Ghostty renders RGBA/BGRA8 premultiplied.
|
||||
fourcc: u32,
|
||||
|
||||
/// DRM modifier of the format.
|
||||
modifier: u64,
|
||||
|
||||
/// Whether the data is premultiplied.
|
||||
/// Ghostty's GL renderers output premultiplied alpha.
|
||||
premultiplied: bool,
|
||||
|
||||
/// The DMABUF planes for a presented frame. The DMABUF owns the fds
|
||||
/// and must either call `deinit` manually, or pass them to an apprt
|
||||
/// that consumes them.
|
||||
planes: Planes,
|
||||
|
||||
pub const Planes = struct {
|
||||
/// Number of planes. Valid planes are `planes[0..count]`.
|
||||
count: u8,
|
||||
|
||||
/// File descriptor for each plane.
|
||||
fds: [max_planes]std.posix.fd_t = @splat(-1),
|
||||
|
||||
/// Offset into the DMABUF where each plane starts, in bytes.
|
||||
offsets: [max_planes]c_int = @splat(0),
|
||||
|
||||
/// Strides of each plane, in bytes.
|
||||
strides: [max_planes]c_int = @splat(0),
|
||||
|
||||
/// Close all valid fds.
|
||||
pub fn deinit(self: Planes) void {
|
||||
for (self.fds[0..self.count]) |fd| {
|
||||
if (fd >= 0) _ = std.posix.system.close(fd);
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate the current planes. If any plane failed to export
|
||||
/// and has an invalid FD, we close all the known valid FDs
|
||||
/// and bail.
|
||||
pub fn validate(self: Planes) error{BadDmabuf}!void {
|
||||
var n_valid: usize = 0;
|
||||
while (n_valid < self.count) : (n_valid += 1) {
|
||||
if (self.fds[n_valid] < 0) {
|
||||
for (self.fds[0..n_valid]) |bad| _ = std.posix.system.close(bad);
|
||||
return error.BadDmabuf;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub fn deinit(self: Dmabuf) void {
|
||||
self.planes.deinit();
|
||||
}
|
||||
@@ -258,11 +258,6 @@ pub inline fn present(self: *Metal, target: Target, sync: bool) !void {
|
||||
}
|
||||
}
|
||||
|
||||
/// Present the last presented target again. (noop for Metal)
|
||||
pub inline fn presentLastTarget(self: *Metal) !void {
|
||||
_ = self;
|
||||
}
|
||||
|
||||
/// Returns the options to use when constructing buffers.
|
||||
pub inline fn bufferOptions(self: Metal) bufferpkg.Options {
|
||||
return .{
|
||||
|
||||
@@ -3,14 +3,15 @@ pub const OpenGL = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const builtin = @import("builtin");
|
||||
const gl = @import("opengl");
|
||||
const egl = gl.egl;
|
||||
const shadertoy = @import("shadertoy.zig");
|
||||
const apprt = @import("../apprt.zig");
|
||||
const font = @import("../font/main.zig");
|
||||
const configpkg = @import("../config.zig");
|
||||
const rendererpkg = @import("../renderer.zig");
|
||||
const Renderer = rendererpkg.GenericRenderer(OpenGL);
|
||||
const Dmabuf = @import("Dmabuf.zig");
|
||||
|
||||
pub const GraphicsAPI = OpenGL;
|
||||
pub const Target = @import("opengl/Target.zig");
|
||||
@@ -27,9 +28,9 @@ pub const custom_shader_target: shadertoy.Target = .glsl;
|
||||
// The fragCoord for OpenGL shaders is +Y = up.
|
||||
pub const custom_shader_y_is_down = false;
|
||||
|
||||
/// Because OpenGL's frame completion is always
|
||||
/// sync, we have no need for multi-buffering.
|
||||
pub const swap_chain_count = 1;
|
||||
/// Triple-buffering gives the GPU room to pipeline renders without
|
||||
/// having to wait on the apprt consuming previous frames.
|
||||
pub const swap_chain_count = 3;
|
||||
|
||||
const log = std.log.scoped(.opengl);
|
||||
|
||||
@@ -42,20 +43,69 @@ alloc: std.mem.Allocator,
|
||||
/// Alpha blending mode
|
||||
blending: configpkg.Config.AlphaBlending,
|
||||
|
||||
/// The most recently presented target, in case we need to present it again.
|
||||
last_target: ?Target = null,
|
||||
egl_display: *gl.egl.Display,
|
||||
egl_context: *gl.egl.Context,
|
||||
|
||||
pub fn init(alloc: Allocator, opts: rendererpkg.Options) !OpenGL {
|
||||
try egl.load();
|
||||
|
||||
const display: *egl.Display = try .init(egl.c.EGL_DEFAULT_DISPLAY);
|
||||
|
||||
log.info("EGL vendor={s}", .{display.queryString(.vendor) orelse "(unknown)"});
|
||||
log.info("EGL extensions={s}", .{display.queryString(.extensions) orelse "(unknown)"});
|
||||
|
||||
try egl.bindApi(egl.c.EGL_OPENGL_API);
|
||||
|
||||
// Choose a config. We need a config that is renderable with
|
||||
// OpenGL and a RGBA8 color buffer.
|
||||
const config = egl.Config.choose(display, &.{
|
||||
egl.c.EGL_RENDERABLE_TYPE, egl.c.EGL_OPENGL_BIT,
|
||||
egl.c.EGL_RED_SIZE, 8,
|
||||
egl.c.EGL_GREEN_SIZE, 8,
|
||||
egl.c.EGL_BLUE_SIZE, 8,
|
||||
egl.c.EGL_ALPHA_SIZE, 8,
|
||||
egl.c.EGL_NONE,
|
||||
}) catch |err| {
|
||||
log.warn("failed to choose config err={}", .{err});
|
||||
return err;
|
||||
};
|
||||
|
||||
// Create our context.
|
||||
const context = egl.Context.create(display, config, null, &.{
|
||||
egl.c.EGL_CONTEXT_MAJOR_VERSION, MIN_VERSION_MAJOR,
|
||||
egl.c.EGL_CONTEXT_MINOR_VERSION, MIN_VERSION_MINOR,
|
||||
egl.c.EGL_CONTEXT_OPENGL_PROFILE_MASK, egl.c.EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT,
|
||||
egl.c.EGL_NONE,
|
||||
}) catch |err| {
|
||||
log.warn("failed to create EGL context err={}", .{err});
|
||||
return err;
|
||||
};
|
||||
errdefer context.destroy(display) catch {};
|
||||
|
||||
display.makeCurrent(null, null, context) catch |err| {
|
||||
log.warn("failed to make EGL context current err={}", .{err});
|
||||
return err;
|
||||
};
|
||||
|
||||
// Release current so that the main thread
|
||||
// doesn't hold onto the GL context forever.
|
||||
defer display.releaseCurrent();
|
||||
|
||||
/// NOTE: This is an error{}!OpenGL instead of just OpenGL for parity with
|
||||
/// Metal, since it needs to be fallible so does this, even though it
|
||||
/// can't actually fail.
|
||||
pub fn init(alloc: Allocator, opts: rendererpkg.Options) error{}!OpenGL {
|
||||
return .{
|
||||
.alloc = alloc,
|
||||
.blending = opts.config.blending,
|
||||
.egl_display = display,
|
||||
.egl_context = context,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *OpenGL) void {
|
||||
self.egl_display.releaseCurrent();
|
||||
self.egl_context.destroy(self.egl_display) catch {};
|
||||
|
||||
// Do not destroy the EGL display here as
|
||||
// it is shared across the entire process.
|
||||
// It will get automatically torn down by the OS.
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
@@ -158,95 +208,43 @@ fn prepareContext(getProcAddress: anytype) !void {
|
||||
try gl.enable(gl.c.GL_FRAMEBUFFER_SRGB);
|
||||
}
|
||||
|
||||
/// This is called early right after surface creation.
|
||||
pub fn surfaceInit(surface: *apprt.Surface) !void {
|
||||
/// Callback called by renderer.Thread when it begins. Called on the render
|
||||
/// thread. The EGL context was created at `init` time on the main thread;
|
||||
/// here we (re)bind it to this thread and load the thread-local glad
|
||||
/// function pointers so all subsequent GL work on this thread is valid.
|
||||
pub fn threadEnter(self: *OpenGL, surface: *apprt.Surface) !void {
|
||||
_ = surface;
|
||||
|
||||
switch (apprt.runtime) {
|
||||
else => @compileError("unsupported app runtime for OpenGL"),
|
||||
|
||||
// GTK uses global OpenGL context so we load from null.
|
||||
apprt.gtk,
|
||||
=> try prepareContext(null),
|
||||
|
||||
apprt.embedded => {
|
||||
// TODO(mitchellh): this does nothing today to allow libghostty
|
||||
// to compile for OpenGL targets but libghostty is strictly
|
||||
// broken for rendering on this platforms.
|
||||
},
|
||||
}
|
||||
|
||||
// These are very noisy so this is commented, but easy to uncomment
|
||||
// whenever we need to check the OpenGL extension list
|
||||
// if (builtin.mode == .Debug) {
|
||||
// var ext_iter = try gl.ext.iterator();
|
||||
// while (try ext_iter.next()) |ext| {
|
||||
// log.debug("OpenGL extension available name={s}", .{ext});
|
||||
// }
|
||||
// }
|
||||
try self.egl_display.makeCurrent(null, null, self.egl_context);
|
||||
// Load our function pointers for this thread's threadlocal.
|
||||
try prepareContext(&gl.egl.getProcAddress);
|
||||
}
|
||||
|
||||
/// This is called just prior to spinning up the renderer
|
||||
/// thread for final main thread setup requirements.
|
||||
pub fn finalizeSurfaceInit(self: *const OpenGL, surface: *apprt.Surface) !void {
|
||||
_ = self;
|
||||
_ = surface;
|
||||
/// Callback called by renderer.Thread when it exits. Called on the render
|
||||
/// thread; unbinds the context from this thread so it can be destroyed on
|
||||
/// the main thread.
|
||||
pub fn threadExit(self: *OpenGL) void {
|
||||
self.egl_display.releaseCurrent();
|
||||
gl.glad.unload();
|
||||
}
|
||||
|
||||
/// Callback called by renderer.Thread when it begins.
|
||||
pub fn threadEnter(self: *const OpenGL, surface: *apprt.Surface) !void {
|
||||
/// Get the current size of the runtime surface.
|
||||
pub fn surfaceSize(self: *const OpenGL) !struct { width: u32, height: u32 } {
|
||||
_ = self;
|
||||
_ = surface;
|
||||
|
||||
switch (apprt.runtime) {
|
||||
else => @compileError("unsupported app runtime for OpenGL"),
|
||||
|
||||
apprt.gtk => {
|
||||
// GTK doesn't support threaded OpenGL operations as far as I can
|
||||
// tell, so we use the renderer thread to setup all the state
|
||||
// but then do the actual draws and texture syncs and all that
|
||||
// on the main thread. As such, we don't do anything here.
|
||||
},
|
||||
|
||||
apprt.embedded => {
|
||||
// TODO(mitchellh): this does nothing today to allow libghostty
|
||||
// to compile for OpenGL targets but libghostty is strictly
|
||||
// broken for rendering on this platforms.
|
||||
},
|
||||
}
|
||||
var viewport: [4]gl.c.GLint = undefined;
|
||||
gl.glad.context.GetIntegerv.?(gl.c.GL_VIEWPORT, &viewport);
|
||||
return .{
|
||||
.width = @intCast(viewport[2]),
|
||||
.height = @intCast(viewport[3]),
|
||||
};
|
||||
}
|
||||
|
||||
/// Callback called by renderer.Thread when it exits.
|
||||
pub fn threadExit(self: *const OpenGL) void {
|
||||
/// Set the GL viewport to cover the given size in device pixels.
|
||||
///
|
||||
/// This used to be automatically called by the GtkGLArea upon resizing,
|
||||
/// but now we need to do this manually.
|
||||
pub fn setViewport(self: *const OpenGL, width: u32, height: u32) void {
|
||||
_ = self;
|
||||
|
||||
switch (apprt.runtime) {
|
||||
else => @compileError("unsupported app runtime for OpenGL"),
|
||||
|
||||
apprt.gtk => {
|
||||
// We don't need to do any unloading for GTK because we may
|
||||
// be sharing the global bindings with other windows.
|
||||
},
|
||||
|
||||
apprt.embedded => {
|
||||
// TODO: see threadEnter
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn displayRealized(self: *const OpenGL) void {
|
||||
_ = self;
|
||||
|
||||
switch (apprt.runtime) {
|
||||
apprt.gtk => prepareContext(null) catch |err| {
|
||||
log.warn(
|
||||
"Error preparing GL context in displayRealized, err={}",
|
||||
.{err},
|
||||
);
|
||||
},
|
||||
|
||||
else => @compileError("only GTK should be calling displayRealized"),
|
||||
}
|
||||
gl.glad.context.Viewport.?(0, 0, @intCast(width), @intCast(height));
|
||||
}
|
||||
|
||||
/// Actions taken before doing anything in `drawFrame`.
|
||||
@@ -275,71 +273,58 @@ pub fn initShaders(
|
||||
);
|
||||
}
|
||||
|
||||
/// Get the current size of the runtime surface.
|
||||
pub fn surfaceSize(self: *const OpenGL) !struct { width: u32, height: u32 } {
|
||||
_ = self;
|
||||
var viewport: [4]gl.c.GLint = undefined;
|
||||
gl.glad.context.GetIntegerv.?(gl.c.GL_VIEWPORT, &viewport);
|
||||
return .{
|
||||
.width = @intCast(viewport[2]),
|
||||
.height = @intCast(viewport[3]),
|
||||
};
|
||||
}
|
||||
|
||||
/// Initialize a new render target which can be presented by this API.
|
||||
pub fn initTarget(self: *const OpenGL, width: usize, height: usize) !Target {
|
||||
_ = self;
|
||||
return Target.init(.{
|
||||
.internal_format = if (self.blending.isLinear()) .srgba else .rgba,
|
||||
.width = width,
|
||||
.height = height,
|
||||
});
|
||||
}
|
||||
|
||||
/// Present the provided target.
|
||||
pub fn present(self: *OpenGL, target: Target) !void {
|
||||
// In order to present a target we blit it to the default framebuffer.
|
||||
/// Export a rendered target. Caller takes ownership
|
||||
/// of the frame and is responsible for freeing it.
|
||||
///
|
||||
/// This runs on the render thread.
|
||||
pub fn present(self: *OpenGL, target: Target) !ExportedFrame {
|
||||
if (target.exportDmabuf(self.egl_display, self.egl_context)) |dmabuf| {
|
||||
return .{ .dmabuf = dmabuf };
|
||||
} else |_| {
|
||||
// If DMABUFs fail, then use CPU buffers
|
||||
return .{ .memory = .{
|
||||
.width = @intCast(target.width),
|
||||
.height = @intCast(target.height),
|
||||
.pixels = try target.readPixelsAlloc(self.alloc),
|
||||
.alloc = self.alloc,
|
||||
} };
|
||||
}
|
||||
}
|
||||
|
||||
// We disable GL_FRAMEBUFFER_SRGB while doing this blit, otherwise the
|
||||
// values may be linearized as they're copied, but even though the draw
|
||||
// framebuffer has a linear internal format, the values in it should be
|
||||
// sRGB, not linear!
|
||||
try gl.disable(gl.c.GL_FRAMEBUFFER_SRGB);
|
||||
defer gl.enable(gl.c.GL_FRAMEBUFFER_SRGB) catch |err| {
|
||||
log.err("Error re-enabling GL_FRAMEBUFFER_SRGB, err={}", .{err});
|
||||
/// A finished frame exported for presentation by the apprt.
|
||||
pub const ExportedFrame = union(enum) {
|
||||
dmabuf: Dmabuf,
|
||||
memory: Memory,
|
||||
|
||||
/// RGBA8 pixel data with premultiplied alpha, tightly packed
|
||||
/// (`width * 4` bytes per row), in CPU memory.
|
||||
pub const Memory = struct {
|
||||
width: u32,
|
||||
height: u32,
|
||||
pixels: []u8,
|
||||
alloc: Allocator,
|
||||
|
||||
pub fn deinit(self: Memory) void {
|
||||
self.alloc.free(self.pixels);
|
||||
}
|
||||
};
|
||||
|
||||
// Bind the target for reading.
|
||||
const fbobind = try target.framebuffer.bind(.read);
|
||||
defer fbobind.unbind();
|
||||
|
||||
// Blit
|
||||
gl.glad.context.BlitFramebuffer.?(
|
||||
0,
|
||||
0,
|
||||
@intCast(target.width),
|
||||
@intCast(target.height),
|
||||
0,
|
||||
0,
|
||||
@intCast(target.width),
|
||||
@intCast(target.height),
|
||||
gl.c.GL_COLOR_BUFFER_BIT,
|
||||
gl.c.GL_NEAREST,
|
||||
);
|
||||
|
||||
// Keep track of this target in case we need to repeat it.
|
||||
self.last_target = target;
|
||||
}
|
||||
|
||||
/// Present the last presented target again.
|
||||
pub fn presentLastTarget(self: *OpenGL) !void {
|
||||
if (self.last_target) |target| try self.present(target);
|
||||
}
|
||||
|
||||
/// Called when the renderer released its GPU resources; the last
|
||||
/// presented target is deinited with them so we must drop our copy.
|
||||
pub fn gpuResourcesReleased(self: *OpenGL) void {
|
||||
self.last_target = null;
|
||||
}
|
||||
pub fn deinit(self: ExportedFrame) void {
|
||||
switch (self) {
|
||||
.dmabuf => |v| v.deinit(),
|
||||
.memory => |v| v.deinit(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// Returns the options to use when constructing buffers.
|
||||
pub inline fn bufferOptions(self: OpenGL) bufferpkg.Options {
|
||||
|
||||
@@ -13,7 +13,6 @@ const apprt = @import("../apprt.zig");
|
||||
const configpkg = @import("../config.zig");
|
||||
const terminalpkg = @import("../terminal/main.zig");
|
||||
const BlockingQueue = @import("../datastruct/main.zig").BlockingQueue;
|
||||
const App = @import("../App.zig");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
const log = std.log.scoped(.renderer_thread);
|
||||
@@ -80,9 +79,6 @@ state: *rendererpkg.State,
|
||||
/// this is a blocking queue so if it is full you will get errors (or block).
|
||||
mailbox: *Mailbox,
|
||||
|
||||
/// Mailbox to send messages to the app thread
|
||||
app_mailbox: App.Mailbox,
|
||||
|
||||
/// Configuration we need derived from the main config.
|
||||
config: DerivedConfig,
|
||||
|
||||
@@ -123,7 +119,6 @@ pub fn init(
|
||||
surface: *apprt.Surface,
|
||||
renderer_impl: *rendererpkg.Renderer,
|
||||
state: *rendererpkg.State,
|
||||
app_mailbox: App.Mailbox,
|
||||
) !Thread {
|
||||
// Create our event loop.
|
||||
var loop = try xev.Loop.init(.{});
|
||||
@@ -166,7 +161,6 @@ pub fn init(
|
||||
.renderer = renderer_impl,
|
||||
.state = state,
|
||||
.mailbox = mailbox,
|
||||
.app_mailbox = app_mailbox,
|
||||
};
|
||||
|
||||
// Only enable compression if we have it enabled... save some
|
||||
@@ -471,15 +465,8 @@ fn drawFrame(self: *Thread, now: bool) void {
|
||||
// when we're forced to via `now`.
|
||||
if (!now and self.renderer.hasVsync()) return;
|
||||
|
||||
if (apprt.must_draw_from_app_thread) {
|
||||
_ = self.app_mailbox.push(
|
||||
.{ .redraw_surface = self.surface },
|
||||
.{ .instant = {} },
|
||||
);
|
||||
} else {
|
||||
self.renderer.drawFrame(false) catch |err|
|
||||
log.warn("error drawing err={}", .{err});
|
||||
}
|
||||
self.renderer.drawFrame(false) catch |err|
|
||||
log.warn("error drawing err={}", .{err});
|
||||
}
|
||||
|
||||
fn wakeupCallback(
|
||||
@@ -564,6 +551,17 @@ fn renderCallback(
|
||||
return .disarm;
|
||||
};
|
||||
|
||||
// If the display is now unrealized, release GPU resources now
|
||||
// we're on the render thread, and do not try to update and draw
|
||||
// this frame.
|
||||
if (!t.renderer.display_realized) {
|
||||
t.renderer.draw_mutex.lockUncancelable(global.io());
|
||||
defer t.renderer.draw_mutex.unlock(global.io());
|
||||
|
||||
t.renderer.releaseGpuResources();
|
||||
return .disarm;
|
||||
}
|
||||
|
||||
// If we're not visible there's no point spending CPU rebuilding cells —
|
||||
// we'll catch up when the .visible mailbox message flips us back on.
|
||||
// Kitty graphics animations pause with us and resume on visibility.
|
||||
|
||||
@@ -80,12 +80,18 @@ const log = std.log.scoped(.generic_renderer);
|
||||
///
|
||||
/// [ Texture ] - An abstraction over a GPU texture.
|
||||
///
|
||||
/// [ ExportedFrame ] - A finished frame ready to be consumed by the apprt
|
||||
/// in case that the frame needs to be composited with
|
||||
/// UI elements by the graphical toolkit manually.
|
||||
///
|
||||
pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
return struct {
|
||||
const Self = @This();
|
||||
|
||||
pub const API = GraphicsAPI;
|
||||
|
||||
pub const ExportedFrame = if (@hasDecl(GraphicsAPI, "ExportedFrame")) GraphicsAPI.ExportedFrame else void;
|
||||
|
||||
const Target = GraphicsAPI.Target;
|
||||
const Buffer = GraphicsAPI.Buffer;
|
||||
const Sampler = GraphicsAPI.Sampler;
|
||||
@@ -108,6 +114,15 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
/// The mailbox for communicating with the window.
|
||||
surface_mailbox: apprt.surface.Mailbox,
|
||||
|
||||
/// The latest exported frame ready to be consumed by an apprt
|
||||
/// who needs to manually composite the frame with UI elements.
|
||||
/// Previously exported frames are released when a new frame is
|
||||
/// exported and pushed onto the queue.
|
||||
///
|
||||
/// Unused if the renderer does not need to export frames to
|
||||
/// present its rendered frame.
|
||||
latest_frame: LatestFrame = .{},
|
||||
|
||||
/// Current font metrics defining our grid.
|
||||
grid_metrics: font.Metrics,
|
||||
|
||||
@@ -678,13 +693,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
|
||||
const has_custom_shaders = options.config.custom_shaders.value.items.len > 0;
|
||||
|
||||
// Prepare our swap chain
|
||||
var swap_chain = try SwapChain.init(
|
||||
api,
|
||||
has_custom_shaders,
|
||||
);
|
||||
errdefer swap_chain.deinit();
|
||||
|
||||
// Create the font shaper.
|
||||
var font_shaper = try font.Shaper.init(alloc, .{
|
||||
.features = options.config.font_features.items,
|
||||
@@ -783,16 +791,15 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
.font_shaper = font_shaper,
|
||||
.font_shaper_cache = font.ShaperCache.init(),
|
||||
|
||||
// Shaders (initialized below)
|
||||
.shaders = undefined,
|
||||
|
||||
// Graphics API stuff
|
||||
.api = api,
|
||||
.swap_chain = swap_chain,
|
||||
.swap_chain = null,
|
||||
.has_custom_shaders = has_custom_shaders,
|
||||
.reinitialize_shaders = true,
|
||||
// Shaders are initialized lazily on the render thread.
|
||||
.shaders = .uninit,
|
||||
};
|
||||
|
||||
try result.initShaders();
|
||||
|
||||
// Ensure our undefined values above are correctly initialized.
|
||||
result.updateFontGridUniforms();
|
||||
result.updateScreenSizeUniforms();
|
||||
@@ -803,11 +810,16 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
// This only deinitializes and frees CPU-side state
|
||||
// and does not free GPU resources like the swap chain and
|
||||
// shaders. Those are freed with `releaseGpuResources`.
|
||||
|
||||
if (self.overlay) |*overlay| overlay.deinit(self.alloc);
|
||||
self.terminal_state.deinit(self.alloc);
|
||||
if (self.search_selected_match) |*m| m.arena.deinit();
|
||||
if (self.search_matches) |*m| m.arena.deinit();
|
||||
if (self.swap_chain) |*sc| sc.deinit();
|
||||
|
||||
self.latest_frame.deinit(global.io());
|
||||
|
||||
if (DisplayLink != void) {
|
||||
if (self.display_link) |display_link| {
|
||||
@@ -822,22 +834,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
self.font_shaper_cache.deinit(self.alloc);
|
||||
|
||||
self.config.deinit();
|
||||
|
||||
self.images.deinit(self.alloc);
|
||||
|
||||
if (self.bg_image) |img| img.deinit(self.alloc);
|
||||
|
||||
self.deinitShaders();
|
||||
|
||||
self.api.deinit();
|
||||
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
fn deinitShaders(self: *Self) void {
|
||||
self.shaders.deinit(self.alloc);
|
||||
}
|
||||
|
||||
fn initShaders(self: *Self) !void {
|
||||
var arena = ArenaAllocator.init(self.alloc);
|
||||
defer arena.deinit();
|
||||
@@ -865,33 +866,38 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
self.has_custom_shaders = has_custom_shaders;
|
||||
}
|
||||
|
||||
/// This is called early right after surface creation.
|
||||
pub fn surfaceInit(surface: *apprt.Surface) !void {
|
||||
// If our API has to do things here, let it.
|
||||
if (@hasDecl(GraphicsAPI, "surfaceInit")) {
|
||||
try GraphicsAPI.surfaceInit(surface);
|
||||
}
|
||||
}
|
||||
|
||||
/// This is called just prior to spinning up the renderer thread for
|
||||
/// final main thread setup requirements.
|
||||
pub fn finalizeSurfaceInit(self: *Self, surface: *apprt.Surface) !void {
|
||||
// If our API has to do things to finalize surface init, let it.
|
||||
if (@hasDecl(GraphicsAPI, "finalizeSurfaceInit")) {
|
||||
try self.api.finalizeSurfaceInit(surface);
|
||||
}
|
||||
}
|
||||
|
||||
/// Callback called by renderer.Thread when it begins.
|
||||
pub fn threadEnter(self: *const Self, surface: *apprt.Surface) !void {
|
||||
pub fn threadEnter(self: *Self, surface: *apprt.Surface) !void {
|
||||
// If our API has to do things on thread enter, let it.
|
||||
if (@hasDecl(GraphicsAPI, "threadEnter")) {
|
||||
try self.api.threadEnter(surface);
|
||||
}
|
||||
}
|
||||
|
||||
/// Callback called by renderer.Thread when it exits.
|
||||
pub fn threadExit(self: *const Self) void {
|
||||
/// Callback called by renderer.Thread when it exits. Called on the
|
||||
/// render thread. Releases all GPU resources before the API tears down
|
||||
/// its context, since after this the GL context will be gone.
|
||||
pub fn threadExit(self: *Self) void {
|
||||
{
|
||||
self.draw_mutex.lockUncancelable(global.io());
|
||||
defer self.draw_mutex.unlock(global.io());
|
||||
|
||||
// Release swap chain and shaders.
|
||||
self.releaseGpuResources();
|
||||
|
||||
// We don't release images in `releaseGpuResources`
|
||||
// since it can be called whenever the terminal is
|
||||
// occluded or unrealized, and we don't want to
|
||||
// reupload images every time that happens.
|
||||
self.images.deinit(self.alloc);
|
||||
self.images = .empty;
|
||||
|
||||
if (self.bg_image) |img| {
|
||||
img.deinit(self.alloc);
|
||||
self.bg_image = null;
|
||||
}
|
||||
}
|
||||
|
||||
// If our API has to do things on thread exit, let it.
|
||||
if (@hasDecl(GraphicsAPI, "threadExit")) {
|
||||
self.api.threadExit();
|
||||
@@ -929,57 +935,95 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
}
|
||||
|
||||
/// This is called by the GTK apprt after the surface is
|
||||
/// reinitialized due to any of the events mentioned in
|
||||
/// the doc comment for `displayUnrealized`.
|
||||
/// reinitialized (e.g. after the widget is re-realized following
|
||||
/// a display change or reparenting).
|
||||
pub fn displayRealized(self: *Self) !void {
|
||||
// If our API has to do things on realize, let it.
|
||||
if (@hasDecl(GraphicsAPI, "displayRealized")) {
|
||||
self.api.displayRealized();
|
||||
}
|
||||
|
||||
// Lock the draw mutex so that we can
|
||||
// safely reinitialize our GPU resources.
|
||||
// Lock the draw mutex so that we can safely update state.
|
||||
self.draw_mutex.lockUncancelable(global.io());
|
||||
defer self.draw_mutex.unlock(global.io());
|
||||
|
||||
// We assume that the swap chain was deinited in
|
||||
// `displayUnrealized`. If not, we have a problem.
|
||||
assert(self.swap_chain == null);
|
||||
assert(!self.display_realized);
|
||||
|
||||
// We reinitialize our shaders and our swap chain.
|
||||
try self.initShaders();
|
||||
self.swap_chain = try SwapChain.init(
|
||||
self.api,
|
||||
self.has_custom_shaders,
|
||||
);
|
||||
// Mark the display as realized. The render thread will lazily
|
||||
// rebuild the swap chain and shaders on the next `drawFrame`,
|
||||
// which is the right place for GL resource creation (it
|
||||
// guarantees a current context on the render thread).
|
||||
self.display_realized = true;
|
||||
self.reinitialize_shaders = false;
|
||||
self.reinitialize_shaders = true;
|
||||
self.target_config_modified = 1;
|
||||
}
|
||||
|
||||
/// This is called by the GTK apprt when the surface is being destroyed.
|
||||
/// This can happen because the surface is being closed but also when
|
||||
/// moving the window between displays or splitting.
|
||||
/// This is called when the surface is being unrealized.
|
||||
/// This can happen because the surface is being closed but
|
||||
/// also when moving the window between displays or splitting.
|
||||
///
|
||||
/// This runs on the main thread and only updates CPU-side state
|
||||
/// here; resource cleanup happens on the render thread via
|
||||
/// `releaseGpuResources`.
|
||||
pub fn displayUnrealized(self: *Self) void {
|
||||
// If our API has to do things on unrealize, let it.
|
||||
if (@hasDecl(GraphicsAPI, "displayUnrealized")) {
|
||||
self.api.displayUnrealized();
|
||||
}
|
||||
|
||||
// Lock the draw mutex so that we can
|
||||
// safely deinitialize our GPU resources.
|
||||
// Lock the draw mutex so that we can safely update state.
|
||||
self.draw_mutex.lockUncancelable(global.io());
|
||||
defer self.draw_mutex.unlock(global.io());
|
||||
|
||||
// We deinit our swap chain and shaders. Clearing
|
||||
// `display_realized` ensures drawFrame doesn't attempt
|
||||
// to rebuild the swap chain (we have no GPU context);
|
||||
// displayRealized will.
|
||||
if (self.swap_chain) |*sc| sc.deinit();
|
||||
self.swap_chain = null;
|
||||
// Clearing `display_realized` ensures drawFrame doesn't attempt
|
||||
// to rebuild the swap chain or make any graphics API calls.
|
||||
// The actual GPU resource release is done by the render thread.
|
||||
self.display_realized = false;
|
||||
self.shaders.deinit(self.alloc);
|
||||
}
|
||||
|
||||
/// A thread-safe, single-slot "latest wins" queue. The render thread
|
||||
/// calls `push` with the latest frame; the apprt calls `take` in its
|
||||
/// snapshot handler to grab the most recent frame. Old frames are
|
||||
/// dropped and released. For a terminal this is correct — we never
|
||||
/// want to queue up frames behind a slow compositor.
|
||||
const LatestFrame = struct {
|
||||
const Self = @This();
|
||||
mutex: std.Io.Mutex = .init,
|
||||
latest: ?ExportedFrame = null,
|
||||
|
||||
pub fn push(self: *LatestFrame, io: std.Io, value: ExportedFrame) void {
|
||||
self.mutex.lockUncancelable(io);
|
||||
defer self.mutex.unlock(io);
|
||||
if (self.latest) |*old| old.deinit();
|
||||
self.latest = value;
|
||||
}
|
||||
|
||||
pub fn take(self: *LatestFrame, io: std.Io) ?ExportedFrame {
|
||||
self.mutex.lockUncancelable(io);
|
||||
defer self.mutex.unlock(io);
|
||||
const result = self.latest orelse return null;
|
||||
self.latest = null;
|
||||
return result;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *LatestFrame, io: std.Io) void {
|
||||
self.mutex.lockUncancelable(io);
|
||||
defer self.mutex.unlock(io);
|
||||
if (self.latest) |*v| v.deinit();
|
||||
self.latest = null;
|
||||
}
|
||||
};
|
||||
|
||||
/// Push the latest completed frame, replacing if one previously
|
||||
/// existed. Called on the render thread.
|
||||
///
|
||||
/// Has no effect for renderers that do not export frames
|
||||
/// (i.e. `ExportedFrame == void`).
|
||||
pub fn pushFrame(self: *Self, frame: ExportedFrame) void {
|
||||
return self.latest_frame.push(global.io(), frame);
|
||||
}
|
||||
|
||||
/// Take the latest completed frame for the apprt to composite.
|
||||
/// Returns null if no frame is available. The caller takes
|
||||
/// ownership of the returned frame. Called on the main thread.
|
||||
///
|
||||
/// Has no effect for renderers that do not export frames
|
||||
/// (i.e. `ExportedFrame == void`).
|
||||
pub fn takeFrame(self: *Self) ?ExportedFrame {
|
||||
return self.latest_frame.take(global.io());
|
||||
}
|
||||
|
||||
fn displayLinkCallback(
|
||||
@@ -1107,46 +1151,37 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
self.visible = visible;
|
||||
self.syncDisplayLink(null, null);
|
||||
|
||||
// When we're hidden, release our GPU resources if GPU
|
||||
// operations are allowed from this thread. Apprts where
|
||||
// they aren't (GTK owns the OpenGL context on the app
|
||||
// thread) call `releaseGpuResources` at the appropriate
|
||||
// time instead.
|
||||
if (comptime !apprt.must_draw_from_app_thread) {
|
||||
if (!visible) self.releaseGpuResources();
|
||||
// When we're hidden, release our GPU resources.
|
||||
if (!visible) {
|
||||
self.draw_mutex.lockUncancelable(global.io());
|
||||
defer self.draw_mutex.unlock(global.io());
|
||||
self.releaseGpuResources();
|
||||
}
|
||||
}
|
||||
|
||||
/// Release the GPU resources we hold while the surface is not
|
||||
/// visible. Today this is the swap chain (render targets, font
|
||||
/// atlas texture copies, cell buffers, custom shader textures),
|
||||
/// which makes up nearly all of a surface's GPU memory usage;
|
||||
/// a hidden surface doesn't draw, so it doesn't need it. The
|
||||
/// swap chain is rebuilt on the next `drawFrame`.
|
||||
/// which makes up nearly all of a surface's GPU memory usage.
|
||||
/// The swap chain is rebuilt on the next `drawFrame`.
|
||||
///
|
||||
/// This is safe to call in any state; resources that are
|
||||
/// already released are skipped.
|
||||
/// Note that images are NOT released here since we don't want
|
||||
/// to reupload images every time the terminal is brought back
|
||||
/// from being occluded or unrealized.
|
||||
///
|
||||
/// For OpenGL this must be called on the app thread with the
|
||||
/// GL context current (the same requirement as
|
||||
/// `displayUnrealized`). Other APIs may call this from the
|
||||
/// render thread; see `apprt.must_draw_from_app_thread`.
|
||||
/// Caller must lock the draw mutex before calling this function.
|
||||
/// Resources that are already released are skipped.
|
||||
pub fn releaseGpuResources(self: *Self) void {
|
||||
self.draw_mutex.lockUncancelable(global.io());
|
||||
defer self.draw_mutex.unlock(global.io());
|
||||
|
||||
if (self.swap_chain) |*sc| {
|
||||
// Waits for any in-flight frames to complete, then
|
||||
// frees all GPU resources.
|
||||
sc.deinit();
|
||||
self.swap_chain = null;
|
||||
}
|
||||
|
||||
// Let the API drop any references it holds to swap
|
||||
// chain resources (e.g. OpenGL's last presented
|
||||
// target).
|
||||
if (comptime @hasDecl(GraphicsAPI, "gpuResourcesReleased")) {
|
||||
self.api.gpuResourcesReleased();
|
||||
}
|
||||
// Release the shaders as well if we're unrealized.
|
||||
if (!self.display_realized) {
|
||||
self.shaders.deinit(self.alloc);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1699,11 +1734,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
sync;
|
||||
|
||||
if (!needs_redraw) {
|
||||
// We still need to present the last target again, because the
|
||||
// apprt may be swapping buffers and display an outdated frame
|
||||
// if we don't draw something new.
|
||||
try self.api.presentLastTarget();
|
||||
|
||||
// Ask our caller to resync the display link once the draw
|
||||
// mutex is released, because we can probably pause the
|
||||
// display link at this point.
|
||||
@@ -1831,6 +1861,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
// would require us to do color space conversion on the
|
||||
// CPU-side. In the future when we have utilities for
|
||||
// that we should remove this step and use clear_color.
|
||||
|
||||
if (self.bg_image) |img| switch (img) {
|
||||
.ready => |texture| pass.step(.{
|
||||
.pipeline = self.shaders.pipelines.bg_image,
|
||||
@@ -2155,12 +2186,15 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
self.draw_mutex.lockUncancelable(global.io());
|
||||
defer self.draw_mutex.unlock(global.io());
|
||||
|
||||
// We only actually need the padding from this,
|
||||
// everything else is derived elsewhere.
|
||||
self.size.padding = size.padding;
|
||||
|
||||
self.size = size;
|
||||
self.updateScreenSizeUniforms();
|
||||
|
||||
// Some graphics APIs need to manually update their viewport,
|
||||
// like OpenGL. Do so here.
|
||||
if (@hasDecl(GraphicsAPI, "setViewport")) {
|
||||
self.api.setViewport(self.size.screen.width, self.size.screen.height);
|
||||
}
|
||||
|
||||
log.debug("screen size size={}", .{size});
|
||||
}
|
||||
|
||||
|
||||
@@ -105,6 +105,13 @@ pub const Shaders = struct {
|
||||
/// of shaders it will just be ignored, to prevent double-free.
|
||||
defunct: bool = false,
|
||||
|
||||
pub const uninit: Shaders = .{
|
||||
.library = undefined,
|
||||
.pipelines = undefined,
|
||||
.post_pipelines = &.{},
|
||||
.defunct = true,
|
||||
};
|
||||
|
||||
/// Initialize our shader set.
|
||||
///
|
||||
/// "post_shaders" is an optional list of postprocess shaders to run
|
||||
|
||||
@@ -5,6 +5,7 @@ const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const gl = @import("opengl");
|
||||
|
||||
const global = @import("../../global.zig");
|
||||
const Renderer = @import("../generic.zig").Renderer(OpenGL);
|
||||
const OpenGL = @import("../OpenGL.zig");
|
||||
const Target = @import("Target.zig");
|
||||
@@ -51,7 +52,8 @@ pub inline fn renderPass(
|
||||
///
|
||||
/// If `sync` is true, this will block until the frame is presented.
|
||||
///
|
||||
/// NOTE: For OpenGL, `sync` is ignored and we always block.
|
||||
/// NOTE: For OpenGL, `sync` is ignored and we never block, instead
|
||||
/// pushing the newly presented and exported frame to the frame queue.
|
||||
pub fn complete(self: *const Self, sync: bool) void {
|
||||
_ = sync;
|
||||
gl.finish();
|
||||
@@ -59,11 +61,18 @@ pub fn complete(self: *const Self, sync: bool) void {
|
||||
// If there are any GL errors, consider the frame unhealthy.
|
||||
const health: Health = if (gl.errors.getError()) .healthy else |_| .unhealthy;
|
||||
|
||||
// If the frame is healthy, present it.
|
||||
if (health == .healthy) {
|
||||
self.renderer.api.present(self.target.*) catch |err| {
|
||||
log.err("Failed to present render target: err={}", .{err});
|
||||
// If the frame is healthy, export it and push to the present queue.
|
||||
// The apprt pulls from this queue in its snapshot handler.
|
||||
if (health == .healthy) frame: {
|
||||
const frame = self.renderer.api.present(self.target.*) catch |err| {
|
||||
log.warn("failed to present render target: err={}", .{err});
|
||||
break :frame;
|
||||
};
|
||||
|
||||
self.renderer.pushFrame(frame);
|
||||
|
||||
// Notify the surface that it should redraw
|
||||
_ = self.renderer.surface_mailbox.push(.redraw, .{ .forever = {} });
|
||||
}
|
||||
|
||||
// Report the health to the renderer.
|
||||
|
||||
@@ -1,11 +1,29 @@
|
||||
//! Represents a render target.
|
||||
//!
|
||||
//! In this case, an OpenGL renderbuffer-backed framebuffer.
|
||||
//! In this case, a texture-backed framebuffer. The color attachment is a
|
||||
//! `GL_TEXTURE_2D` texture instead of a renderbuffer so that we can create
|
||||
//! an EGLImage from it and export the rendered frame as a dma-buf for
|
||||
//! presentation by the apprt.
|
||||
//!
|
||||
//! We use two textures:
|
||||
//!
|
||||
//! - `texture`: `GL_SRGB8_ALPHA8`. We render to this. With
|
||||
//! `GL_FRAMEBUFFER_SRGB` enabled, the GPU automatically converts linear
|
||||
//! shader output to sRGB on write. This is required for the
|
||||
//! linear-blending color pipeline to produce correct output.
|
||||
//!
|
||||
//! - `export_texture`: `GL_RGBA8`. This is the texture we actually export
|
||||
//! as a dma-buf. Mesa cannot export `GL_SRGB8_ALPHA8` textures to
|
||||
//! dma-buf, so we blit the rendered sRGB texture into this plain RGBA8
|
||||
//! texture (the blit copies the already-sRGB-encoded pixel values
|
||||
//! verbatim) and export that instead.
|
||||
const Self = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const gl = @import("opengl");
|
||||
const egl = gl.egl;
|
||||
const Dmabuf = @import("../Dmabuf.zig");
|
||||
|
||||
const log = std.log.scoped(.opengl);
|
||||
|
||||
@@ -15,16 +33,19 @@ pub const Options = struct {
|
||||
width: usize,
|
||||
/// Desired height
|
||||
height: usize,
|
||||
|
||||
/// Internal format for the renderbuffer.
|
||||
internal_format: gl.Texture.InternalFormat,
|
||||
};
|
||||
|
||||
/// The underlying `gl.Framebuffer` instance.
|
||||
/// The framebuffer we render to.
|
||||
framebuffer: gl.Framebuffer,
|
||||
|
||||
/// The underlying `gl.Renderbuffer` instance.
|
||||
renderbuffer: gl.Renderbuffer,
|
||||
/// The sRGB color attachment texture we render to.
|
||||
texture: gl.Texture,
|
||||
|
||||
/// A plain RGBA8 texture + framebuffer that we blit `texture` into for
|
||||
/// dma-buf export. Mesa can't export sRGB textures, so we blit the
|
||||
/// already-sRGB-encoded pixels into this non-sRGB texture and export it.
|
||||
export_texture: gl.Texture,
|
||||
export_framebuffer: gl.Framebuffer,
|
||||
|
||||
/// Current width of this target.
|
||||
width: usize,
|
||||
@@ -32,23 +53,86 @@ width: usize,
|
||||
height: usize,
|
||||
|
||||
pub fn init(opts: Options) !Self {
|
||||
const rbo = try gl.Renderbuffer.create();
|
||||
const bound_rbo = try rbo.bind();
|
||||
defer bound_rbo.unbind();
|
||||
try bound_rbo.storage(
|
||||
opts.internal_format,
|
||||
@intCast(opts.width),
|
||||
@intCast(opts.height),
|
||||
);
|
||||
const texture = try gl.Texture.create();
|
||||
errdefer texture.destroy();
|
||||
{
|
||||
const bound_tex = try texture.bind(.@"2D");
|
||||
defer bound_tex.unbind();
|
||||
try bound_tex.parameter(.MinFilter, @intFromEnum(gl.Texture.MinFilter.nearest));
|
||||
try bound_tex.parameter(.MagFilter, @intFromEnum(gl.Texture.MagFilter.nearest));
|
||||
try bound_tex.parameter(.WrapS, @intFromEnum(gl.Texture.Wrap.clamp_to_edge));
|
||||
try bound_tex.parameter(.WrapT, @intFromEnum(gl.Texture.Wrap.clamp_to_edge));
|
||||
try bound_tex.image2D(
|
||||
0,
|
||||
.srgba,
|
||||
@intCast(opts.width),
|
||||
@intCast(opts.height),
|
||||
.rgba,
|
||||
.UnsignedByte,
|
||||
null,
|
||||
);
|
||||
try bound_tex.parameter(.BaseLevel, @as(gl.c.GLint, 0));
|
||||
try bound_tex.parameter(.MaxLevel, @as(gl.c.GLint, 0));
|
||||
}
|
||||
|
||||
const fbo = try gl.Framebuffer.create();
|
||||
const bound_fbo = try fbo.bind(.framebuffer);
|
||||
defer bound_fbo.unbind();
|
||||
try bound_fbo.renderbuffer(.color0, rbo);
|
||||
errdefer fbo.destroy();
|
||||
{
|
||||
const bound_fbo = try fbo.bind(.framebuffer);
|
||||
defer bound_fbo.unbind();
|
||||
try bound_fbo.texture2D(.color0, .@"2D", texture, 0);
|
||||
switch (bound_fbo.checkStatus()) {
|
||||
.complete => {},
|
||||
else => |status| {
|
||||
log.warn("render framebuffer incomplete status={}", .{status});
|
||||
return error.FramebufferIncomplete;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// --- Export texture (plain RGBA8, for dma-buf export) ---
|
||||
const export_texture = try gl.Texture.create();
|
||||
errdefer export_texture.destroy();
|
||||
{
|
||||
const bound_tex = try export_texture.bind(.@"2D");
|
||||
defer bound_tex.unbind();
|
||||
try bound_tex.parameter(.MinFilter, @intFromEnum(gl.Texture.MinFilter.nearest));
|
||||
try bound_tex.parameter(.MagFilter, @intFromEnum(gl.Texture.MagFilter.nearest));
|
||||
try bound_tex.parameter(.WrapS, @intFromEnum(gl.Texture.Wrap.clamp_to_edge));
|
||||
try bound_tex.parameter(.WrapT, @intFromEnum(gl.Texture.Wrap.clamp_to_edge));
|
||||
try bound_tex.image2D(
|
||||
0,
|
||||
.rgba,
|
||||
@intCast(opts.width),
|
||||
@intCast(opts.height),
|
||||
.rgba,
|
||||
.UnsignedByte,
|
||||
null,
|
||||
);
|
||||
try bound_tex.parameter(.BaseLevel, @as(gl.c.GLint, 0));
|
||||
try bound_tex.parameter(.MaxLevel, @as(gl.c.GLint, 0));
|
||||
}
|
||||
|
||||
const export_fbo = try gl.Framebuffer.create();
|
||||
errdefer export_fbo.destroy();
|
||||
{
|
||||
const bound_fbo = try export_fbo.bind(.framebuffer);
|
||||
defer bound_fbo.unbind();
|
||||
try bound_fbo.texture2D(.color0, .@"2D", export_texture, 0);
|
||||
switch (bound_fbo.checkStatus()) {
|
||||
.complete => {},
|
||||
else => |status| {
|
||||
log.warn("export framebuffer incomplete status={}", .{status});
|
||||
return error.FramebufferIncomplete;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return .{
|
||||
.framebuffer = fbo,
|
||||
.renderbuffer = rbo,
|
||||
.texture = texture,
|
||||
.export_framebuffer = export_fbo,
|
||||
.export_texture = export_texture,
|
||||
.width = opts.width,
|
||||
.height = opts.height,
|
||||
};
|
||||
@@ -56,5 +140,102 @@ pub fn init(opts: Options) !Self {
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
self.framebuffer.destroy();
|
||||
self.renderbuffer.destroy();
|
||||
self.texture.destroy();
|
||||
self.export_framebuffer.destroy();
|
||||
self.export_texture.destroy();
|
||||
}
|
||||
|
||||
pub fn exportDmabuf(
|
||||
self: *const Self,
|
||||
display: *egl.Display,
|
||||
context: *egl.Context,
|
||||
) !Dmabuf {
|
||||
// We disable GL_FRAMEBUFFER_SRGB while doing this blit, otherwise
|
||||
// the values may be linearized as they're copied, but even though
|
||||
// the draw framebuffer has a linear internal format, the values in
|
||||
// it should be sRGB, not linear!
|
||||
try gl.disable(gl.c.GL_FRAMEBUFFER_SRGB);
|
||||
defer gl.enable(gl.c.GL_FRAMEBUFFER_SRGB) catch |err| {
|
||||
log.err("Error re-enabling GL_FRAMEBUFFER_SRGB, err={}", .{err});
|
||||
};
|
||||
|
||||
const read_bind = try self.framebuffer.bind(.read);
|
||||
defer read_bind.unbind();
|
||||
|
||||
const draw_bind = try self.export_framebuffer.bind(.draw);
|
||||
defer draw_bind.unbind();
|
||||
|
||||
try gl.blitFramebuffer(
|
||||
0,
|
||||
0,
|
||||
@intCast(self.width),
|
||||
@intCast(self.height),
|
||||
0,
|
||||
0,
|
||||
@intCast(self.width),
|
||||
@intCast(self.height),
|
||||
.{ .color_buffer_bit = true },
|
||||
.nearest,
|
||||
);
|
||||
|
||||
const image: *egl.Image = try .create(
|
||||
display,
|
||||
context,
|
||||
.{ .texture_2d = self.export_texture.id },
|
||||
&.{},
|
||||
);
|
||||
defer image.destroy(display) catch {};
|
||||
|
||||
const query = try image.exportDmabufQuery(display);
|
||||
|
||||
if (query.num_planes < 1 or query.num_planes > Dmabuf.max_planes) {
|
||||
log.err("DMABUF has too many planes={}", .{query.num_planes});
|
||||
return error.DmabufTooManyPlanes;
|
||||
}
|
||||
|
||||
var planes: Dmabuf.Planes = .{ .count = @intCast(query.num_planes) };
|
||||
try image.exportDmabuf(
|
||||
display,
|
||||
&planes.fds,
|
||||
&planes.strides,
|
||||
&planes.offsets,
|
||||
);
|
||||
|
||||
return .{
|
||||
.width = @intCast(self.width),
|
||||
.height = @intCast(self.height),
|
||||
// bitCast instead of intCast since the numerical value of fourccs
|
||||
// is rather meaningless, and we only care about the bit pattern
|
||||
.fourcc = @bitCast(query.fourcc),
|
||||
.modifier = query.modifier,
|
||||
.premultiplied = true,
|
||||
.planes = planes,
|
||||
};
|
||||
}
|
||||
|
||||
/// Read the current contents of the framebuffer into CPU memory.
|
||||
///
|
||||
/// This is used by the CPU readback presentation fallback. The
|
||||
/// returned data is tightly-packed RGBA8 with premultiplied alpha,
|
||||
/// i.e. `width * 4` bytes per row, containing sRGB-encoded values:
|
||||
/// `GL_FRAMEBUFFER_SRGB` has no effect on `ReadPixels`, so the stored
|
||||
/// values are returned verbatim.
|
||||
pub fn readPixelsAlloc(self: *const Self, alloc: std.mem.Allocator) ![]u8 {
|
||||
const bind = try self.framebuffer.bind(.read);
|
||||
defer bind.unbind();
|
||||
|
||||
const pixels = try alloc.alloc(u8, self.width * self.height * 4);
|
||||
errdefer alloc.free(pixels);
|
||||
|
||||
try gl.readPixels(
|
||||
0,
|
||||
0,
|
||||
@intCast(self.width),
|
||||
@intCast(self.height),
|
||||
.rgba,
|
||||
.unsigned_byte,
|
||||
pixels.ptr,
|
||||
);
|
||||
|
||||
return pixels;
|
||||
}
|
||||
|
||||
@@ -89,6 +89,12 @@ pub const Shaders = struct {
|
||||
/// of shaders it will just be ignored, to prevent double-free.
|
||||
defunct: bool = false,
|
||||
|
||||
pub const uninit: Shaders = .{
|
||||
.pipelines = undefined,
|
||||
.post_pipelines = &.{},
|
||||
.defunct = true,
|
||||
};
|
||||
|
||||
/// Initialize our shader set.
|
||||
///
|
||||
/// "post_shaders" is an optional list of postprocess shaders to run
|
||||
|
||||
Reference in New Issue
Block a user