gtk,opengl: free us from the clutches of GtkGLArea (#14052)

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:
Mitchell Hashimoto
2026-09-14 12:07:05 -07:00
committed by GitHub
45 changed files with 2066 additions and 636 deletions

View File

@@ -100,7 +100,6 @@ jobs:
- build-libghostty-vt-android
- build-libghostty-vt-macos
- build-libghostty-vt-windows
- build-libghostty-windows-gnu
- build-linux
- build-linux-libghostty
- build-nix
@@ -119,7 +118,6 @@ jobs:
- test-lib-vt
- test-lib-vt-pkgconfig
- test-macos
- test-windows
- pinact
- prettier
- swiftlint
@@ -956,20 +954,6 @@ jobs:
working-directory: example/c-vt-static
run: zig build
build-libghostty-windows-gnu:
runs-on: namespace-profile-ghostty-windows
timeout-minutes: 45
needs: test
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Zig
uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1
- name: Build libghostty (GNU ABI)
run: zig build -Dtarget=native-native-gnu -Dapp-runtime=none
build-linux:
strategy:
fail-fast: false
@@ -1708,21 +1692,6 @@ jobs:
- name: test
run: nix develop -c zig build test --system ${{ steps.deps.outputs.deps }}
test-windows:
if: github.repository == 'ghostty-org/ghostty' && needs.skip.outputs.skip != 'true'
needs: skip
runs-on: namespace-profile-ghostty-windows
timeout-minutes: 45
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Zig
uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1
- name: Test
run: zig build -Dapp-runtime=none test
test-i18n:
strategy:
fail-fast: false

View File

@@ -49,6 +49,7 @@
adwaita-icon-theme,
hicolor-icon-theme,
harfbuzz,
libglvnd,
libpng,
libxkbcommon,
libX11,
@@ -185,6 +186,7 @@ in
glslang
spirv-cross
libglvnd
libxkbcommon
libX11
libXcursor

View File

@@ -1,7 +1,7 @@
const Buffer = @This();
const std = @import("std");
const c = @import("c.zig").c;
const c = @import("c");
const errors = @import("errors.zig");
const glad = @import("glad.zig");

View File

@@ -1,7 +1,7 @@
const Framebuffer = @This();
const std = @import("std");
const c = @import("c.zig").c;
const c = @import("c");
const errors = @import("errors.zig");
const glad = @import("glad.zig");
const Texture = @import("Texture.zig");

View File

@@ -4,7 +4,7 @@ const std = @import("std");
const assert = std.debug.assert;
const log = std.log.scoped(.opengl);
const c = @import("c.zig").c;
const c = @import("c");
const Shader = @import("Shader.zig");
const errors = @import("errors.zig");
const glad = @import("glad.zig");

View File

@@ -1,7 +1,7 @@
const Renderbuffer = @This();
const std = @import("std");
const c = @import("c.zig").c;
const c = @import("c");
const errors = @import("errors.zig");
const glad = @import("glad.zig");

View File

@@ -1,7 +1,7 @@
const Sampler = @This();
const std = @import("std");
const c = @import("c.zig").c;
const c = @import("c");
const errors = @import("errors.zig");
const glad = @import("glad.zig");
const Texture = @import("Texture.zig");
@@ -24,8 +24,8 @@ pub fn bind(v: Sampler, index: c_uint) !void {
pub fn parameter(
self: Sampler,
name: Texture.Parameter,
value: anytype,
comptime name: Texture.Parameter,
value: name.Type(),
) errors.Error!void {
switch (@TypeOf(value)) {
c.GLint => glad.context.SamplerParameteri.?(

View File

@@ -4,7 +4,7 @@ const std = @import("std");
const assert = std.debug.assert;
const log = std.log.scoped(.opengl);
const c = @import("c.zig").c;
const c = @import("c");
const errors = @import("errors.zig");
const glad = @import("glad.zig");

View File

@@ -1,7 +1,7 @@
const Texture = @This();
const std = @import("std");
const c = @import("c.zig").c;
const c = @import("c");
const errors = @import("errors.zig");
const glad = @import("glad.zig");
@@ -33,36 +33,45 @@ pub fn destroy(v: Texture) void {
/// Enum for possible texture binding targets.
pub const Target = enum(c_uint) {
@"1D" = c.GL_TEXTURE_1D,
@"2D" = c.GL_TEXTURE_2D,
@"3D" = c.GL_TEXTURE_3D,
@"1DArray" = c.GL_TEXTURE_1D_ARRAY,
@"2DArray" = c.GL_TEXTURE_2D_ARRAY,
Rectangle = c.GL_TEXTURE_RECTANGLE,
CubeMap = c.GL_TEXTURE_CUBE_MAP,
Buffer = c.GL_TEXTURE_BUFFER,
@"2DMultisample" = c.GL_TEXTURE_2D_MULTISAMPLE,
@"2DMultisampleArray" = c.GL_TEXTURE_2D_MULTISAMPLE_ARRAY,
@"1d" = c.GL_TEXTURE_1D,
@"2d" = c.GL_TEXTURE_2D,
@"3d" = c.GL_TEXTURE_3D,
@"1d_array" = c.GL_TEXTURE_1D_ARRAY,
@"2d_array" = c.GL_TEXTURE_2D_ARRAY,
rectangle = c.GL_TEXTURE_RECTANGLE,
cube_map = c.GL_TEXTURE_CUBE_MAP,
buffer = c.GL_TEXTURE_BUFFER,
@"2d_multisample" = c.GL_TEXTURE_2D_MULTISAMPLE,
@"2d_multisample_array" = c.GL_TEXTURE_2D_MULTISAMPLE_ARRAY,
};
/// Enum for possible texture parameters.
pub const Parameter = enum(c_uint) {
BaseLevel = c.GL_TEXTURE_BASE_LEVEL,
CompareFunc = c.GL_TEXTURE_COMPARE_FUNC,
CompareMode = c.GL_TEXTURE_COMPARE_MODE,
LodBias = c.GL_TEXTURE_LOD_BIAS,
MinFilter = c.GL_TEXTURE_MIN_FILTER,
MagFilter = c.GL_TEXTURE_MAG_FILTER,
MinLod = c.GL_TEXTURE_MIN_LOD,
MaxLod = c.GL_TEXTURE_MAX_LOD,
MaxLevel = c.GL_TEXTURE_MAX_LEVEL,
SwizzleR = c.GL_TEXTURE_SWIZZLE_R,
SwizzleG = c.GL_TEXTURE_SWIZZLE_G,
SwizzleB = c.GL_TEXTURE_SWIZZLE_B,
SwizzleA = c.GL_TEXTURE_SWIZZLE_A,
WrapS = c.GL_TEXTURE_WRAP_S,
WrapT = c.GL_TEXTURE_WRAP_T,
WrapR = c.GL_TEXTURE_WRAP_R,
base_level = c.GL_TEXTURE_BASE_LEVEL,
compare_func = c.GL_TEXTURE_COMPARE_FUNC,
compare_mode = c.GL_TEXTURE_COMPARE_MODE,
lod_bias = c.GL_TEXTURE_LOD_BIAS,
min_filter = c.GL_TEXTURE_MIN_FILTER,
mag_filter = c.GL_TEXTURE_MAG_FILTER,
min_lod = c.GL_TEXTURE_MIN_LOD,
max_lod = c.GL_TEXTURE_MAX_LOD,
max_level = c.GL_TEXTURE_MAX_LEVEL,
swizzle_r = c.GL_TEXTURE_SWIZZLE_R,
swizzle_g = c.GL_TEXTURE_SWIZZLE_G,
swizzle_b = c.GL_TEXTURE_SWIZZLE_B,
swizzle_a = c.GL_TEXTURE_SWIZZLE_A,
wrap_s = c.GL_TEXTURE_WRAP_S,
wrap_t = c.GL_TEXTURE_WRAP_T,
wrap_r = c.GL_TEXTURE_WRAP_R,
pub fn Type(comptime self: Parameter) type {
return switch (self) {
.min_filter => MinFilter,
.mag_filter => MagFilter,
.wrap_s, .wrap_t, .wrap_r => Wrap,
else => c.GLint,
};
}
};
/// Internal format enum for texture images.
@@ -118,7 +127,7 @@ pub const Wrap = enum(c_int) {
/// Data type for texture images.
pub const DataType = enum(c_uint) {
UnsignedByte = c.GL_UNSIGNED_BYTE,
unsigned_byte = c.GL_UNSIGNED_BYTE,
// There are so many more that I haven't filled in.
_,
@@ -135,14 +144,21 @@ pub const Binding = struct {
glad.context.GenerateMipmap.?(@intFromEnum(b.target));
}
pub fn parameter(b: Binding, name: Parameter, value: anytype) errors.Error!void {
switch (@TypeOf(value)) {
pub fn parameter(b: Binding, comptime name: Parameter, value: name.Type()) errors.Error!void {
switch (name.Type()) {
c.GLint => glad.context.TexParameteri.?(
@intFromEnum(b.target),
@intFromEnum(name),
value,
),
else => unreachable,
else => switch (@typeInfo(name.Type())) {
.@"enum" => glad.context.TexParameteri.?(
@intFromEnum(b.target),
@intFromEnum(name),
@intFromEnum(value),
),
else => @compileError("unknown parameter type"),
},
}
try errors.getError();
}

View File

@@ -1,6 +1,6 @@
const VertexArray = @This();
const c = @import("c.zig").c;
const c = @import("c");
const glad = @import("glad.zig");
const errors = @import("errors.zig");

View File

@@ -1,6 +1,16 @@
const std = @import("std");
pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const c = b.addTranslateC(.{
.root_source_file = b.path("gl.c"),
.target = target,
.optimize = optimize,
});
c.addIncludePath(b.path("../../vendor/glad/include"));
const module = b.addModule("opengl", .{ .root_source_file = b.path("main.zig") });
module.addIncludePath(b.path("../../vendor/glad/include"));
module.addImport("c", c.createModule());
}

View File

@@ -1,3 +0,0 @@
pub const c = @cImport({
@cInclude("glad/gl.h");
});

View File

@@ -1,7 +1,9 @@
const c = @import("c.zig").c;
const std = @import("std");
const c = @import("c");
const errors = @import("errors.zig");
const glad = @import("glad.zig");
const Primitive = @import("primitives.zig").Primitive;
const Texture = @import("Texture.zig");
pub fn clearColor(r: f32, g: f32, b: f32, a: f32) void {
glad.context.ClearColor.?(r, g, b, a);
@@ -75,11 +77,60 @@ pub fn blendFunc(sfactor: c.GLenum, dfactor: c.GLenum) !void {
pub fn viewport(x: c.GLint, y: c.GLint, width: c.GLsizei, height: c.GLsizei) !void {
glad.context.Viewport.?(x, y, width, height);
try errors.getError();
}
pub fn readPixels(
x: c.GLint,
y: c.GLint,
width: c.GLsizei,
height: c.GLsizei,
format: Texture.Format,
typ: Texture.DataType,
data: ?*anyopaque,
) !void {
glad.context.ReadPixels.?(
x,
y,
width,
height,
@intFromEnum(format),
@intFromEnum(typ),
data,
);
try errors.getError();
}
pub fn blitFramebuffer(
src_x0: c.GLint,
src_y0: c.GLint,
src_x1: c.GLint,
src_y1: c.GLint,
dst_x0: c.GLint,
dst_y0: c.GLint,
dst_x1: c.GLint,
dst_y1: c.GLint,
mask: BlitMask,
filter: Texture.MagFilter,
) !void {
glad.context.BlitFramebuffer.?(
src_x0,
src_y0,
src_x1,
src_y1,
dst_x0,
dst_y0,
dst_x1,
dst_y1,
@bitCast(mask),
@intCast(@intFromEnum(filter)),
);
try errors.getError();
}
pub fn pixelStore(mode: c.GLenum, value: anytype) !void {
switch (@typeInfo(@TypeOf(value))) {
.ComptimeInt, .Int => glad.context.PixelStorei.?(mode, value),
.comptime_int, .int => glad.context.PixelStorei.?(mode, value),
else => unreachable,
}
try errors.getError();
@@ -92,3 +143,13 @@ pub fn finish() void {
pub fn flush() void {
glad.context.Flush.?();
}
pub const BlitMask = packed struct(c.GLbitfield) {
_pad1: u8 = 0,
depth_buffer_bit: bool = false,
_pad2: u1 = 0,
stencil_buffer_bit: bool = false,
_pad3: u3 = 0,
color_buffer_bit: bool = false,
_pad4: std.meta.Int(.unsigned, @bitSizeOf(c.GLbitfield) - 15) = 0,
};

267
pkg/opengl/egl.zig Normal file
View File

@@ -0,0 +1,267 @@
//! Thin EGL bindings via GLAD.
//!
//! Only types and functions used in Ghostty are modelled,
//! although the API design is rather flexible and can be extended
//! to add whatever is required. Drop down to `gl.egl.c` to access
//! raw EGL functions.
//!
//! Call `load()` once before using any EGL extension functions.
const std = @import("std");
pub const c = @import("c");
const log = std.log.scoped(.opengl_egl);
pub fn load() error{EglInitFailed}!void {
if (c.gladLoadEGL() == 0) return error.EglInitFailed;
}
/// Wraps `eglGetProcAddress` for GLAD.
pub fn getProcAddress(name: [*c]const u8) callconv(.c) ?*const fn () callconv(.c) void {
return c.eglGetProcAddress(name);
}
pub const Error = error{
NotInitialized,
BadAccess,
BadAlloc,
BadAttribute,
BadContext,
BadConfig,
BadCurrentSurface,
BadDisplay,
BadSurface,
BadMatch,
BadParameter,
BadNativePixmap,
BadNativeWindow,
ContextLost,
Unknown,
};
pub fn getError() Error!void {
return switch (c.eglGetError()) {
c.EGL_SUCCESS => {},
c.EGL_NOT_INITIALIZED => error.NotInitialized,
c.EGL_BAD_ACCESS => error.BadAccess,
c.EGL_BAD_ALLOC => error.BadAlloc,
c.EGL_BAD_ATTRIBUTE => error.BadAttribute,
c.EGL_BAD_CONTEXT => error.BadContext,
c.EGL_BAD_CONFIG => error.BadConfig,
c.EGL_BAD_CURRENT_SURFACE => error.BadCurrentSurface,
c.EGL_BAD_DISPLAY => error.BadDisplay,
c.EGL_BAD_SURFACE => error.BadSurface,
c.EGL_BAD_MATCH => error.BadMatch,
c.EGL_BAD_PARAMETER => error.BadParameter,
c.EGL_BAD_NATIVE_PIXMAP => error.BadNativePixmap,
c.EGL_BAD_NATIVE_WINDOW => error.BadNativeWindow,
c.EGL_CONTEXT_LOST => error.ContextLost,
else => Error.Unknown,
};
}
pub fn mustError() Error {
try getError();
return error.Unknown;
}
pub fn bindApi(api: c.EGLenum) Error!void {
if (c.eglBindAPI(api) != c.EGL_TRUE) {
return mustError();
}
}
pub const Display = opaque {
pub fn init(id: c.EGLNativeDisplayType) Error!*Display {
const display = c.eglGetDisplay(id) orelse return mustError();
return initialize(display);
}
fn initialize(display: c.EGLDisplay) Error!*Display {
var major: c.EGLint = undefined;
var minor: c.EGLint = undefined;
if (c.eglInitialize(display, &major, &minor) != c.EGL_TRUE) {
return mustError();
}
log.debug("EGL initialized {}.{}", .{ major, minor });
return @ptrCast(display.?);
}
pub fn terminate(self: *Display) void {
_ = c.eglTerminate(@ptrCast(self));
}
const StringQuery = enum(c.EGLint) {
vendor = c.EGL_VENDOR,
extensions = c.EGL_EXTENSIONS,
};
pub fn queryString(self: *Display, name: StringQuery) ?[:0]const u8 {
return std.mem.span(c.eglQueryString(self, @intFromEnum(name)));
}
/// Make the EGL context current on the calling thread and (re)load
/// the thread-local GLAD function pointers so subsequent GL work is
/// valid. Returns an error if the context can't be made current.
pub fn makeCurrent(self: *Display, draw: ?*Surface, read: ?*Surface, context: ?*Context) Error!void {
if (c.eglMakeCurrent(self, draw, read, context) != c.EGL_TRUE) {
return mustError();
}
}
pub fn releaseCurrent(self: *Display) void {
_ = c.eglMakeCurrent(self, null, null, null);
}
};
pub const Config = opaque {
pub fn choose(display: *Display, attrs: [:c.EGL_NONE]const c.EGLint) Error!*Config {
var config: c.EGLConfig = undefined;
var num_config: c.EGLint = 0;
if (c.eglChooseConfig(
display,
attrs.ptr,
&config,
1,
&num_config,
) != c.EGL_TRUE or num_config == 0) {
return mustError();
}
return @ptrCast(config);
}
};
pub const Surface = opaque {};
pub const Context = opaque {
pub fn create(
display: *Display,
config: *Config,
surface: ?*Surface,
attrs: [:c.EGL_NONE]const c.EGLint,
) Error!*Context {
const context = c.eglCreateContext(
display,
config,
surface,
attrs.ptr,
) orelse return mustError();
return @ptrCast(context);
}
pub fn destroy(self: *Context, display: *Display) Error!void {
if (c.eglDestroyContext(display, self) != c.EGL_TRUE) {
return mustError();
}
}
};
pub fn AttribsBuilder(comptime cap: usize) type {
return struct {
attribs: [capacity:c.EGL_NONE]c.EGLAttrib,
len: usize,
const Attribs = @This();
pub const capacity = cap;
pub const empty: Attribs = .{
// Save ourselves the trouble of manually terminating
// the array with an `EGL_NONE`
.attribs = @splat(c.EGL_NONE),
.len = 0,
};
pub fn add(
self: *Attribs,
k: c.EGLAttrib,
v: c.EGLAttrib,
) void {
std.debug.assert(capacity - self.len >= 2);
self.attribs[self.len] = k;
self.attribs[self.len + 1] = v;
self.len += 2;
}
pub fn items(self: *const Attribs) [:c.EGL_NONE]const c.EGLAttrib {
return self.attribs[0..self.len :c.EGL_NONE];
}
};
}
pub const Image = opaque {
pub fn create(
display: *Display,
context: ?*Context,
target: ImageTarget,
attrs: ?[:c.EGL_NONE]const c.EGLAttrib,
) Error!*Image {
const image = c.eglCreateImage(
display,
context,
@intFromEnum(target),
target.toClientBuffer(),
if (attrs) |a| a.ptr else null,
) orelse return mustError();
return @ptrCast(image);
}
pub fn destroy(self: *Image, display: *Display) Error!void {
if (c.eglDestroyImage(display, self) != c.EGL_TRUE) {
return mustError();
}
}
pub fn exportDmabufQuery(self: *Image, display: *Display) Error!DmabufQuery {
var query: DmabufQuery = undefined;
if (c.eglExportDMABUFImageQueryMESA(
display,
self,
&query.fourcc,
&query.num_planes,
&query.modifier,
) != c.EGL_TRUE) {
return mustError();
}
return query;
}
pub fn exportDmabuf(
self: *Image,
display: *Display,
fds: []c_int,
strides: []c_int,
offsets: []c_int,
) Error!void {
if (c.eglExportDMABUFImageMESA(
display,
self,
fds.ptr,
strides.ptr,
offsets.ptr,
) != c.EGL_TRUE) {
return mustError();
}
}
};
pub const ImageTarget = union(ImageTarget.Tag) {
/// OpenGL 2D Texture with the given name.
texture_2d: u32,
// Many other variants, add when needed
pub const Tag = enum(c.EGLenum) {
texture_2d = c.EGL_GL_TEXTURE_2D,
};
pub fn toClientBuffer(self: ImageTarget) c.EGLClientBuffer {
return switch (self) {
// Yes this really is that cursed
.texture_2d => |v| @ptrFromInt(@as(usize, v)),
};
}
};
pub const DmabufQuery = struct {
fourcc: c_int,
num_planes: c_int,
modifier: u64,
};

View File

@@ -1,5 +1,5 @@
const std = @import("std");
const c = @import("c.zig").c;
const c = @import("c");
const glad = @import("glad.zig");
pub const Error = error{

View File

@@ -1,5 +1,5 @@
const std = @import("std");
const c = @import("c.zig").c;
const c = @import("c");
const errors = @import("errors.zig");
const glad = @import("glad.zig");

2
pkg/opengl/gl.c Normal file
View File

@@ -0,0 +1,2 @@
#include <glad/gl.h>
#include <glad/glad_egl.h>

View File

@@ -1,5 +1,5 @@
const std = @import("std");
const c = @import("c.zig").c;
const c = @import("c");
pub const Context = c.GladGLContext;

View File

@@ -10,9 +10,9 @@
//!
//! WARNING: Lots of performance improvements that we can make with Zig
//! comptime help. I'm deferring this until later but have some fun ideas.
pub const c = @import("c.zig").c;
pub const c = @import("c");
pub const glad = @import("glad.zig");
pub const egl = @import("egl.zig");
pub const ext = @import("extensions.zig");
pub const Buffer = @import("Buffer.zig");
pub const Framebuffer = @import("Framebuffer.zig");
@@ -39,7 +39,9 @@ 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 blitFramebuffer = draw.blitFramebuffer;
pub const flush = draw.flush;
pub const finish = draw.finish;

View File

@@ -1,4 +1,4 @@
pub const c = @import("c.zig").c;
const c = @import("c");
pub const Primitive = enum(c_int) {
point = c.GL_POINTS,

View File

@@ -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,

View File

@@ -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.

View File

@@ -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;

View File

@@ -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;

View File

@@ -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(

View File

@@ -3379,14 +3379,8 @@ fn setGtkEnv(config: *const CoreConfig) std.Io.Writer.Error!void {
var gdk_debug: struct {
/// output OpenGL debug information
opengl: bool = false,
/// disable GLES, Ghostty can't use GLES
@"gl-disable-gles": bool = false,
// GTK's new renderer can cause blurry font when using fractional scaling.
@"gl-no-fractional": bool = false,
/// Disabling Vulkan can improve startup times by hundreds of
/// milliseconds on some systems. We don't use Vulkan so we can just
/// disable it.
@"vulkan-disable": bool = false,
} = .{
// `gtk-opengl-debug` dumps logs directly to stderr so both must be true
// to enable OpenGL debugging.
@@ -3394,50 +3388,18 @@ fn setGtkEnv(config: *const CoreConfig) std.Io.Writer.Error!void {
};
var gdk_disable: struct {
@"gles-api": bool = false,
/// current gtk implementation for color management is not good enough.
/// see: https://bugs.kde.org/show_bug.cgi?id=495647
/// gtk issue: https://gitlab.gnome.org/GNOME/gtk/-/issues/6864
@"color-mgmt": bool = true,
/// Disabling Vulkan can improve startup times by hundreds of
/// milliseconds on some systems. We don't use Vulkan so we can just
/// disable it.
vulkan: bool = false,
} = .{};
environment: {
if (gtk_version.runtimeAtLeast(4, 18, 0)) {
gdk_disable.@"color-mgmt" = false;
}
if (gtk_version.runtimeAtLeast(4, 16, 0)) {
// From gtk 4.16, GDK_DEBUG is split into GDK_DEBUG and GDK_DISABLE.
// For the remainder of "why" see the 4.14 comment below.
gdk_disable.@"gles-api" = true;
gdk_disable.vulkan = true;
break :environment;
}
if (gtk_version.runtimeAtLeast(4, 14, 0)) {
// We need to export GDK_DEBUG to run on Wayland after GTK 4.14.
// Older versions of GTK do not support these values so it is safe
// to always set this. Forwards versions are uncertain so we'll have
// to reassess...
//
// Upstream issue: https://gitlab.gnome.org/GNOME/gtk/-/issues/6589
gdk_debug.@"gl-disable-gles" = true;
gdk_debug.@"vulkan-disable" = true;
if (gtk_version.runtimeUntil(4, 17, 5)) {
// Removed at GTK v4.17.5
gdk_debug.@"gl-no-fractional" = true;
}
break :environment;
}
// Versions prior to 4.14 are a bit of an unknown for Ghostty. It
// is an environment that isn't tested well and we don't have a
// good understanding of what we may need to do.
gdk_debug.@"vulkan-disable" = true;
if (gtk_version.runtimeAtLeast(4, 18, 0)) {
gdk_disable.@"color-mgmt" = false;
}
if (gtk_version.runtimeUntil(4, 17, 5)) {
// Removed at GTK v4.17.5
gdk_debug.@"gl-no-fractional" = true;
}
{

View File

@@ -0,0 +1,345 @@
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;
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();
var err_: ?*glib.Error = null;
const texture = builder.build(
dmabufDestroy,
planes,
&err_,
) orelse {
if (err_) |err|
log.warn("failed to build dmabuf err={s}", .{err.f_message orelse "(unknown)"});
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;
};
};

View File

@@ -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,16 @@ 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.
render_surface: *RenderSurface,
/// The labels for the left/right sides of the URL hover tooltip.
url_left: *gtk.Label,
@@ -640,15 +640,12 @@ pub const Surface = extern struct {
/// The apprt Surface.
rt_surface: ApprtSurface = undefined,
/// The core surface backing this GTK surface. This starts out
/// null because it can't be initialized until there is an available
/// GLArea that is realized.
//
// NOTE(mitchellh): This is a limitation we should definitely remove
// at some point by modifying our OpenGL renderer for GTK to
// start in an unrealized state. There are other benefits to being
// able to initialize the surface early so we should aim for that,
// eventually.
/// The core surface backing this GTK surface.
///
/// This starts out null and unrealized and is initialized eagerly
/// when we get our first resize event, since we don't know what
/// size GTK will allocate for this widget beforehand. This will
/// then be realized when the widget itself is realized.
core_surface: ?*CoreSurface = null,
/// Cached metrics for libghostty callbacks
@@ -824,7 +821,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 +1373,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 +1497,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 +1545,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();
@@ -1600,8 +1596,8 @@ pub const Surface = extern struct {
const priv = self.private();
// By the time this is called, we should be in a widget tree.
// This should not be called before that. We ensure this by initializing
// the surface in `glareaResize`. This is VERY important because it
// avoids the pty having an incorrect initial size.
// the surface in `renderSurfaceResize`. This is VERY important because
// it avoids the pty having an incorrect initial size.
assert(priv.size.width >= 0 and priv.size.height >= 0);
return priv.size;
}
@@ -1765,7 +1761,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 +2860,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 +2998,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,34 +3311,18 @@ 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.
// Notify our core surface that it should realize.
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| {
if (priv.core_surface) |v| {
v.displayRealized() catch |err| {
log.warn("core displayRealized failed err={}", .{err});
break :realize;
};
self.redraw();
}
@@ -3352,50 +3332,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 +3386,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;
@@ -3478,7 +3421,7 @@ pub const Surface = extern struct {
const changed = !priv.size.eql(&new_size);
priv.size = new_size;
// If our surface is realize, we send callbacks.
// If our surface is initialized, we send callbacks.
if (priv.core_surface) |surface| {
// We also update the content scale because there is no signal for
// content scale change and it seems to trigger a resize event.
@@ -3493,35 +3436,27 @@ pub const Surface = extern struct {
// Setup our resize overlay if configured
self.resizeOverlaySchedule();
}
return;
} else {
// If we haven't initalized a surface yet, now's the time to
// do so. We cannot do this any earlier since GTK defers the
// layouting and size allocation until after widget initialization.
// Note that initialization is different from realization:
// `initSurface` will start the core surface in an unrealized
// state and wait for the `realize` signal, unless the widget
// is somehow realized before the first resize signal ever fires.
self.initSurface() catch |err| {
log.warn("surface failed to initialize err={}", .{err});
};
}
// If we don't have a surface, then we initialize it.
self.initSurface() catch |err| {
log.warn("surface failed to initialize err={}", .{err});
};
}
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 +3514,17 @@ 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);
// If the widget isn't realized yet, start the renderer in an
// unrealized state so it waits for `displayRealized` (called from
// `renderSurfaceRealize`) before building GPU resources.
if (priv.render_surface.as(gtk.Widget).getRealized() == 0) {
surface.displayUnrealized();
}
// Emit the signal that we initialized the surface.
Surface.signals.init.impl.emit(
self,
@@ -3656,7 +3602,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 +3825,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 +3856,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 +4006,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 +4092,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;

View File

@@ -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 {

View File

@@ -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,

View File

@@ -667,6 +667,15 @@ pub fn add(
.flags = &.{},
});
// Link EGL for GTK.
if (self.config.app_runtime == .gtk) {
step.root_module.addCSourceFile(.{
.file = b.path("vendor/glad/src/glad_egl.c"),
.flags = &.{},
});
step.root_module.linkSystemLibrary("egl", dynamic_link_opts);
}
// When we're targeting flatpak we ALWAYS link GTK so we
// get access to glib for dbus.
if (self.config.flatpak) {

75
src/renderer/Dmabuf.zig Normal file
View 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();
}

View File

@@ -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 .{

View File

@@ -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,45 @@ 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.viewport(0, 0, @intCast(width), @intCast(height)) catch |err| {
log.warn("failed to set OpenGL viewport err={}", .{err});
};
}
/// Actions taken before doing anything in `drawFrame`.
@@ -275,71 +275,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 {
@@ -363,7 +350,7 @@ pub inline fn textureOptions(self: OpenGL) Texture.Options {
return .{
.format = .rgba,
.internal_format = .srgba,
.target = .@"2D",
.target = .@"2d",
.min_filter = .linear,
.mag_filter = .linear,
.wrap_s = .clamp_to_edge,
@@ -410,7 +397,7 @@ pub inline fn imageTextureOptions(
return .{
.format = format.toPixelFormat(),
.internal_format = if (srgb) .srgba else .rgba,
.target = .@"2D",
.target = .@"2d",
// TODO: Generate mipmaps for image textures and use
// linear_mipmap_linear filtering so that they
// look good even when scaled way down.
@@ -441,7 +428,7 @@ pub fn initAtlasTexture(
.{
.format = format,
.internal_format = internal_format,
.target = .Rectangle,
.target = .rectangle,
.min_filter = .nearest,
.mag_filter = .nearest,
.wrap_s = .clamp_to_edge,

View File

@@ -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.

View File

@@ -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,101 @@ 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 {
if (comptime ExportedFrame == void) return;
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 {
if (comptime ExportedFrame == void) return null;
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 {
if (comptime ExportedFrame == void) return;
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 +1157,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 +1740,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 +1867,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 +2192,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});
}

View File

@@ -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

View File

@@ -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,20 +52,28 @@ 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();
// 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 = {} });
}
gl.finish();
// Report the health to the renderer.
self.renderer.frameCompleted(health);

View File

@@ -30,10 +30,10 @@ pub fn init(
) Error!Self {
const sampler = gl.Sampler.create() catch return error.OpenGLFailed;
errdefer sampler.destroy();
sampler.parameter(.WrapS, @intFromEnum(opts.wrap_s)) catch return error.OpenGLFailed;
sampler.parameter(.WrapT, @intFromEnum(opts.wrap_t)) catch return error.OpenGLFailed;
sampler.parameter(.MinFilter, @intFromEnum(opts.min_filter)) catch return error.OpenGLFailed;
sampler.parameter(.MagFilter, @intFromEnum(opts.mag_filter)) catch return error.OpenGLFailed;
sampler.parameter(.wrap_s, opts.wrap_s) catch return error.OpenGLFailed;
sampler.parameter(.wrap_t, opts.wrap_t) catch return error.OpenGLFailed;
sampler.parameter(.min_filter, opts.min_filter) catch return error.OpenGLFailed;
sampler.parameter(.mag_filter, opts.mag_filter) catch return error.OpenGLFailed;
return .{
.sampler = sampler,

View File

@@ -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(.min_filter, .nearest);
try bound_tex.parameter(.mag_filter, .nearest);
try bound_tex.parameter(.wrap_s, .clamp_to_edge);
try bound_tex.parameter(.wrap_t, .clamp_to_edge);
try bound_tex.image2D(
0,
.srgba,
@intCast(opts.width),
@intCast(opts.height),
.rgba,
.unsigned_byte,
null,
);
try bound_tex.parameter(.base_level, 0);
try bound_tex.parameter(.max_level, 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(.min_filter, .nearest);
try bound_tex.parameter(.mag_filter, .nearest);
try bound_tex.parameter(.wrap_s, .clamp_to_edge);
try bound_tex.parameter(.wrap_t, .clamp_to_edge);
try bound_tex.image2D(
0,
.rgba,
@intCast(opts.width),
@intCast(opts.height),
.rgba,
.unsigned_byte,
null,
);
try bound_tex.parameter(.base_level, 0);
try bound_tex.parameter(.max_level, 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,106 @@ 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();
// Flip the Y axis during the blit so apprts don't have to
// handle this. We do this explicitly in OpenGL only
// because it's the only one that thinks +Y should point up
// for some reason.
try gl.blitFramebuffer(
0,
0,
@intCast(self.width),
@intCast(self.height),
0,
@intCast(self.height),
@intCast(self.width),
0,
.{ .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;
}

View File

@@ -50,17 +50,17 @@ pub fn init(
{
const texbind = tex.bind(opts.target) catch return error.OpenGLFailed;
defer texbind.unbind();
texbind.parameter(.WrapS, @intFromEnum(opts.wrap_s)) catch return error.OpenGLFailed;
texbind.parameter(.WrapT, @intFromEnum(opts.wrap_t)) catch return error.OpenGLFailed;
texbind.parameter(.MinFilter, @intFromEnum(opts.min_filter)) catch return error.OpenGLFailed;
texbind.parameter(.MagFilter, @intFromEnum(opts.mag_filter)) catch return error.OpenGLFailed;
texbind.parameter(.wrap_s, opts.wrap_s) catch return error.OpenGLFailed;
texbind.parameter(.wrap_t, opts.wrap_t) catch return error.OpenGLFailed;
texbind.parameter(.min_filter, opts.min_filter) catch return error.OpenGLFailed;
texbind.parameter(.mag_filter, opts.mag_filter) catch return error.OpenGLFailed;
texbind.image2D(
0,
opts.internal_format,
@intCast(width),
@intCast(height),
opts.format,
.UnsignedByte,
.unsigned_byte,
if (data) |d| @ptrCast(d.ptr) else null,
) catch return error.OpenGLFailed;
}
@@ -98,7 +98,7 @@ pub fn replaceRegion(
@intCast(width),
@intCast(height),
self.format,
.UnsignedByte,
.unsigned_byte,
data.ptr,
) catch return error.OpenGLFailed;
}

View File

@@ -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

175
vendor/glad/include/EGL/eglplatform.h vendored Normal file
View File

@@ -0,0 +1,175 @@
#ifndef __eglplatform_h_
#define __eglplatform_h_
/*
** Copyright 2007-2020 The Khronos Group Inc.
** SPDX-License-Identifier: Apache-2.0
*/
/* Platform-specific types and definitions for egl.h
*
* Adopters may modify khrplatform.h and this file to suit their platform.
* You are encouraged to submit all modifications to the Khronos group so that
* they can be included in future versions of this file. Please submit changes
* by filing an issue or pull request on the public Khronos EGL Registry, at
* https://www.github.com/KhronosGroup/EGL-Registry/
*/
#include <KHR/khrplatform.h>
/* Macros used in EGL function prototype declarations.
*
* EGL functions should be prototyped as:
*
* EGLAPI return-type EGLAPIENTRY eglFunction(arguments);
* typedef return-type (EXPAPIENTRYP PFNEGLFUNCTIONPROC) (arguments);
*
* KHRONOS_APICALL and KHRONOS_APIENTRY are defined in KHR/khrplatform.h
*/
#ifndef EGLAPI
#define EGLAPI KHRONOS_APICALL
#endif
#ifndef EGLAPIENTRY
#define EGLAPIENTRY KHRONOS_APIENTRY
#endif
#define EGLAPIENTRYP EGLAPIENTRY*
/* The types NativeDisplayType, NativeWindowType, and NativePixmapType
* are aliases of window-system-dependent types, such as X Display * or
* Windows Device Context. They must be defined in platform-specific
* code below. The EGL-prefixed versions of Native*Type are the same
* types, renamed in EGL 1.3 so all types in the API start with "EGL".
*
* Khronos STRONGLY RECOMMENDS that you use the default definitions
* provided below, since these changes affect both binary and source
* portability of applications using EGL running on different EGL
* implementations.
*/
#if defined(EGL_NO_PLATFORM_SPECIFIC_TYPES)
typedef void *EGLNativeDisplayType;
typedef void *EGLNativePixmapType;
typedef void *EGLNativeWindowType;
#elif defined(_WIN32) || defined(__VC32__) && !defined(__CYGWIN__) && !defined(__SCITECH_SNAP__) /* Win32 and WinCE */
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN 1
#endif
#include <windows.h>
typedef HDC EGLNativeDisplayType;
typedef HBITMAP EGLNativePixmapType;
typedef HWND EGLNativeWindowType;
#elif defined(__QNX__)
typedef khronos_uintptr_t EGLNativeDisplayType;
typedef struct _screen_pixmap* EGLNativePixmapType; /* screen_pixmap_t */
typedef struct _screen_window* EGLNativeWindowType; /* screen_window_t */
#elif defined(__EMSCRIPTEN__)
typedef int EGLNativeDisplayType;
typedef int EGLNativePixmapType;
typedef int EGLNativeWindowType;
#elif defined(__WINSCW__) || defined(__SYMBIAN32__) /* Symbian */
typedef int EGLNativeDisplayType;
typedef void *EGLNativePixmapType;
typedef void *EGLNativeWindowType;
#elif defined(WL_EGL_PLATFORM)
typedef struct wl_display *EGLNativeDisplayType;
typedef struct wl_egl_pixmap *EGLNativePixmapType;
typedef struct wl_egl_window *EGLNativeWindowType;
#elif defined(__GBM__)
typedef struct gbm_device *EGLNativeDisplayType;
typedef struct gbm_bo *EGLNativePixmapType;
typedef void *EGLNativeWindowType;
#elif defined(__ANDROID__) || defined(ANDROID)
struct ANativeWindow;
struct egl_native_pixmap_t;
typedef void* EGLNativeDisplayType;
typedef struct egl_native_pixmap_t* EGLNativePixmapType;
typedef struct ANativeWindow* EGLNativeWindowType;
#elif defined(USE_OZONE)
typedef intptr_t EGLNativeDisplayType;
typedef intptr_t EGLNativePixmapType;
typedef intptr_t EGLNativeWindowType;
#elif defined(USE_X11)
/* X11 (tentative) */
#include <X11/Xlib.h>
#include <X11/Xutil.h>
typedef Display *EGLNativeDisplayType;
typedef Pixmap EGLNativePixmapType;
typedef Window EGLNativeWindowType;
#elif defined(__unix__)
typedef void *EGLNativeDisplayType;
typedef khronos_uintptr_t EGLNativePixmapType;
typedef khronos_uintptr_t EGLNativeWindowType;
#elif defined(__APPLE__)
typedef int EGLNativeDisplayType;
typedef void *EGLNativePixmapType;
typedef void *EGLNativeWindowType;
#elif defined(__HAIKU__)
#include <kernel/image.h>
typedef void *EGLNativeDisplayType;
typedef khronos_uintptr_t EGLNativePixmapType;
typedef khronos_uintptr_t EGLNativeWindowType;
#elif defined(__Fuchsia__)
typedef void *EGLNativeDisplayType;
typedef khronos_uintptr_t EGLNativePixmapType;
typedef khronos_uintptr_t EGLNativeWindowType;
#else
#error "Platform not recognized"
#endif
/* EGL 1.2 types, renamed for consistency in EGL 1.3 */
typedef EGLNativeDisplayType NativeDisplayType;
typedef EGLNativePixmapType NativePixmapType;
typedef EGLNativeWindowType NativeWindowType;
/* Define EGLint. This must be a signed integral type large enough to contain
* all legal attribute names and values passed into and out of EGL, whether
* their type is boolean, bitmask, enumerant (symbolic constant), integer,
* handle, or other. While in general a 32-bit integer will suffice, if
* handles are 64 bit types, then EGLint should be defined as a signed 64-bit
* integer type.
*/
typedef khronos_int32_t EGLint;
/* C++ / C typecast macros for special EGL handle values */
#if defined(__cplusplus)
#define EGL_CAST(type, value) (static_cast<type>(value))
#else
#define EGL_CAST(type, value) ((type) (value))
#endif
#endif /* __eglplatform_h */

326
vendor/glad/include/glad/glad_egl.h vendored Normal file
View File

@@ -0,0 +1,326 @@
/*
EGL loader generated by glad 0.1.36 on Fri Sep 4 22:17:39 2026.
Language/Generator: C/C++
Specification: egl
APIs: egl=1.5
Profile: -
Extensions:
EGL_MESA_image_dma_buf_export
Loader: True
Local files: False
Omit khrplatform: False
Reproducible: False
Commandline:
--api="egl=1.5" --generator="c" --spec="egl" --extensions="EGL_MESA_image_dma_buf_export"
Online:
https://glad.dav1d.de/#language=c&specification=egl&loader=on&api=egl%3D1.5&extensions=EGL_MESA_image_dma_buf_export
*/
#ifndef __glad_egl_h_
#ifdef __egl_h_
#error EGL header already included, remove this include, glad already provides it
#endif
#define __glad_egl_h_
#define __egl_h_
#if defined(_WIN32) && !defined(APIENTRY) && !defined(__CYGWIN__) && !defined(__SCITECH_SNAP__)
#define APIENTRY __stdcall
#endif
#ifndef APIENTRY
#define APIENTRY
#endif
#ifndef APIENTRYP
#define APIENTRYP APIENTRY *
#endif
#ifndef GLAPI
#define GLAPI extern
#endif
#ifdef __cplusplus
extern "C" {
#endif
typedef void* (* GLADloadproc)(const char *name);
GLAPI int gladLoadEGL(void);
GLAPI int gladLoadEGLLoader(GLADloadproc);
#include <KHR/khrplatform.h>
#include <EGL/eglplatform.h>
struct AHardwareBuffer;
struct wl_buffer;
struct wl_display;
struct wl_resource;
typedef unsigned int EGLBoolean;
typedef unsigned int EGLenum;
typedef intptr_t EGLAttribKHR;
typedef intptr_t EGLAttrib;
typedef void *EGLClientBuffer;
typedef void *EGLConfig;
typedef void *EGLContext;
typedef void *EGLDeviceEXT;
typedef void *EGLDisplay;
typedef void *EGLImage;
typedef void *EGLImageKHR;
typedef void *EGLLabelKHR;
typedef void *EGLObjectKHR;
typedef void *EGLOutputLayerEXT;
typedef void *EGLOutputPortEXT;
typedef void *EGLStreamKHR;
typedef void *EGLSurface;
typedef void *EGLSync;
typedef void *EGLSyncKHR;
typedef void *EGLSyncNV;
typedef void (*__eglMustCastToProperFunctionPointerType)(void);
typedef khronos_utime_nanoseconds_t EGLTimeKHR;
typedef khronos_utime_nanoseconds_t EGLTime;
typedef khronos_utime_nanoseconds_t EGLTimeNV;
typedef khronos_utime_nanoseconds_t EGLuint64NV;
typedef khronos_uint64_t EGLuint64KHR;
typedef khronos_stime_nanoseconds_t EGLnsecsANDROID;
typedef int EGLNativeFileDescriptorKHR;
typedef khronos_ssize_t EGLsizeiANDROID;
typedef void (*EGLSetBlobFuncANDROID) (const void *key, EGLsizeiANDROID keySize, const void *value, EGLsizeiANDROID valueSize);
typedef EGLsizeiANDROID (*EGLGetBlobFuncANDROID) (const void *key, EGLsizeiANDROID keySize, void *value, EGLsizeiANDROID valueSize);
struct EGLClientPixmapHI {
void *pData;
EGLint iWidth;
EGLint iHeight;
EGLint iStride;
};
typedef void (APIENTRY *EGLDEBUGPROCKHR)(EGLenum error,const char *command,EGLint messageType,EGLLabelKHR threadLabel,EGLLabelKHR objectLabel,const char* message);
#define PFNEGLBINDWAYLANDDISPLAYWL PFNEGLBINDWAYLANDDISPLAYWLPROC
#define PFNEGLUNBINDWAYLANDDISPLAYWL PFNEGLUNBINDWAYLANDDISPLAYWLPROC
#define PFNEGLQUERYWAYLANDBUFFERWL PFNEGLQUERYWAYLANDBUFFERWLPROC
#define PFNEGLCREATEWAYLANDBUFFERFROMIMAGEWL PFNEGLCREATEWAYLANDBUFFERFROMIMAGEWLPROC
#define EGL_ALPHA_SIZE 0x3021
#define EGL_BAD_ACCESS 0x3002
#define EGL_BAD_ALLOC 0x3003
#define EGL_BAD_ATTRIBUTE 0x3004
#define EGL_BAD_CONFIG 0x3005
#define EGL_BAD_CONTEXT 0x3006
#define EGL_BAD_CURRENT_SURFACE 0x3007
#define EGL_BAD_DISPLAY 0x3008
#define EGL_BAD_MATCH 0x3009
#define EGL_BAD_NATIVE_PIXMAP 0x300A
#define EGL_BAD_NATIVE_WINDOW 0x300B
#define EGL_BAD_PARAMETER 0x300C
#define EGL_BAD_SURFACE 0x300D
#define EGL_BLUE_SIZE 0x3022
#define EGL_BUFFER_SIZE 0x3020
#define EGL_CONFIG_CAVEAT 0x3027
#define EGL_CONFIG_ID 0x3028
#define EGL_CORE_NATIVE_ENGINE 0x305B
#define EGL_DEPTH_SIZE 0x3025
#define EGL_DONT_CARE EGL_CAST(EGLint,-1)
#define EGL_DRAW 0x3059
#define EGL_EXTENSIONS 0x3055
#define EGL_FALSE 0
#define EGL_GREEN_SIZE 0x3023
#define EGL_HEIGHT 0x3056
#define EGL_LARGEST_PBUFFER 0x3058
#define EGL_LEVEL 0x3029
#define EGL_MAX_PBUFFER_HEIGHT 0x302A
#define EGL_MAX_PBUFFER_PIXELS 0x302B
#define EGL_MAX_PBUFFER_WIDTH 0x302C
#define EGL_NATIVE_RENDERABLE 0x302D
#define EGL_NATIVE_VISUAL_ID 0x302E
#define EGL_NATIVE_VISUAL_TYPE 0x302F
#define EGL_NONE 0x3038
#define EGL_NON_CONFORMANT_CONFIG 0x3051
#define EGL_NOT_INITIALIZED 0x3001
#define EGL_NO_CONTEXT EGL_CAST(EGLContext,0)
#define EGL_NO_DISPLAY EGL_CAST(EGLDisplay,0)
#define EGL_NO_SURFACE EGL_CAST(EGLSurface,0)
#define EGL_PBUFFER_BIT 0x0001
#define EGL_PIXMAP_BIT 0x0002
#define EGL_READ 0x305A
#define EGL_RED_SIZE 0x3024
#define EGL_SAMPLES 0x3031
#define EGL_SAMPLE_BUFFERS 0x3032
#define EGL_SLOW_CONFIG 0x3050
#define EGL_STENCIL_SIZE 0x3026
#define EGL_SUCCESS 0x3000
#define EGL_SURFACE_TYPE 0x3033
#define EGL_TRANSPARENT_BLUE_VALUE 0x3035
#define EGL_TRANSPARENT_GREEN_VALUE 0x3036
#define EGL_TRANSPARENT_RED_VALUE 0x3037
#define EGL_TRANSPARENT_RGB 0x3052
#define EGL_TRANSPARENT_TYPE 0x3034
#define EGL_TRUE 1
#define EGL_VENDOR 0x3053
#define EGL_VERSION 0x3054
#define EGL_WIDTH 0x3057
#define EGL_WINDOW_BIT 0x0004
#define EGL_BACK_BUFFER 0x3084
#define EGL_BIND_TO_TEXTURE_RGB 0x3039
#define EGL_BIND_TO_TEXTURE_RGBA 0x303A
#define EGL_CONTEXT_LOST 0x300E
#define EGL_MIN_SWAP_INTERVAL 0x303B
#define EGL_MAX_SWAP_INTERVAL 0x303C
#define EGL_MIPMAP_TEXTURE 0x3082
#define EGL_MIPMAP_LEVEL 0x3083
#define EGL_NO_TEXTURE 0x305C
#define EGL_TEXTURE_2D 0x305F
#define EGL_TEXTURE_FORMAT 0x3080
#define EGL_TEXTURE_RGB 0x305D
#define EGL_TEXTURE_RGBA 0x305E
#define EGL_TEXTURE_TARGET 0x3081
#define EGL_ALPHA_FORMAT 0x3088
#define EGL_ALPHA_FORMAT_NONPRE 0x308B
#define EGL_ALPHA_FORMAT_PRE 0x308C
#define EGL_ALPHA_MASK_SIZE 0x303E
#define EGL_BUFFER_PRESERVED 0x3094
#define EGL_BUFFER_DESTROYED 0x3095
#define EGL_CLIENT_APIS 0x308D
#define EGL_COLORSPACE 0x3087
#define EGL_COLORSPACE_sRGB 0x3089
#define EGL_COLORSPACE_LINEAR 0x308A
#define EGL_COLOR_BUFFER_TYPE 0x303F
#define EGL_CONTEXT_CLIENT_TYPE 0x3097
#define EGL_DISPLAY_SCALING 10000
#define EGL_HORIZONTAL_RESOLUTION 0x3090
#define EGL_LUMINANCE_BUFFER 0x308F
#define EGL_LUMINANCE_SIZE 0x303D
#define EGL_OPENGL_ES_BIT 0x0001
#define EGL_OPENVG_BIT 0x0002
#define EGL_OPENGL_ES_API 0x30A0
#define EGL_OPENVG_API 0x30A1
#define EGL_OPENVG_IMAGE 0x3096
#define EGL_PIXEL_ASPECT_RATIO 0x3092
#define EGL_RENDERABLE_TYPE 0x3040
#define EGL_RENDER_BUFFER 0x3086
#define EGL_RGB_BUFFER 0x308E
#define EGL_SINGLE_BUFFER 0x3085
#define EGL_SWAP_BEHAVIOR 0x3093
#define EGL_UNKNOWN EGL_CAST(EGLint,-1)
#define EGL_VERTICAL_RESOLUTION 0x3091
#define EGL_CONFORMANT 0x3042
#define EGL_CONTEXT_CLIENT_VERSION 0x3098
#define EGL_MATCH_NATIVE_PIXMAP 0x3041
#define EGL_OPENGL_ES2_BIT 0x0004
#define EGL_VG_ALPHA_FORMAT 0x3088
#define EGL_VG_ALPHA_FORMAT_NONPRE 0x308B
#define EGL_VG_ALPHA_FORMAT_PRE 0x308C
#define EGL_VG_ALPHA_FORMAT_PRE_BIT 0x0040
#define EGL_VG_COLORSPACE 0x3087
#define EGL_VG_COLORSPACE_sRGB 0x3089
#define EGL_VG_COLORSPACE_LINEAR 0x308A
#define EGL_VG_COLORSPACE_LINEAR_BIT 0x0020
#define EGL_DEFAULT_DISPLAY EGL_CAST(EGLNativeDisplayType,0)
#define EGL_MULTISAMPLE_RESOLVE_BOX_BIT 0x0200
#define EGL_MULTISAMPLE_RESOLVE 0x3099
#define EGL_MULTISAMPLE_RESOLVE_DEFAULT 0x309A
#define EGL_MULTISAMPLE_RESOLVE_BOX 0x309B
#define EGL_OPENGL_API 0x30A2
#define EGL_OPENGL_BIT 0x0008
#define EGL_SWAP_BEHAVIOR_PRESERVED_BIT 0x0400
#define EGL_CONTEXT_MAJOR_VERSION 0x3098
#define EGL_CONTEXT_MINOR_VERSION 0x30FB
#define EGL_CONTEXT_OPENGL_PROFILE_MASK 0x30FD
#define EGL_CONTEXT_OPENGL_RESET_NOTIFICATION_STRATEGY 0x31BD
#define EGL_NO_RESET_NOTIFICATION 0x31BE
#define EGL_LOSE_CONTEXT_ON_RESET 0x31BF
#define EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT 0x00000001
#define EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT 0x00000002
#define EGL_CONTEXT_OPENGL_DEBUG 0x31B0
#define EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE 0x31B1
#define EGL_CONTEXT_OPENGL_ROBUST_ACCESS 0x31B2
#define EGL_OPENGL_ES3_BIT 0x00000040
#define EGL_CL_EVENT_HANDLE 0x309C
#define EGL_SYNC_CL_EVENT 0x30FE
#define EGL_SYNC_CL_EVENT_COMPLETE 0x30FF
#define EGL_SYNC_PRIOR_COMMANDS_COMPLETE 0x30F0
#define EGL_SYNC_TYPE 0x30F7
#define EGL_SYNC_STATUS 0x30F1
#define EGL_SYNC_CONDITION 0x30F8
#define EGL_SIGNALED 0x30F2
#define EGL_UNSIGNALED 0x30F3
#define EGL_SYNC_FLUSH_COMMANDS_BIT 0x0001
#define EGL_FOREVER 0xFFFFFFFFFFFFFFFF
#define EGL_TIMEOUT_EXPIRED 0x30F5
#define EGL_CONDITION_SATISFIED 0x30F6
#define EGL_NO_SYNC EGL_CAST(EGLSync,0)
#define EGL_SYNC_FENCE 0x30F9
#define EGL_GL_COLORSPACE 0x309D
#define EGL_GL_COLORSPACE_SRGB 0x3089
#define EGL_GL_COLORSPACE_LINEAR 0x308A
#define EGL_GL_RENDERBUFFER 0x30B9
#define EGL_GL_TEXTURE_2D 0x30B1
#define EGL_GL_TEXTURE_LEVEL 0x30BC
#define EGL_GL_TEXTURE_3D 0x30B2
#define EGL_GL_TEXTURE_ZOFFSET 0x30BD
#define EGL_GL_TEXTURE_CUBE_MAP_POSITIVE_X 0x30B3
#define EGL_GL_TEXTURE_CUBE_MAP_NEGATIVE_X 0x30B4
#define EGL_GL_TEXTURE_CUBE_MAP_POSITIVE_Y 0x30B5
#define EGL_GL_TEXTURE_CUBE_MAP_NEGATIVE_Y 0x30B6
#define EGL_GL_TEXTURE_CUBE_MAP_POSITIVE_Z 0x30B7
#define EGL_GL_TEXTURE_CUBE_MAP_NEGATIVE_Z 0x30B8
#define EGL_IMAGE_PRESERVED 0x30D2
#define EGL_NO_IMAGE EGL_CAST(EGLImage,0)
EGLBoolean eglChooseConfig(EGLDisplay dpy, const EGLint *attrib_list, EGLConfig *configs, EGLint config_size, EGLint *num_config);
EGLBoolean eglCopyBuffers(EGLDisplay dpy, EGLSurface surface, EGLNativePixmapType target);
EGLContext eglCreateContext(EGLDisplay dpy, EGLConfig config, EGLContext share_context, const EGLint *attrib_list);
EGLSurface eglCreatePbufferSurface(EGLDisplay dpy, EGLConfig config, const EGLint *attrib_list);
EGLSurface eglCreatePixmapSurface(EGLDisplay dpy, EGLConfig config, EGLNativePixmapType pixmap, const EGLint *attrib_list);
EGLSurface eglCreateWindowSurface(EGLDisplay dpy, EGLConfig config, EGLNativeWindowType win, const EGLint *attrib_list);
EGLBoolean eglDestroyContext(EGLDisplay dpy, EGLContext ctx);
EGLBoolean eglDestroySurface(EGLDisplay dpy, EGLSurface surface);
EGLBoolean eglGetConfigAttrib(EGLDisplay dpy, EGLConfig config, EGLint attribute, EGLint *value);
EGLBoolean eglGetConfigs(EGLDisplay dpy, EGLConfig *configs, EGLint config_size, EGLint *num_config);
EGLDisplay eglGetCurrentDisplay(void);
EGLSurface eglGetCurrentSurface(EGLint readdraw);
EGLDisplay eglGetDisplay(EGLNativeDisplayType display_id);
EGLint eglGetError(void);
__eglMustCastToProperFunctionPointerType eglGetProcAddress(const char *procname);
EGLBoolean eglInitialize(EGLDisplay dpy, EGLint *major, EGLint *minor);
EGLBoolean eglMakeCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx);
EGLBoolean eglQueryContext(EGLDisplay dpy, EGLContext ctx, EGLint attribute, EGLint *value);
const char *eglQueryString(EGLDisplay dpy, EGLint name);
EGLBoolean eglQuerySurface(EGLDisplay dpy, EGLSurface surface, EGLint attribute, EGLint *value);
EGLBoolean eglSwapBuffers(EGLDisplay dpy, EGLSurface surface);
EGLBoolean eglTerminate(EGLDisplay dpy);
EGLBoolean eglWaitGL(void);
EGLBoolean eglWaitNative(EGLint engine);
EGLBoolean eglBindTexImage(EGLDisplay dpy, EGLSurface surface, EGLint buffer);
EGLBoolean eglReleaseTexImage(EGLDisplay dpy, EGLSurface surface, EGLint buffer);
EGLBoolean eglSurfaceAttrib(EGLDisplay dpy, EGLSurface surface, EGLint attribute, EGLint value);
EGLBoolean eglSwapInterval(EGLDisplay dpy, EGLint interval);
EGLBoolean eglBindAPI(EGLenum api);
EGLenum eglQueryAPI(void);
EGLSurface eglCreatePbufferFromClientBuffer(EGLDisplay dpy, EGLenum buftype, EGLClientBuffer buffer, EGLConfig config, const EGLint *attrib_list);
EGLBoolean eglReleaseThread(void);
EGLBoolean eglWaitClient(void);
EGLContext eglGetCurrentContext(void);
EGLSync eglCreateSync(EGLDisplay dpy, EGLenum type, const EGLAttrib *attrib_list);
EGLBoolean eglDestroySync(EGLDisplay dpy, EGLSync sync);
EGLint eglClientWaitSync(EGLDisplay dpy, EGLSync sync, EGLint flags, EGLTime timeout);
EGLBoolean eglGetSyncAttrib(EGLDisplay dpy, EGLSync sync, EGLint attribute, EGLAttrib *value);
EGLImage eglCreateImage(EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer, const EGLAttrib *attrib_list);
EGLBoolean eglDestroyImage(EGLDisplay dpy, EGLImage image);
EGLDisplay eglGetPlatformDisplay(EGLenum platform, void *native_display, const EGLAttrib *attrib_list);
EGLSurface eglCreatePlatformWindowSurface(EGLDisplay dpy, EGLConfig config, void *native_window, const EGLAttrib *attrib_list);
EGLSurface eglCreatePlatformPixmapSurface(EGLDisplay dpy, EGLConfig config, void *native_pixmap, const EGLAttrib *attrib_list);
EGLBoolean eglWaitSync(EGLDisplay dpy, EGLSync sync, EGLint flags);
#ifndef EGL_MESA_image_dma_buf_export
#define EGL_MESA_image_dma_buf_export 1
typedef EGLBoolean (APIENTRYP PFNEGLEXPORTDMABUFIMAGEQUERYMESAPROC)(EGLDisplay dpy, EGLImageKHR image, int *fourcc, int *num_planes, EGLuint64KHR *modifiers);
GLAPI PFNEGLEXPORTDMABUFIMAGEQUERYMESAPROC glad_eglExportDMABUFImageQueryMESA;
#define eglExportDMABUFImageQueryMESA glad_eglExportDMABUFImageQueryMESA
typedef EGLBoolean (APIENTRYP PFNEGLEXPORTDMABUFIMAGEMESAPROC)(EGLDisplay dpy, EGLImageKHR image, int *fds, EGLint *strides, EGLint *offsets);
GLAPI PFNEGLEXPORTDMABUFIMAGEMESAPROC glad_eglExportDMABUFImageMESA;
#define eglExportDMABUFImageMESA glad_eglExportDMABUFImageMESA
#endif
#ifdef __cplusplus
}
#endif
#endif

52
vendor/glad/src/glad_egl.c vendored Normal file
View File

@@ -0,0 +1,52 @@
/*
EGL loader generated by glad 0.1.36 on Fri Sep 4 22:17:39 2026.
Language/Generator: C/C++
Specification: egl
APIs: egl=1.5
Profile: -
Extensions:
EGL_MESA_image_dma_buf_export
Loader: True
Local files: False
Omit khrplatform: False
Reproducible: False
Commandline:
--api="egl=1.5" --generator="c" --spec="egl" --extensions="EGL_MESA_image_dma_buf_export"
Online:
https://glad.dav1d.de/#language=c&specification=egl&loader=on&api=egl%3D1.5&extensions=EGL_MESA_image_dma_buf_export
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <glad/glad_egl.h>
int gladLoadEGL(void) {
return gladLoadEGLLoader((GLADloadproc)eglGetProcAddress);
}
PFNEGLEXPORTDMABUFIMAGEQUERYMESAPROC glad_eglExportDMABUFImageQueryMESA = NULL;
PFNEGLEXPORTDMABUFIMAGEMESAPROC glad_eglExportDMABUFImageMESA = NULL;
static void load_EGL_MESA_image_dma_buf_export(GLADloadproc load) {
glad_eglExportDMABUFImageQueryMESA = (PFNEGLEXPORTDMABUFIMAGEQUERYMESAPROC)load("eglExportDMABUFImageQueryMESA");
glad_eglExportDMABUFImageMESA = (PFNEGLEXPORTDMABUFIMAGEMESAPROC)load("eglExportDMABUFImageMESA");
}
static int find_extensionsEGL(void) {
return 1;
}
static void find_coreEGL(void) {
}
int gladLoadEGLLoader(GLADloadproc load) {
(void) load;
find_coreEGL();
if (!find_extensionsEGL()) return 0;
load_EGL_MESA_image_dma_buf_export(load);
return 1;
}