Files
ghostty/src/renderer/OpenGL.zig
2026-09-09 23:09:12 +08:00

455 lines
14 KiB
Zig

//! Graphics API wrapper for OpenGL.
pub const OpenGL = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
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");
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;
/// 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);
/// 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,
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();
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;
}
/// 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);
}
/// 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;
try self.egl_display.makeCurrent(null, null, self.egl_context);
// Load our function pointers for this thread's threadlocal.
try prepareContext(&gl.egl.getProcAddress);
}
/// 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();
}
/// 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]),
};
}
/// 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;
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`.
///
/// 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,
);
}
/// 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(.{
.width = width,
.height = height,
});
}
/// 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,
} };
}
}
/// 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);
}
};
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 {
_ = 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);
}