mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-12-31 18:52:12 +00:00
The GLSL to MSL conversion process uses a passed-in sampler state for the `iChannel0` parameter and we weren't providing it. This magically worked on Apple Silicon for unknown reasons but failed on Intel GPUs. In normal, hand-written MSL, we'd explicitly create the sampler state as a normal variable (we do this in `shaders.metal` already!), but the Shadertoy conversion stuff doesn't do this, probably because the exact sampler parameters can't be safely known. This fixes a Metal validation error when using custom shaders: ``` -[MTLDebugRenderCommandEncoder validateCommonDrawErrors:]:5970: failed assertion `Draw Errors Validation Fragment Function(main0): missing Sampler binding at index 0 for iChannel0Smplr[0]. ```
462 lines
14 KiB
Zig
462 lines
14 KiB
Zig
//! Graphics API wrapper for OpenGL.
|
|
pub const OpenGL = @This();
|
|
|
|
const std = @import("std");
|
|
const assert = std.debug.assert;
|
|
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 });
|
|
|
|
// 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);
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// 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);
|
|
}
|