diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-04-04 19:22:04 +0300 |
|---|---|---|
| committer | Can Celik <ogulcancelik@gmail.com> | 2026-04-05 01:23:06 +0300 |
| commit | 41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch) | |
| tree | 3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/renderer/OpenGL.zig | |
| parent | 55c138263e190a1e79d4b997bfb9397306c0ec1f (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.zig | 461 |
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); +} |
