diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 45d2002e0..cb4d80f90 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 diff --git a/nix/devShell.nix b/nix/devShell.nix index fba128a21..bbe79109a 100644 --- a/nix/devShell.nix +++ b/nix/devShell.nix @@ -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 diff --git a/pkg/opengl/Buffer.zig b/pkg/opengl/Buffer.zig index 609342958..bb04b75dd 100644 --- a/pkg/opengl/Buffer.zig +++ b/pkg/opengl/Buffer.zig @@ -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"); diff --git a/pkg/opengl/Framebuffer.zig b/pkg/opengl/Framebuffer.zig index ea1f0d2ba..f1bca9da1 100644 --- a/pkg/opengl/Framebuffer.zig +++ b/pkg/opengl/Framebuffer.zig @@ -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"); diff --git a/pkg/opengl/Program.zig b/pkg/opengl/Program.zig index ee77582c1..c563aebdb 100644 --- a/pkg/opengl/Program.zig +++ b/pkg/opengl/Program.zig @@ -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"); diff --git a/pkg/opengl/Renderbuffer.zig b/pkg/opengl/Renderbuffer.zig index ef21287f7..2f4647d60 100644 --- a/pkg/opengl/Renderbuffer.zig +++ b/pkg/opengl/Renderbuffer.zig @@ -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"); diff --git a/pkg/opengl/Sampler.zig b/pkg/opengl/Sampler.zig index f5c15699f..c4fad658f 100644 --- a/pkg/opengl/Sampler.zig +++ b/pkg/opengl/Sampler.zig @@ -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.?( diff --git a/pkg/opengl/Shader.zig b/pkg/opengl/Shader.zig index 8973ace30..0084c8729 100644 --- a/pkg/opengl/Shader.zig +++ b/pkg/opengl/Shader.zig @@ -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"); diff --git a/pkg/opengl/Texture.zig b/pkg/opengl/Texture.zig index 03e794855..06a454df6 100644 --- a/pkg/opengl/Texture.zig +++ b/pkg/opengl/Texture.zig @@ -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(); } diff --git a/pkg/opengl/VertexArray.zig b/pkg/opengl/VertexArray.zig index 44bf31621..3817f8fa0 100644 --- a/pkg/opengl/VertexArray.zig +++ b/pkg/opengl/VertexArray.zig @@ -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"); diff --git a/pkg/opengl/build.zig b/pkg/opengl/build.zig index f472053ed..e63313a99 100644 --- a/pkg/opengl/build.zig +++ b/pkg/opengl/build.zig @@ -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()); } diff --git a/pkg/opengl/c.zig b/pkg/opengl/c.zig deleted file mode 100644 index fc95ee94c..000000000 --- a/pkg/opengl/c.zig +++ /dev/null @@ -1,3 +0,0 @@ -pub const c = @cImport({ - @cInclude("glad/gl.h"); -}); diff --git a/pkg/opengl/draw.zig b/pkg/opengl/draw.zig index 50110f605..e932baca4 100644 --- a/pkg/opengl/draw.zig +++ b/pkg/opengl/draw.zig @@ -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, +}; diff --git a/pkg/opengl/egl.zig b/pkg/opengl/egl.zig new file mode 100644 index 000000000..53aacd10a --- /dev/null +++ b/pkg/opengl/egl.zig @@ -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, +}; diff --git a/pkg/opengl/errors.zig b/pkg/opengl/errors.zig index ec0464604..cfae2c276 100644 --- a/pkg/opengl/errors.zig +++ b/pkg/opengl/errors.zig @@ -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{ diff --git a/pkg/opengl/extensions.zig b/pkg/opengl/extensions.zig index 5fdbada93..1c49bdbce 100644 --- a/pkg/opengl/extensions.zig +++ b/pkg/opengl/extensions.zig @@ -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"); diff --git a/pkg/opengl/gl.c b/pkg/opengl/gl.c new file mode 100644 index 000000000..94e25eb70 --- /dev/null +++ b/pkg/opengl/gl.c @@ -0,0 +1,2 @@ +#include +#include diff --git a/pkg/opengl/glad.zig b/pkg/opengl/glad.zig index 663e75e12..d766a60b6 100644 --- a/pkg/opengl/glad.zig +++ b/pkg/opengl/glad.zig @@ -1,5 +1,5 @@ const std = @import("std"); -const c = @import("c.zig").c; +const c = @import("c"); pub const Context = c.GladGLContext; diff --git a/pkg/opengl/main.zig b/pkg/opengl/main.zig index 2f22154c6..c99909eb2 100644 --- a/pkg/opengl/main.zig +++ b/pkg/opengl/main.zig @@ -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; diff --git a/pkg/opengl/primitives.zig b/pkg/opengl/primitives.zig index e12f51a66..52ef2a1ad 100644 --- a/pkg/opengl/primitives.zig +++ b/pkg/opengl/primitives.zig @@ -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, diff --git a/src/App.zig b/src/App.zig index 2ee9f936c..dedc9cf9a 100644 --- a/src/App.zig +++ b/src/App.zig @@ -267,7 +267,6 @@ fn drainMailbox(self: *App, rt_app: *apprt.App) !void { if (comptime std.log.logEnabled(.debug, .app)) { switch (message) { // these tend to be way too verbose for normal debugging - .redraw_surface => {}, else => log.debug("mailbox message={t}", .{message}), } } @@ -284,7 +283,6 @@ fn drainMailbox(self: *App, rt_app: *apprt.App) !void { .new_window => |msg| try self.newWindow(rt_app, msg), .close => |surface| self.closeSurface(surface), .surface_message => |msg| try self.surfaceMessage(msg.surface, msg.message), - .redraw_surface => |surface| try self.redrawSurface(rt_app, surface), // If we're quitting, then we set the quit flag and stop // draining the mailbox immediately. This lets us defer @@ -309,20 +307,6 @@ pub fn focusSurface(self: *App, surface: *Surface) void { self.focused_surface = surface; } -fn redrawSurface( - self: *App, - rt_app: *apprt.App, - surface: *apprt.Surface, -) !void { - if (!self.hasRtSurface(surface)) return; - - _ = try rt_app.performAction( - .{ .surface = surface.core() }, - .render, - {}, - ); -} - /// Create a new window pub fn newWindow(self: *App, rt_app: *apprt.App, msg: Message.NewWindow) !void { const target: apprt.Target = target: { @@ -557,14 +541,6 @@ pub fn findSurfaceByID(self: *const App, id: u64) ?*Surface { return null; } -fn hasRtSurface(self: *const App, surface: *apprt.Surface) bool { - for (self.surfaces.items) |v| { - if (v == surface) return true; - } - - return false; -} - /// The message types that can be sent to the app thread. pub const Message = union(enum) { // Open the configuration file @@ -586,12 +562,6 @@ pub const Message = union(enum) { message: apprt.surface.Message, }, - /// Redraw a surface. This only has an effect for runtimes that - /// use single-threaded draws. To redraw a surface for all runtimes, - /// wake up the renderer thread. The renderer thread will send this - /// message if it needs to. - redraw_surface: *apprt.Surface, - const NewWindow = struct { /// The parent surface parent: ?*Surface = null, diff --git a/src/Surface.zig b/src/Surface.zig index 775292127..24d2d26be 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -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. diff --git a/src/apprt.zig b/src/apprt.zig index 22256cc97..ef788c9bb 100644 --- a/src/apprt.zig +++ b/src/apprt.zig @@ -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; diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 9fbea7c1e..90ff25880 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -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; diff --git a/src/apprt/gtk/class.zig b/src/apprt/gtk/class.zig index a1dba7169..0c33a6262 100644 --- a/src/apprt/gtk/class.zig +++ b/src/apprt/gtk/class.zig @@ -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( diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index 5b9d3bf2e..4c3aa673c 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -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; } { diff --git a/src/apprt/gtk/class/render_surface.zig b/src/apprt/gtk/class/render_surface.zig new file mode 100644 index 000000000..f4ac78cab --- /dev/null +++ b/src/apprt/gtk/class/render_surface.zig @@ -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; + }; +}; diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index 9d45bc6ce..c1eacda9c 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -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; diff --git a/src/apprt/gtk/ui/1.2/surface.blp b/src/apprt/gtk/ui/1.2/surface.blp index 010798ae5..b19a4c19b 100644 --- a/src/apprt/gtk/ui/1.2/surface.blp +++ b/src/apprt/gtk/ui/1.2/surface.blp @@ -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 { diff --git a/src/apprt/surface.zig b/src/apprt/surface.zig index 51da3374a..2cf8ef7da 100644 --- a/src/apprt/surface.zig +++ b/src/apprt/surface.zig @@ -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, diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index f6d9c119a..36daf8eb3 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -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) { diff --git a/src/renderer/Dmabuf.zig b/src/renderer/Dmabuf.zig new file mode 100644 index 000000000..f19b802b5 --- /dev/null +++ b/src/renderer/Dmabuf.zig @@ -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(); +} diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 9247454db..0458b2dc1 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -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 .{ diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 40c11f606..31e9df4f2 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -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, diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index f66fd3842..bfd9c4f3b 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -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. diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index e376e1bfe..52ecaa217 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -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}); } diff --git a/src/renderer/metal/shaders.zig b/src/renderer/metal/shaders.zig index 6ae321068..84d536c41 100644 --- a/src/renderer/metal/shaders.zig +++ b/src/renderer/metal/shaders.zig @@ -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 diff --git a/src/renderer/opengl/Frame.zig b/src/renderer/opengl/Frame.zig index 289413b0a..d7a3f3e44 100644 --- a/src/renderer/opengl/Frame.zig +++ b/src/renderer/opengl/Frame.zig @@ -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); diff --git a/src/renderer/opengl/Sampler.zig b/src/renderer/opengl/Sampler.zig index f4013c686..294dc1ec8 100644 --- a/src/renderer/opengl/Sampler.zig +++ b/src/renderer/opengl/Sampler.zig @@ -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, diff --git a/src/renderer/opengl/Target.zig b/src/renderer/opengl/Target.zig index 5c6d818f1..55e7e3384 100644 --- a/src/renderer/opengl/Target.zig +++ b/src/renderer/opengl/Target.zig @@ -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; } diff --git a/src/renderer/opengl/Texture.zig b/src/renderer/opengl/Texture.zig index c37ec6866..52c6017cd 100644 --- a/src/renderer/opengl/Texture.zig +++ b/src/renderer/opengl/Texture.zig @@ -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; } diff --git a/src/renderer/opengl/shaders.zig b/src/renderer/opengl/shaders.zig index 68d2babf6..2155e289b 100644 --- a/src/renderer/opengl/shaders.zig +++ b/src/renderer/opengl/shaders.zig @@ -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 diff --git a/vendor/glad/include/EGL/eglplatform.h b/vendor/glad/include/EGL/eglplatform.h new file mode 100644 index 000000000..6786afd90 --- /dev/null +++ b/vendor/glad/include/EGL/eglplatform.h @@ -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 + +/* 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 + +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 +#include + +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 + +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(value)) +#else +#define EGL_CAST(type, value) ((type) (value)) +#endif + +#endif /* __eglplatform_h */ diff --git a/vendor/glad/include/glad/glad_egl.h b/vendor/glad/include/glad/glad_egl.h new file mode 100644 index 000000000..359132d98 --- /dev/null +++ b/vendor/glad/include/glad/glad_egl.h @@ -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 +#include +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 diff --git a/vendor/glad/src/glad_egl.c b/vendor/glad/src/glad_egl.c new file mode 100644 index 000000000..b4887a6bf --- /dev/null +++ b/vendor/glad/src/glad_egl.c @@ -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 +#include +#include +#include + +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; +} +