aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/renderer/OpenGL.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-04 19:22:04 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-04-05 01:23:06 +0300
commit41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch)
tree3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/renderer/OpenGL.zig
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/renderer/OpenGL.zig')
-rw-r--r--vendor/libghostty-vt/src/renderer/OpenGL.zig461
1 files changed, 461 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/renderer/OpenGL.zig b/vendor/libghostty-vt/src/renderer/OpenGL.zig
new file mode 100644
index 00000000..4b01da0c
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/OpenGL.zig
@@ -0,0 +1,461 @@
+//! Graphics API wrapper for OpenGL.
+pub const OpenGL = @This();
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const builtin = @import("builtin");
+const gl = @import("opengl");
+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);
+
+pub const GraphicsAPI = OpenGL;
+pub const Target = @import("opengl/Target.zig");
+pub const Frame = @import("opengl/Frame.zig");
+pub const RenderPass = @import("opengl/RenderPass.zig");
+pub const Pipeline = @import("opengl/Pipeline.zig");
+const bufferpkg = @import("opengl/buffer.zig");
+pub const Buffer = bufferpkg.Buffer;
+pub const Sampler = @import("opengl/Sampler.zig");
+pub const Texture = @import("opengl/Texture.zig");
+pub const shaders = @import("opengl/shaders.zig");
+
+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;
+
+const log = std.log.scoped(.opengl);
+
+/// We require at least OpenGL 4.3
+pub const MIN_VERSION_MAJOR = 4;
+pub const MIN_VERSION_MINOR = 3;
+
+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,
+
+/// 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,
+ };
+}
+
+pub fn deinit(self: *OpenGL) void {
+ self.* = undefined;
+}
+
+/// 32-bit windows cross-compilation breaks with `.c` for some reason, so...
+const gl_debug_proc_callconv =
+ @typeInfo(
+ @typeInfo(
+ @typeInfo(
+ gl.c.GLDEBUGPROC,
+ ).optional.child,
+ ).pointer.child,
+ ).@"fn".calling_convention;
+
+fn glDebugMessageCallback(
+ src: gl.c.GLenum,
+ typ: gl.c.GLenum,
+ id: gl.c.GLuint,
+ severity: gl.c.GLenum,
+ len: gl.c.GLsizei,
+ msg: [*c]const gl.c.GLchar,
+ user_param: ?*const anyopaque,
+) callconv(gl_debug_proc_callconv) void {
+ _ = user_param;
+
+ const src_str: []const u8 = switch (src) {
+ gl.c.GL_DEBUG_SOURCE_API => "OpenGL API",
+ gl.c.GL_DEBUG_SOURCE_WINDOW_SYSTEM => "Window System",
+ gl.c.GL_DEBUG_SOURCE_SHADER_COMPILER => "Shader Compiler",
+ gl.c.GL_DEBUG_SOURCE_THIRD_PARTY => "Third Party",
+ gl.c.GL_DEBUG_SOURCE_APPLICATION => "User",
+ gl.c.GL_DEBUG_SOURCE_OTHER => "Other",
+ else => "Unknown",
+ };
+
+ const typ_str: []const u8 = switch (typ) {
+ gl.c.GL_DEBUG_TYPE_ERROR => "Error",
+ gl.c.GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR => "Deprecated Behavior",
+ gl.c.GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR => "Undefined Behavior",
+ gl.c.GL_DEBUG_TYPE_PORTABILITY => "Portability Issue",
+ gl.c.GL_DEBUG_TYPE_PERFORMANCE => "Performance Issue",
+ gl.c.GL_DEBUG_TYPE_MARKER => "Marker",
+ gl.c.GL_DEBUG_TYPE_PUSH_GROUP => "Group Push",
+ gl.c.GL_DEBUG_TYPE_POP_GROUP => "Group Pop",
+ gl.c.GL_DEBUG_TYPE_OTHER => "Other",
+ else => "Unknown",
+ };
+
+ const msg_str = msg[0..@intCast(len)];
+
+ (switch (severity) {
+ gl.c.GL_DEBUG_SEVERITY_HIGH => log.err(
+ "[{d}] ({s}: {s}) {s}",
+ .{ id, src_str, typ_str, msg_str },
+ ),
+ gl.c.GL_DEBUG_SEVERITY_MEDIUM => log.warn(
+ "[{d}] ({s}: {s}) {s}",
+ .{ id, src_str, typ_str, msg_str },
+ ),
+ gl.c.GL_DEBUG_SEVERITY_LOW => log.info(
+ "[{d}] ({s}: {s}) {s}",
+ .{ id, src_str, typ_str, msg_str },
+ ),
+ gl.c.GL_DEBUG_SEVERITY_NOTIFICATION => log.debug(
+ "[{d}] ({s}: {s}) {s}",
+ .{ id, src_str, typ_str, msg_str },
+ ),
+ else => log.warn(
+ "UNKNOWN SEVERITY [{d}] ({s}: {s}) {s}",
+ .{ id, src_str, typ_str, msg_str },
+ ),
+ });
+}
+
+/// Prepares the provided GL context, loading it with glad.
+fn prepareContext(getProcAddress: anytype) !void {
+ const version = try gl.glad.load(getProcAddress);
+ const major = gl.glad.versionMajor(@intCast(version));
+ const minor = gl.glad.versionMinor(@intCast(version));
+ errdefer gl.glad.unload();
+ log.info("loaded OpenGL {}.{}", .{ major, minor });
+
+ // Need to check version before trying to enable it
+ if (major < MIN_VERSION_MAJOR or
+ (major == MIN_VERSION_MAJOR and minor < MIN_VERSION_MINOR))
+ {
+ log.warn(
+ "OpenGL version is too old. Ghostty requires OpenGL {d}.{d}",
+ .{ MIN_VERSION_MAJOR, MIN_VERSION_MINOR },
+ );
+ return error.OpenGLOutdated;
+ }
+
+ // Enable debug output for the context.
+ try gl.enable(gl.c.GL_DEBUG_OUTPUT);
+
+ // Register our debug message callback with the OpenGL context.
+ gl.glad.context.DebugMessageCallback.?(glDebugMessageCallback, null);
+
+ // Enable SRGB framebuffer for linear blending support.
+ try gl.enable(gl.c.GL_FRAMEBUFFER_SRGB);
+}
+
+/// This is called early right after surface creation.
+pub fn surfaceInit(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});
+ // }
+ // }
+}
+
+/// 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 begins.
+pub fn threadEnter(self: *const OpenGL, surface: *apprt.Surface) !void {
+ _ = 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.
+ },
+ }
+}
+
+/// Callback called by renderer.Thread when it exits.
+pub fn threadExit(self: *const OpenGL) 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"),
+ }
+}
+
+/// Actions taken before doing anything in `drawFrame`.
+///
+/// Right now there's nothing we need to do for OpenGL.
+pub fn drawFrameStart(self: *OpenGL) void {
+ _ = self;
+}
+
+/// Actions taken after `drawFrame` is done.
+///
+/// Right now there's nothing we need to do for OpenGL.
+pub fn drawFrameEnd(self: *OpenGL) void {
+ _ = self;
+}
+
+pub fn initShaders(
+ self: *const OpenGL,
+ alloc: Allocator,
+ custom_shaders: []const [:0]const u8,
+) !shaders.Shaders {
+ _ = alloc;
+ return try shaders.Shaders.init(
+ self.alloc,
+ custom_shaders,
+ );
+}
+
+/// 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 {
+ 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.
+
+ // 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});
+ };
+
+ // 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);
+}
+
+/// Returns the options to use when constructing buffers.
+pub inline fn bufferOptions(self: OpenGL) bufferpkg.Options {
+ _ = self;
+ return .{
+ .target = .array,
+ .usage = .dynamic_draw,
+ };
+}
+
+pub const instanceBufferOptions = bufferOptions;
+pub const uniformBufferOptions = bufferOptions;
+pub const fgBufferOptions = bufferOptions;
+pub const bgBufferOptions = bufferOptions;
+pub const imageBufferOptions = bufferOptions;
+pub const bgImageBufferOptions = bufferOptions;
+
+/// Returns the options to use when constructing textures.
+pub inline fn textureOptions(self: OpenGL) Texture.Options {
+ _ = self;
+ return .{
+ .format = .rgba,
+ .internal_format = .srgba,
+ .target = .@"2D",
+ .min_filter = .linear,
+ .mag_filter = .linear,
+ .wrap_s = .clamp_to_edge,
+ .wrap_t = .clamp_to_edge,
+ };
+}
+
+/// Returns the options to use when constructing samplers.
+pub inline fn samplerOptions(self: OpenGL) Sampler.Options {
+ _ = self;
+ return .{
+ .min_filter = .linear,
+ .mag_filter = .linear,
+ .wrap_s = .clamp_to_edge,
+ .wrap_t = .clamp_to_edge,
+ };
+}
+
+/// Pixel format for image texture options.
+pub const ImageTextureFormat = enum {
+ /// 1 byte per pixel grayscale.
+ gray,
+ /// 4 bytes per pixel RGBA.
+ rgba,
+ /// 4 bytes per pixel BGRA.
+ bgra,
+
+ fn toPixelFormat(self: ImageTextureFormat) gl.Texture.Format {
+ return switch (self) {
+ .gray => .red,
+ .rgba => .rgba,
+ .bgra => .bgra,
+ };
+ }
+};
+
+/// Returns the options to use when constructing textures for images.
+pub inline fn imageTextureOptions(
+ self: OpenGL,
+ format: ImageTextureFormat,
+ srgb: bool,
+) Texture.Options {
+ _ = self;
+ return .{
+ .format = format.toPixelFormat(),
+ .internal_format = if (srgb) .srgba else .rgba,
+ .target = .@"2D",
+ // TODO: Generate mipmaps for image textures and use
+ // linear_mipmap_linear filtering so that they
+ // look good even when scaled way down.
+ .min_filter = .linear,
+ .mag_filter = .linear,
+ // TODO: Separate out background image options, use
+ // repeating coordinate modes so we don't have
+ // to do the modulus in the shader.
+ .wrap_s = .clamp_to_edge,
+ .wrap_t = .clamp_to_edge,
+ };
+}
+
+/// Initializes a Texture suitable for the provided font atlas.
+pub fn initAtlasTexture(
+ self: *const OpenGL,
+ atlas: *const font.Atlas,
+) Texture.Error!Texture {
+ _ = self;
+ const format: gl.Texture.Format, const internal_format: gl.Texture.InternalFormat =
+ switch (atlas.format) {
+ .grayscale => .{ .red, .red },
+ .bgra => .{ .bgra, .srgba },
+ else => @panic("unsupported atlas format for OpenGL texture"),
+ };
+
+ return try Texture.init(
+ .{
+ .format = format,
+ .internal_format = internal_format,
+ .target = .Rectangle,
+ .min_filter = .nearest,
+ .mag_filter = .nearest,
+ .wrap_s = .clamp_to_edge,
+ .wrap_t = .clamp_to_edge,
+ },
+ atlas.size,
+ atlas.size,
+ null,
+ );
+}
+
+/// Begin a frame.
+pub inline fn beginFrame(
+ self: *const OpenGL,
+ /// Once the frame has been completed, the `frameCompleted` method
+ /// on the renderer is called with the health status of the frame.
+ renderer: *Renderer,
+ /// The target is presented via the provided renderer's API when completed.
+ target: *Target,
+) !Frame {
+ _ = self;
+ return try Frame.begin(.{}, renderer, target);
+}