ghostty/src/renderer/OpenGL.zig
2023-11-17 21:51:06 -08:00

1682 lines
55 KiB
Zig

//! Rendering implementation for OpenGL.
pub const OpenGL = @This();
const std = @import("std");
const builtin = @import("builtin");
const glfw = @import("glfw");
const assert = std.debug.assert;
const testing = std.testing;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const shadertoy = @import("shadertoy.zig");
const apprt = @import("../apprt.zig");
const configpkg = @import("../config.zig");
const font = @import("../font/main.zig");
const imgui = @import("imgui");
const renderer = @import("../renderer.zig");
const terminal = @import("../terminal/main.zig");
const Terminal = terminal.Terminal;
const gl = @import("opengl");
const trace = @import("tracy").trace;
const math = @import("../math.zig");
const Surface = @import("../Surface.zig");
const CellProgram = @import("opengl/CellProgram.zig");
const custom = @import("opengl/custom.zig");
const log = std.log.scoped(.grid);
/// The runtime can request a single-threaded draw by setting this boolean
/// to true. In this case, the renderer.draw() call is expected to be called
/// from the runtime.
pub const single_threaded_draw = if (@hasDecl(apprt.Surface, "opengl_single_threaded_draw"))
apprt.Surface.opengl_single_threaded_draw
else
false;
const DrawMutex = if (single_threaded_draw) std.Thread.Mutex else void;
const drawMutexZero = if (DrawMutex == void) void{} else .{};
alloc: std.mem.Allocator,
/// The configuration we need derived from the main config.
config: DerivedConfig,
/// Current cell dimensions for this grid.
cell_size: renderer.CellSize,
/// Current screen size dimensions for this grid. This is set on the first
/// resize event, and is not immediately available.
screen_size: ?renderer.ScreenSize,
/// The current set of cells to render. Each set of cells goes into
/// a separate shader call.
cells_bg: std.ArrayListUnmanaged(CellProgram.Cell),
cells: std.ArrayListUnmanaged(CellProgram.Cell),
/// The size of the cells list that was sent to the GPU. This is used
/// to detect when the cells array was reallocated/resized and handle that
/// accordingly.
gl_cells_size: usize = 0,
/// The last length of the cells that was written to the GPU. This is used to
/// determine what data needs to be rewritten on the GPU.
gl_cells_written: usize = 0,
/// Shader program for cell rendering.
gl_state: ?GLState = null,
/// The font structures.
font_group: *font.GroupCache,
font_shaper: font.Shaper,
/// True if the window is focused
focused: bool,
/// The actual foreground color. May differ from the config foreground color if
/// changed by a terminal application
foreground_color: terminal.color.RGB,
/// The actual background color. May differ from the config background color if
/// changed by a terminal application
background_color: terminal.color.RGB,
/// The actual cursor color. May differ from the config cursor color if changed
/// by a terminal application
cursor_color: ?terminal.color.RGB,
/// Padding options
padding: renderer.Options.Padding,
/// The mailbox for communicating with the window.
surface_mailbox: apprt.surface.Mailbox,
/// Deferred operations. This is used to apply changes to the OpenGL context.
/// Some runtimes (GTK) do not support multi-threading so to keep our logic
/// simple we apply all OpenGL context changes in the render() call.
deferred_screen_size: ?SetScreenSize = null,
deferred_font_size: ?SetFontSize = null,
/// If we're drawing with single threaded operations
draw_mutex: DrawMutex = drawMutexZero,
/// Current background to draw. This may not match self.background if the
/// terminal is in reversed mode.
draw_background: terminal.color.RGB,
/// Defererred OpenGL operation to update the screen size.
const SetScreenSize = struct {
size: renderer.ScreenSize,
fn apply(self: SetScreenSize, r: *OpenGL) !void {
const gl_state: *GLState = if (r.gl_state) |*v|
v
else
return error.OpenGLUninitialized;
// Apply our padding
const padding = if (r.padding.balance)
renderer.Padding.balanced(self.size, r.gridSize(self.size), r.cell_size)
else
r.padding.explicit;
const padded_size = self.size.subPadding(padding);
log.debug("GL api: screen size padded={} screen={} grid={} cell={} padding={}", .{
padded_size,
self.size,
r.gridSize(self.size),
r.cell_size,
r.padding.explicit,
});
// Update our viewport for this context to be the entire window.
// OpenGL works in pixels, so we have to use the pixel size.
try gl.viewport(
0,
0,
@intCast(self.size.width),
@intCast(self.size.height),
);
// Update the projection uniform within our shader
try gl_state.cell_program.program.setUniform(
"projection",
// 2D orthographic projection with the full w/h
math.ortho2d(
-1 * @as(f32, @floatFromInt(padding.left)),
@floatFromInt(padded_size.width + padding.right),
@floatFromInt(padded_size.height + padding.bottom),
-1 * @as(f32, @floatFromInt(padding.top)),
),
);
// Update our custom shader resolution
if (gl_state.custom) |*custom_state| {
try custom_state.setScreenSize(self.size);
}
}
};
const SetFontSize = struct {
metrics: font.face.Metrics,
fn apply(self: SetFontSize, r: *const OpenGL) !void {
const gl_state = r.gl_state orelse return error.OpenGLUninitialized;
try gl_state.cell_program.program.setUniform(
"cell_size",
@Vector(2, f32){
@floatFromInt(self.metrics.cell_width),
@floatFromInt(self.metrics.cell_height),
},
);
try gl_state.cell_program.program.setUniform(
"strikethrough_position",
@as(f32, @floatFromInt(self.metrics.strikethrough_position)),
);
try gl_state.cell_program.program.setUniform(
"strikethrough_thickness",
@as(f32, @floatFromInt(self.metrics.strikethrough_thickness)),
);
}
};
/// The configuration for this renderer that is derived from the main
/// configuration. This must be exported so that we don't need to
/// pass around Config pointers which makes memory management a pain.
pub const DerivedConfig = struct {
arena: ArenaAllocator,
font_thicken: bool,
font_features: std.ArrayListUnmanaged([]const u8),
font_styles: font.Group.StyleStatus,
cursor_color: ?terminal.color.RGB,
cursor_text: ?terminal.color.RGB,
cursor_opacity: f64,
background: terminal.color.RGB,
background_opacity: f64,
foreground: terminal.color.RGB,
selection_background: ?terminal.color.RGB,
selection_foreground: ?terminal.color.RGB,
invert_selection_fg_bg: bool,
custom_shaders: std.ArrayListUnmanaged([]const u8),
pub fn init(
alloc_gpa: Allocator,
config: *const configpkg.Config,
) !DerivedConfig {
var arena = ArenaAllocator.init(alloc_gpa);
errdefer arena.deinit();
const alloc = arena.allocator();
// Copy our shaders
const custom_shaders = try config.@"custom-shader".value.list.clone(alloc);
// Copy our font features
const font_features = try config.@"font-feature".list.clone(alloc);
// Get our font styles
var font_styles = font.Group.StyleStatus.initFill(true);
font_styles.set(.bold, config.@"font-style-bold" != .false);
font_styles.set(.italic, config.@"font-style-italic" != .false);
font_styles.set(.bold_italic, config.@"font-style-bold-italic" != .false);
return .{
.background_opacity = @max(0, @min(1, config.@"background-opacity")),
.font_thicken = config.@"font-thicken",
.font_features = font_features,
.font_styles = font_styles,
.cursor_color = if (config.@"cursor-color") |col|
col.toTerminalRGB()
else
null,
.cursor_text = if (config.@"cursor-text") |txt|
txt.toTerminalRGB()
else
null,
.cursor_opacity = @max(0, @min(1, config.@"cursor-opacity")),
.background = config.background.toTerminalRGB(),
.foreground = config.foreground.toTerminalRGB(),
.invert_selection_fg_bg = config.@"selection-invert-fg-bg",
.selection_background = if (config.@"selection-background") |bg|
bg.toTerminalRGB()
else
null,
.selection_foreground = if (config.@"selection-foreground") |bg|
bg.toTerminalRGB()
else
null,
.custom_shaders = custom_shaders,
.arena = arena,
};
}
pub fn deinit(self: *DerivedConfig) void {
self.arena.deinit();
}
};
pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL {
// Create the initial font shaper
var shaper = try font.Shaper.init(alloc, .{
.features = options.config.font_features.items,
});
errdefer shaper.deinit();
// Setup our font metrics uniform
const metrics = try resetFontMetrics(
alloc,
options.font_group,
options.config.font_thicken,
);
var gl_state = try GLState.init(alloc, options.config, options.font_group);
errdefer gl_state.deinit();
return OpenGL{
.alloc = alloc,
.config = options.config,
.cells_bg = .{},
.cells = .{},
.cell_size = .{ .width = metrics.cell_width, .height = metrics.cell_height },
.screen_size = null,
.gl_state = gl_state,
.font_group = options.font_group,
.font_shaper = shaper,
.draw_background = options.config.background,
.focused = true,
.foreground_color = options.config.foreground,
.background_color = options.config.background,
.cursor_color = options.config.cursor_color,
.padding = options.padding,
.surface_mailbox = options.surface_mailbox,
.deferred_font_size = .{ .metrics = metrics },
};
}
pub fn deinit(self: *OpenGL) void {
self.font_shaper.deinit();
if (self.gl_state) |*v| v.deinit(self.alloc);
self.cells.deinit(self.alloc);
self.cells_bg.deinit(self.alloc);
self.config.deinit();
self.* = undefined;
}
/// Returns the hints that we want for this
pub fn glfwWindowHints(config: *const configpkg.Config) glfw.Window.Hints {
return .{
.context_version_major = 3,
.context_version_minor = 3,
.opengl_profile = .opengl_core_profile,
.opengl_forward_compat = true,
.cocoa_graphics_switching = builtin.os.tag == .macos,
.cocoa_retina_framebuffer = true,
.transparent_framebuffer = config.@"background-opacity" < 1,
};
}
/// This is called early right after surface creation.
pub fn surfaceInit(surface: *apprt.Surface) !void {
// Treat this like a thread entry
const self: OpenGL = undefined;
switch (apprt.runtime) {
else => @compileError("unsupported app runtime for OpenGL"),
apprt.gtk => {
// GTK uses global OpenGL context so we load from null.
const version = try gl.glad.load(null);
errdefer gl.glad.unload();
log.info("loaded OpenGL {}.{}", .{
gl.glad.versionMajor(@intCast(version)),
gl.glad.versionMinor(@intCast(version)),
});
},
apprt.glfw => try self.threadEnter(surface),
}
// 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;
// For GLFW, we grabbed the OpenGL context in surfaceInit and we
// need to release it before we start the renderer thread.
if (apprt.runtime == apprt.glfw) {
glfw.makeContextCurrent(null);
}
}
/// Called when the OpenGL context is made invalid, so we need to free
/// all previous resources and stop rendering.
pub fn displayUnrealized(self: *OpenGL) void {
if (single_threaded_draw) self.draw_mutex.lock();
defer if (single_threaded_draw) self.draw_mutex.unlock();
if (self.gl_state) |*v| {
v.deinit(self.alloc);
self.gl_state = null;
}
}
/// Called when the OpenGL is ready to be initialized.
pub fn displayRealize(self: *OpenGL) !void {
if (single_threaded_draw) self.draw_mutex.lock();
defer if (single_threaded_draw) self.draw_mutex.unlock();
// Reset our GPU uniforms
const metrics = try resetFontMetrics(
self.alloc,
self.font_group,
self.config.font_thicken,
);
// Make our new state
var gl_state = try GLState.init(self.alloc, self.config, self.font_group);
errdefer gl_state.deinit();
// Unrealize if we have to
if (self.gl_state) |*v| v.deinit(self.alloc);
// Set our new state
self.gl_state = gl_state;
// Make sure we invalidate all the fields so that we
// reflush everything
self.gl_cells_size = 0;
self.gl_cells_written = 0;
self.font_group.atlas_greyscale.modified = true;
self.font_group.atlas_color.modified = true;
// We need to reset our uniforms
if (self.screen_size) |size| {
self.deferred_screen_size = .{ .size = size };
}
self.deferred_font_size = .{ .metrics = metrics };
}
/// Callback called by renderer.Thread when it begins.
pub fn threadEnter(self: *const OpenGL, surface: *apprt.Surface) !void {
_ = self;
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.glfw => {
// We need to make the OpenGL context current. OpenGL requires
// that a single thread own the a single OpenGL context (if any). This
// ensures that the context switches over to our thread. Important:
// the prior thread MUST have detached the context prior to calling
// this entrypoint.
glfw.makeContextCurrent(surface.window);
errdefer glfw.makeContextCurrent(null);
glfw.swapInterval(1);
// Load OpenGL bindings. This API is context-aware so this sets
// a threadlocal context for these pointers.
const version = try gl.glad.load(&glfw.getProcAddress);
errdefer gl.glad.unload();
log.info("loaded OpenGL {}.{}", .{
gl.glad.versionMajor(@intCast(version)),
gl.glad.versionMinor(@intCast(version)),
});
},
}
}
/// 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.glfw => {
gl.glad.unload();
glfw.makeContextCurrent(null);
},
}
}
/// True if our renderer has animations so that a higher frequency
/// timer is used.
pub fn hasAnimations(self: *const OpenGL) bool {
const state = self.gl_state orelse return false;
return state.custom != null;
}
/// Callback when the focus changes for the terminal this is rendering.
///
/// Must be called on the render thread.
pub fn setFocus(self: *OpenGL, focus: bool) !void {
self.focused = focus;
}
/// Set the new font size.
///
/// Must be called on the render thread.
pub fn setFontSize(self: *OpenGL, size: font.face.DesiredSize) !void {
if (single_threaded_draw) self.draw_mutex.lock();
defer if (single_threaded_draw) self.draw_mutex.unlock();
log.info("set font size={}", .{size});
// Set our new size, this will also reset our font atlas.
try self.font_group.setSize(size);
// Reset our GPU uniforms
const metrics = try resetFontMetrics(
self.alloc,
self.font_group,
self.config.font_thicken,
);
// Defer our GPU updates
self.deferred_font_size = .{ .metrics = metrics };
// Recalculate our cell size. If it is the same as before, then we do
// nothing since the grid size couldn't have possibly changed.
const new_cell_size = .{ .width = metrics.cell_width, .height = metrics.cell_height };
if (std.meta.eql(self.cell_size, new_cell_size)) return;
self.cell_size = new_cell_size;
// Notify the window that the cell size changed.
_ = self.surface_mailbox.push(.{
.cell_size = new_cell_size,
}, .{ .forever = {} });
}
/// Reload the font metrics, recalculate cell size, and send that all
/// down to the GPU.
fn resetFontMetrics(
alloc: Allocator,
font_group: *font.GroupCache,
font_thicken: bool,
) !font.face.Metrics {
// Get our cell metrics based on a regular font ascii 'M'. Why 'M'?
// Doesn't matter, any normal ASCII will do we're just trying to make
// sure we use the regular font.
const metrics = metrics: {
const index = (try font_group.indexForCodepoint(alloc, 'M', .regular, .text)).?;
const face = try font_group.group.faceFromIndex(index);
break :metrics face.metrics;
};
log.debug("cell dimensions={}", .{metrics});
// Set details for our sprite font
font_group.group.sprite = font.sprite.Face{
.width = metrics.cell_width,
.height = metrics.cell_height,
.thickness = metrics.underline_thickness * @as(u32, if (font_thicken) 2 else 1),
.underline_position = metrics.underline_position,
};
return metrics;
}
/// The primary render callback that is completely thread-safe.
pub fn updateFrame(
self: *OpenGL,
surface: *apprt.Surface,
state: *renderer.State,
cursor_blink_visible: bool,
) !void {
_ = surface;
// Data we extract out of the critical area.
const Critical = struct {
gl_bg: terminal.color.RGB,
selection: ?terminal.Selection,
screen: terminal.Screen,
preedit: ?renderer.State.Preedit,
cursor_style: ?renderer.CursorStyle,
};
// Update all our data as tightly as possible within the mutex.
var critical: Critical = critical: {
state.mutex.lock();
defer state.mutex.unlock();
// If we're in a synchronized output state, we pause all rendering.
if (state.terminal.modes.get(.synchronized_output)) {
log.debug("synchronized output started, skipping render", .{});
return;
}
// Swap bg/fg if the terminal is reversed
const bg = self.background_color;
const fg = self.foreground_color;
defer {
self.background_color = bg;
self.foreground_color = fg;
}
if (state.terminal.modes.get(.reverse_colors)) {
self.background_color = fg;
self.foreground_color = bg;
}
// We used to share terminal state, but we've since learned through
// analysis that it is faster to copy the terminal state than to
// hold the lock wile rebuilding GPU cells.
const viewport_bottom = state.terminal.screen.viewportIsBottom();
var screen_copy = if (viewport_bottom) try state.terminal.screen.clone(
self.alloc,
.{ .active = 0 },
.{ .active = state.terminal.rows - 1 },
) else try state.terminal.screen.clone(
self.alloc,
.{ .viewport = 0 },
.{ .viewport = state.terminal.rows - 1 },
);
errdefer screen_copy.deinit();
// Convert our selection to viewport points because we copy only
// the viewport above.
const selection: ?terminal.Selection = if (state.terminal.screen.selection) |sel|
sel.toViewport(&state.terminal.screen)
else
null;
// Whether to draw our cursor or not.
const cursor_style = renderer.cursorStyle(
state,
self.focused,
cursor_blink_visible,
);
break :critical .{
.gl_bg = self.background_color,
.selection = selection,
.screen = screen_copy,
.preedit = if (cursor_style != null) state.preedit else null,
.cursor_style = cursor_style,
};
};
defer critical.screen.deinit();
// Grab our draw mutex if we have it and update our data
{
if (single_threaded_draw) self.draw_mutex.lock();
defer if (single_threaded_draw) self.draw_mutex.unlock();
// Set our draw data
self.draw_background = critical.gl_bg;
// Build our GPU cells
try self.rebuildCells(
critical.selection,
&critical.screen,
critical.preedit,
critical.cursor_style,
);
}
}
/// rebuildCells rebuilds all the GPU cells from our CPU state. This is a
/// slow operation but ensures that the GPU state exactly matches the CPU state.
/// In steady-state operation, we use some GPU tricks to send down stale data
/// that is ignored. This accumulates more memory; rebuildCells clears it.
///
/// Note this doesn't have to typically be manually called. Internally,
/// the renderer will do this when it needs more memory space.
pub fn rebuildCells(
self: *OpenGL,
term_selection: ?terminal.Selection,
screen: *terminal.Screen,
preedit: ?renderer.State.Preedit,
cursor_style_: ?renderer.CursorStyle,
) !void {
const t = trace(@src());
defer t.end();
// Bg cells at most will need space for the visible screen size
self.cells_bg.clearRetainingCapacity();
try self.cells_bg.ensureTotalCapacity(self.alloc, screen.rows * screen.cols);
// For now, we just ensure that we have enough cells for all the lines
// we have plus a full width. This is very likely too much but its
// the probably close enough while guaranteeing no more allocations.
self.cells.clearRetainingCapacity();
try self.cells.ensureTotalCapacity(
self.alloc,
// * 3 for background modes and cursor and underlines
// + 1 for cursor
(screen.rows * screen.cols * 2) + 1,
);
// We've written no data to the GPU, refresh it all
self.gl_cells_written = 0;
// Determine our x/y range for preedit. We don't want to render anything
// here because we will render the preedit separately.
const preedit_range: ?struct {
y: usize,
x: [2]usize,
} = if (preedit) |preedit_v| preedit: {
break :preedit .{
.y = screen.cursor.y,
.x = preedit_v.range(screen.cursor.x, screen.cols - 1),
};
} else null;
// This is the cell that has [mode == .fg] and is underneath our cursor.
// We keep track of it so that we can invert the colors so the character
// remains visible.
var cursor_cell: ?CellProgram.Cell = null;
// Build each cell
var rowIter = screen.rowIterator(.viewport);
var y: usize = 0;
while (rowIter.next()) |row| {
defer y += 1;
// Our selection value is only non-null if this selection happens
// to contain this row. This selection value will be set to only be
// the selection that contains this row. This way, if the selection
// changes but not for this line, we don't invalidate the cache.
const selection = sel: {
if (term_selection) |sel| {
const screen_point = (terminal.point.Viewport{
.x = 0,
.y = y,
}).toScreen(screen);
// If we are selected, we our colors are just inverted fg/bg.
if (sel.containedRow(screen, screen_point)) |row_sel| {
break :sel row_sel;
}
}
break :sel null;
};
// See Metal.zig
const cursor_row = if (cursor_style_) |cursor_style|
cursor_style == .block and
screen.viewportIsBottom() and
y == screen.cursor.y
else
false;
// True if we want to do font shaping around the cursor. We want to
// do font shaping as long as the cursor is enabled.
const shape_cursor = screen.viewportIsBottom() and
y == screen.cursor.y;
// If this is the row with our cursor, then we may have to modify
// the cell with the cursor.
const start_i: usize = self.cells.items.len;
defer if (cursor_row) {
// If we're on a wide spacer tail, then we want to look for
// the previous cell.
const screen_cell = row.getCell(screen.cursor.x);
const x = screen.cursor.x - @intFromBool(screen_cell.attrs.wide_spacer_tail);
for (self.cells.items[start_i..]) |cell| {
if (cell.grid_col == x and
(cell.mode == .fg or cell.mode == .fg_color))
{
cursor_cell = cell;
break;
}
}
};
// Split our row into runs and shape each one.
var iter = self.font_shaper.runIterator(
self.font_group,
row,
selection,
if (shape_cursor) screen.cursor.x else null,
);
while (try iter.next(self.alloc)) |run| {
for (try self.font_shaper.shape(run)) |shaper_cell| {
// If this cell falls within our preedit range then we skip it.
// We do this so we don't have conflicting data on the same
// cell.
if (preedit_range) |range| {
if (range.y == y and
shaper_cell.x >= range.x[0] and
shaper_cell.x <= range.x[1])
{
continue;
}
}
if (self.updateCell(
term_selection,
screen,
row.getCell(shaper_cell.x),
shaper_cell,
run,
shaper_cell.x,
y,
)) |update| {
assert(update);
} else |err| {
log.warn("error building cell, will be invalid x={} y={}, err={}", .{
shaper_cell.x,
y,
err,
});
}
}
}
// Set row is not dirty anymore
row.setDirty(false);
}
// Add the cursor at the end so that it overlays everything. If we have
// a cursor cell then we invert the colors on that and add it in so
// that we can always see it.
if (cursor_style_) |cursor_style| cursor_style: {
// If we have a preedit, we try to render the preedit text on top
// of the cursor.
if (preedit) |preedit_v| {
const range = preedit_range.?;
var x = range.x[0];
for (preedit_v.codepoints[0..preedit_v.len]) |cp| {
self.addPreeditCell(cp, x, range.y) catch |err| {
log.warn("error building preedit cell, will be invalid x={} y={}, err={}", .{
x,
range.y,
err,
});
};
x += if (cp.wide) 2 else 1;
}
// Preedit hides the cursor
break :cursor_style;
}
_ = self.addCursor(screen, cursor_style);
if (cursor_cell) |*cell| {
if (cell.mode == .fg) {
if (self.config.cursor_text) |txt| {
cell.fg_r = txt.r;
cell.fg_g = txt.g;
cell.fg_b = txt.b;
cell.fg_a = 255;
} else {
cell.fg_r = 0;
cell.fg_g = 0;
cell.fg_b = 0;
cell.fg_a = 255;
}
}
self.cells.appendAssumeCapacity(cell.*);
}
}
// Some debug mode safety checks
if (std.debug.runtime_safety) {
for (self.cells_bg.items) |cell| assert(cell.mode == .bg);
for (self.cells.items) |cell| assert(cell.mode != .bg);
}
}
fn addPreeditCell(
self: *OpenGL,
cp: renderer.State.Preedit.Codepoint,
x: usize,
y: usize,
) !void {
// Preedit is rendered inverted
const bg = self.foreground_color;
const fg = self.background_color;
// Get the font for this codepoint.
const font_index = if (self.font_group.indexForCodepoint(
self.alloc,
@intCast(cp.codepoint),
.regular,
.text,
)) |index| index orelse return else |_| return;
// Get the font face so we can get the glyph
const face = self.font_group.group.faceFromIndex(font_index) catch |err| {
log.warn("error getting face for font_index={} err={}", .{ font_index, err });
return;
};
// Use the face to now get the glyph index
const glyph_index = face.glyphIndex(@intCast(cp.codepoint)) orelse return;
// Render the glyph for our preedit text
const glyph = self.font_group.renderGlyph(
self.alloc,
font_index,
glyph_index,
.{},
) catch |err| {
log.warn("error rendering preedit glyph err={}", .{err});
return;
};
// Add our opaque background cell
self.cells_bg.appendAssumeCapacity(.{
.mode = .bg,
.grid_col = @intCast(x),
.grid_row = @intCast(y),
.grid_width = if (cp.wide) 2 else 1,
.glyph_x = 0,
.glyph_y = 0,
.glyph_width = 0,
.glyph_height = 0,
.glyph_offset_x = 0,
.glyph_offset_y = 0,
.fg_r = 0,
.fg_g = 0,
.fg_b = 0,
.fg_a = 0,
.bg_r = bg.r,
.bg_g = bg.g,
.bg_b = bg.b,
.bg_a = 255,
});
// Add our text
self.cells.appendAssumeCapacity(.{
.mode = .fg,
.grid_col = @intCast(x),
.grid_row = @intCast(y),
.grid_width = if (cp.wide) 2 else 1,
.glyph_x = glyph.atlas_x,
.glyph_y = glyph.atlas_y,
.glyph_width = glyph.width,
.glyph_height = glyph.height,
.glyph_offset_x = glyph.offset_x,
.glyph_offset_y = glyph.offset_y,
.fg_r = fg.r,
.fg_g = fg.g,
.fg_b = fg.b,
.fg_a = 255,
.bg_r = 0,
.bg_g = 0,
.bg_b = 0,
.bg_a = 0,
});
}
fn addCursor(
self: *OpenGL,
screen: *terminal.Screen,
cursor_style: renderer.CursorStyle,
) ?*const CellProgram.Cell {
// Add the cursor. We render the cursor over the wide character if
// we're on the wide characer tail.
const wide, const x = cell: {
// The cursor goes over the screen cursor position.
const cell = screen.getCell(
.active,
screen.cursor.y,
screen.cursor.x,
);
if (!cell.attrs.wide_spacer_tail or screen.cursor.x == 0)
break :cell .{ cell.attrs.wide, screen.cursor.x };
// If we're part of a wide character, we move the cursor back to
// the actual character.
break :cell .{ screen.getCell(
.active,
screen.cursor.y,
screen.cursor.x - 1,
).attrs.wide, screen.cursor.x - 1 };
};
const color = self.cursor_color orelse self.foreground_color;
const alpha: u8 = if (!self.focused) 255 else alpha: {
const alpha = 255 * self.config.cursor_opacity;
break :alpha @intFromFloat(@ceil(alpha));
};
const sprite: font.Sprite = switch (cursor_style) {
.block => .cursor_rect,
.block_hollow => .cursor_hollow_rect,
.bar => .cursor_bar,
.underline => .underline,
};
const glyph = self.font_group.renderGlyph(
self.alloc,
font.sprite_index,
@intFromEnum(sprite),
.{ .cell_width = if (wide) 2 else 1 },
) catch |err| {
log.warn("error rendering cursor glyph err={}", .{err});
return null;
};
self.cells.appendAssumeCapacity(.{
.mode = .fg,
.grid_col = @intCast(x),
.grid_row = @intCast(screen.cursor.y),
.grid_width = if (wide) 2 else 1,
.fg_r = color.r,
.fg_g = color.g,
.fg_b = color.b,
.fg_a = alpha,
.bg_r = 0,
.bg_g = 0,
.bg_b = 0,
.bg_a = 0,
.glyph_x = glyph.atlas_x,
.glyph_y = glyph.atlas_y,
.glyph_width = glyph.width,
.glyph_height = glyph.height,
.glyph_offset_x = glyph.offset_x,
.glyph_offset_y = glyph.offset_y,
});
return &self.cells.items[self.cells.items.len - 1];
}
/// Update a single cell. The bool returns whether the cell was updated
/// or not. If the cell wasn't updated, a full refreshCells call is
/// needed.
pub fn updateCell(
self: *OpenGL,
selection: ?terminal.Selection,
screen: *terminal.Screen,
cell: terminal.Screen.Cell,
shaper_cell: font.shape.Cell,
shaper_run: font.shape.TextRun,
x: usize,
y: usize,
) !bool {
const t = trace(@src());
defer t.end();
const BgFg = struct {
/// Background is optional because in un-inverted mode
/// it may just be equivalent to the default background in
/// which case we do nothing to save on GPU render time.
bg: ?terminal.color.RGB,
/// Fg is always set to some color, though we may not render
/// any fg if the cell is empty or has no attributes like
/// underline.
fg: terminal.color.RGB,
};
// True if this cell is selected
// TODO(perf): we can check in advance if selection is in
// our viewport at all and not run this on every point.
const selected: bool = if (selection) |sel| selected: {
const screen_point = (terminal.point.Viewport{
.x = x,
.y = y,
}).toScreen(screen);
break :selected sel.contains(screen_point);
} else false;
// The colors for the cell.
const colors: BgFg = colors: {
// The normal cell result
const cell_res: BgFg = if (!cell.attrs.inverse) .{
// In normal mode, background and fg match the cell. We
// un-optionalize the fg by defaulting to our fg color.
.bg = if (cell.attrs.has_bg) cell.bg else null,
.fg = if (cell.attrs.has_fg) cell.fg else self.foreground_color,
} else .{
// In inverted mode, the background MUST be set to something
// (is never null) so it is either the fg or default fg. The
// fg is either the bg or default background.
.bg = if (cell.attrs.has_fg) cell.fg else self.foreground_color,
.fg = if (cell.attrs.has_bg) cell.bg else self.background_color,
};
// If we are selected, we our colors are just inverted fg/bg
const selection_res: ?BgFg = if (selected) .{
.bg = if (self.config.invert_selection_fg_bg)
cell_res.fg
else
self.config.selection_background orelse self.foreground_color,
.fg = if (self.config.invert_selection_fg_bg)
cell_res.bg orelse self.background_color
else
self.config.selection_foreground orelse self.background_color,
} else null;
// If the cell is "invisible" then we just make fg = bg so that
// the cell is transparent but still copy-able.
const res: BgFg = selection_res orelse cell_res;
if (cell.attrs.invisible) {
break :colors BgFg{
.bg = res.bg,
.fg = res.bg orelse self.background_color,
};
}
break :colors res;
};
// Calculate the amount of space we need in the cells list.
const needed = needed: {
var i: usize = 0;
if (colors.bg != null) i += 1;
if (!cell.empty()) i += 1;
if (cell.attrs.underline != .none) i += 1;
if (cell.attrs.strikethrough) i += 1;
break :needed i;
};
if (self.cells.items.len + needed > self.cells.capacity) return false;
// Alpha multiplier
const alpha: u8 = if (cell.attrs.faint) 175 else 255;
// If the cell has a background, we always draw it.
if (colors.bg) |rgb| {
// Determine our background alpha. If we have transparency configured
// then this is dynamic depending on some situations. This is all
// in an attempt to make transparency look the best for various
// situations. See inline comments.
const bg_alpha: u8 = bg_alpha: {
const default: u8 = 255;
if (self.config.background_opacity >= 1) break :bg_alpha default;
// If we're selected, we do not apply background opacity
if (selected) break :bg_alpha default;
// If we're reversed, do not apply background opacity
if (cell.attrs.inverse) break :bg_alpha default;
// If we have a background and its not the default background
// then we apply background opacity
if (cell.attrs.has_bg and !std.meta.eql(rgb, self.background_color)) {
break :bg_alpha default;
}
// We apply background opacity.
var bg_alpha: f64 = @floatFromInt(default);
bg_alpha *= self.config.background_opacity;
bg_alpha = @ceil(bg_alpha);
break :bg_alpha @intFromFloat(bg_alpha);
};
self.cells_bg.appendAssumeCapacity(.{
.mode = .bg,
.grid_col = @intCast(x),
.grid_row = @intCast(y),
.grid_width = cell.widthLegacy(),
.glyph_x = 0,
.glyph_y = 0,
.glyph_width = 0,
.glyph_height = 0,
.glyph_offset_x = 0,
.glyph_offset_y = 0,
.fg_r = 0,
.fg_g = 0,
.fg_b = 0,
.fg_a = 0,
.bg_r = rgb.r,
.bg_g = rgb.g,
.bg_b = rgb.b,
.bg_a = bg_alpha,
});
}
// If the cell has a character, draw it
if (cell.char > 0) {
// Render
const glyph = try self.font_group.renderGlyph(
self.alloc,
shaper_run.font_index,
shaper_cell.glyph_index,
.{
.max_height = @intCast(self.cell_size.height),
.thicken = self.config.font_thicken,
},
);
// If we're rendering a color font, we use the color atlas
const presentation = try self.font_group.group.presentationFromIndex(shaper_run.font_index);
const mode: CellProgram.CellMode = switch (presentation) {
.text => .fg,
.emoji => .fg_color,
};
self.cells.appendAssumeCapacity(.{
.mode = mode,
.grid_col = @intCast(x),
.grid_row = @intCast(y),
.grid_width = cell.widthLegacy(),
.glyph_x = glyph.atlas_x,
.glyph_y = glyph.atlas_y,
.glyph_width = glyph.width,
.glyph_height = glyph.height,
.glyph_offset_x = glyph.offset_x,
.glyph_offset_y = glyph.offset_y,
.fg_r = colors.fg.r,
.fg_g = colors.fg.g,
.fg_b = colors.fg.b,
.fg_a = alpha,
.bg_r = 0,
.bg_g = 0,
.bg_b = 0,
.bg_a = 0,
});
}
if (cell.attrs.underline != .none) {
const sprite: font.Sprite = switch (cell.attrs.underline) {
.none => unreachable,
.single => .underline,
.double => .underline_double,
.dotted => .underline_dotted,
.dashed => .underline_dashed,
.curly => .underline_curly,
};
const underline_glyph = try self.font_group.renderGlyph(
self.alloc,
font.sprite_index,
@intFromEnum(sprite),
.{ .cell_width = if (cell.attrs.wide) 2 else 1 },
);
const color = if (cell.attrs.underline_color) cell.underline_fg else colors.fg;
self.cells.appendAssumeCapacity(.{
.mode = .fg,
.grid_col = @intCast(x),
.grid_row = @intCast(y),
.grid_width = cell.widthLegacy(),
.glyph_x = underline_glyph.atlas_x,
.glyph_y = underline_glyph.atlas_y,
.glyph_width = underline_glyph.width,
.glyph_height = underline_glyph.height,
.glyph_offset_x = underline_glyph.offset_x,
.glyph_offset_y = underline_glyph.offset_y,
.fg_r = color.r,
.fg_g = color.g,
.fg_b = color.b,
.fg_a = alpha,
.bg_r = 0,
.bg_g = 0,
.bg_b = 0,
.bg_a = 0,
});
}
if (cell.attrs.strikethrough) {
self.cells.appendAssumeCapacity(.{
.mode = .strikethrough,
.grid_col = @intCast(x),
.grid_row = @intCast(y),
.grid_width = cell.widthLegacy(),
.glyph_x = 0,
.glyph_y = 0,
.glyph_width = 0,
.glyph_height = 0,
.glyph_offset_x = 0,
.glyph_offset_y = 0,
.fg_r = colors.fg.r,
.fg_g = colors.fg.g,
.fg_b = colors.fg.b,
.fg_a = alpha,
.bg_r = 0,
.bg_g = 0,
.bg_b = 0,
.bg_a = 0,
});
}
return true;
}
/// Returns the grid size for a given screen size. This is safe to call
/// on any thread.
fn gridSize(self: *const OpenGL, screen_size: renderer.ScreenSize) renderer.GridSize {
return renderer.GridSize.init(
screen_size.subPadding(self.padding.explicit),
self.cell_size,
);
}
/// Update the configuration.
pub fn changeConfig(self: *OpenGL, config: *DerivedConfig) !void {
// On configuration change we always reset our font group. There
// are a variety of configurations that can change font settings
// so to be safe we just always reset it. This has a performance hit
// when its not necessary but config reloading shouldn't be so
// common to cause a problem.
self.font_group.reset();
self.font_group.group.styles = config.font_styles;
self.font_group.atlas_greyscale.clear();
self.font_group.atlas_color.clear();
// We always redo the font shaper in case font features changed. We
// could check to see if there was an actual config change but this is
// easier and rare enough to not cause performance issues.
{
var font_shaper = try font.Shaper.init(self.alloc, .{
.features = config.font_features.items,
});
errdefer font_shaper.deinit();
self.font_shaper.deinit();
self.font_shaper = font_shaper;
}
self.config.deinit();
self.config = config.*;
}
/// Set the screen size for rendering. This will update the projection
/// used for the shader so that the scaling of the grid is correct.
pub fn setScreenSize(
self: *OpenGL,
dim: renderer.ScreenSize,
pad: renderer.Padding,
) !void {
if (single_threaded_draw) self.draw_mutex.lock();
defer if (single_threaded_draw) self.draw_mutex.unlock();
// Reset our buffer sizes so that we free memory when the screen shrinks.
// This could be made more clever by only doing this when the screen
// shrinks but the performance cost really isn't that much.
self.cells.clearAndFree(self.alloc);
self.cells_bg.clearAndFree(self.alloc);
// Store our screen size
self.screen_size = dim;
self.padding.explicit = pad;
// Recalculate the rows/columns.
const grid_size = self.gridSize(dim);
log.debug("screen size screen={} grid={} cell={} padding={}", .{
dim,
grid_size,
self.cell_size,
self.padding.explicit,
});
// Defer our OpenGL updates
self.deferred_screen_size = .{ .size = dim };
}
/// Updates the font texture atlas if it is dirty.
fn flushAtlas(self: *OpenGL) !void {
const gl_state = self.gl_state orelse return;
{
const atlas = &self.font_group.atlas_greyscale;
if (atlas.modified) {
atlas.modified = false;
var texbind = try gl_state.texture.bind(.@"2D");
defer texbind.unbind();
if (atlas.resized) {
atlas.resized = false;
try texbind.image2D(
0,
.red,
@intCast(atlas.size),
@intCast(atlas.size),
0,
.red,
.UnsignedByte,
atlas.data.ptr,
);
} else {
try texbind.subImage2D(
0,
0,
0,
@intCast(atlas.size),
@intCast(atlas.size),
.red,
.UnsignedByte,
atlas.data.ptr,
);
}
}
}
{
const atlas = &self.font_group.atlas_color;
if (atlas.modified) {
atlas.modified = false;
var texbind = try gl_state.texture_color.bind(.@"2D");
defer texbind.unbind();
if (atlas.resized) {
atlas.resized = false;
try texbind.image2D(
0,
.rgba,
@intCast(atlas.size),
@intCast(atlas.size),
0,
.bgra,
.UnsignedByte,
atlas.data.ptr,
);
} else {
try texbind.subImage2D(
0,
0,
0,
@intCast(atlas.size),
@intCast(atlas.size),
.bgra,
.UnsignedByte,
atlas.data.ptr,
);
}
}
}
}
/// Render renders the current cell state. This will not modify any of
/// the cells.
pub fn drawFrame(self: *OpenGL, surface: *apprt.Surface) !void {
const t = trace(@src());
defer t.end();
// If we're in single-threaded more we grab a lock since we use shared data.
if (single_threaded_draw) self.draw_mutex.lock();
defer if (single_threaded_draw) self.draw_mutex.unlock();
const gl_state: *GLState = if (self.gl_state) |*v| v else return;
// Draw our terminal cells
try self.drawCellProgram(gl_state);
// Draw our custom shaders
if (gl_state.custom) |*custom_state| {
try self.drawCustomPrograms(custom_state);
}
// Swap our window buffers
switch (apprt.runtime) {
apprt.glfw => surface.window.swapBuffers(),
apprt.gtk => {},
else => @compileError("unsupported runtime"),
}
}
fn drawCustomPrograms(
self: *OpenGL,
custom_state: *custom.State,
) !void {
_ = self;
// Bind our state that is global to all custom shaders
const custom_bind = try custom_state.bind();
defer custom_bind.unbind();
// Setup the new frame
try custom_state.newFrame();
// Go through each custom shader and draw it.
for (custom_state.programs) |program| {
// Bind our cell program state, buffers
const bind = try program.bind();
defer bind.unbind();
try gl.drawElementsInstanced(
gl.c.GL_TRIANGLES,
6,
gl.c.GL_UNSIGNED_BYTE,
1,
);
}
}
/// Runs the cell program (shaders) to draw the terminal grid.
fn drawCellProgram(
self: *OpenGL,
gl_state: *const GLState,
) !void {
// Try to flush our atlas, this will only do something if there
// are changes to the atlas.
try self.flushAtlas();
// If we have custom shaders, then we draw to the custom
// shader framebuffer.
const fbobind: ?gl.Framebuffer.Binding = fbobind: {
const state = gl_state.custom orelse break :fbobind null;
break :fbobind try state.fbo.bind(.framebuffer);
};
defer if (fbobind) |v| v.unbind();
// Clear the surface
gl.clearColor(
@as(f32, @floatFromInt(self.draw_background.r)) / 255,
@as(f32, @floatFromInt(self.draw_background.g)) / 255,
@as(f32, @floatFromInt(self.draw_background.b)) / 255,
@floatCast(self.config.background_opacity),
);
gl.clear(gl.c.GL_COLOR_BUFFER_BIT);
// Bind our cell program state, buffers
const bind = try gl_state.cell_program.bind();
defer bind.unbind();
// Bind our textures
try gl.Texture.active(gl.c.GL_TEXTURE0);
var texbind = try gl_state.texture.bind(.@"2D");
defer texbind.unbind();
try gl.Texture.active(gl.c.GL_TEXTURE1);
var texbind1 = try gl_state.texture_color.bind(.@"2D");
defer texbind1.unbind();
// If we have deferred operations, run them.
if (self.deferred_screen_size) |v| {
try v.apply(self);
self.deferred_screen_size = null;
}
if (self.deferred_font_size) |v| {
try v.apply(self);
self.deferred_font_size = null;
}
// Draw our background, then draw the fg on top of it.
try self.drawCells(bind.vbo, self.cells_bg);
try self.drawCells(bind.vbo, self.cells);
}
/// Loads some set of cell data into our buffer and issues a draw call.
/// This expects all the OpenGL state to be setup.
///
/// Future: when we move to multiple shaders, this will go away and
/// we'll have a draw call per-shader.
fn drawCells(
self: *OpenGL,
binding: gl.Buffer.Binding,
cells: std.ArrayListUnmanaged(CellProgram.Cell),
) !void {
// If we have no cells to render, then we render nothing.
if (cells.items.len == 0) return;
// Todo: get rid of this completely
self.gl_cells_written = 0;
// Our allocated buffer on the GPU is smaller than our capacity.
// We reallocate a new buffer with the full new capacity.
if (self.gl_cells_size < cells.capacity) {
log.info("reallocating GPU buffer old={} new={}", .{
self.gl_cells_size,
cells.capacity,
});
try binding.setDataNullManual(
@sizeOf(CellProgram.Cell) * cells.capacity,
.static_draw,
);
self.gl_cells_size = cells.capacity;
self.gl_cells_written = 0;
}
// If we have data to write to the GPU, send it.
if (self.gl_cells_written < cells.items.len) {
const data = cells.items[self.gl_cells_written..];
// log.info("sending {} cells to GPU", .{data.len});
try binding.setSubData(self.gl_cells_written * @sizeOf(CellProgram.Cell), data);
self.gl_cells_written += data.len;
assert(data.len > 0);
assert(self.gl_cells_written <= cells.items.len);
}
try gl.drawElementsInstanced(
gl.c.GL_TRIANGLES,
6,
gl.c.GL_UNSIGNED_BYTE,
cells.items.len,
);
}
/// The OpenGL objects that are associated with a renderer. This makes it
/// easy to create/destroy these as a set in situations i.e. where the
/// OpenGL context is replaced.
const GLState = struct {
cell_program: CellProgram,
texture: gl.Texture,
texture_color: gl.Texture,
custom: ?custom.State,
pub fn init(
alloc: Allocator,
config: DerivedConfig,
font_group: *font.GroupCache,
) !GLState {
var arena = ArenaAllocator.init(alloc);
defer arena.deinit();
const arena_alloc = arena.allocator();
// Load our custom shaders
const custom_state: ?custom.State = custom: {
const shaders: []const [:0]const u8 = shadertoy.loadFromFiles(
arena_alloc,
config.custom_shaders.items,
.glsl,
) catch |err| err: {
log.warn("error loading custom shaders err={}", .{err});
break :err &.{};
};
if (shaders.len == 0) break :custom null;
break :custom custom.State.init(
alloc,
shaders,
) catch |err| err: {
log.warn("error initializing custom shaders err={}", .{err});
break :err null;
};
};
// Blending for text. We use GL_ONE here because we should be using
// premultiplied alpha for all our colors in our fragment shaders.
// This avoids having a blurry border where transparency is expected on
// pixels.
try gl.enable(gl.c.GL_BLEND);
try gl.blendFunc(gl.c.GL_ONE, gl.c.GL_ONE_MINUS_SRC_ALPHA);
// Build our texture
const tex = try gl.Texture.create();
errdefer tex.destroy();
{
const texbind = try tex.bind(.@"2D");
try texbind.parameter(.WrapS, gl.c.GL_CLAMP_TO_EDGE);
try texbind.parameter(.WrapT, gl.c.GL_CLAMP_TO_EDGE);
try texbind.parameter(.MinFilter, gl.c.GL_LINEAR);
try texbind.parameter(.MagFilter, gl.c.GL_LINEAR);
try texbind.image2D(
0,
.red,
@intCast(font_group.atlas_greyscale.size),
@intCast(font_group.atlas_greyscale.size),
0,
.red,
.UnsignedByte,
font_group.atlas_greyscale.data.ptr,
);
}
// Build our color texture
const tex_color = try gl.Texture.create();
errdefer tex_color.destroy();
{
const texbind = try tex_color.bind(.@"2D");
try texbind.parameter(.WrapS, gl.c.GL_CLAMP_TO_EDGE);
try texbind.parameter(.WrapT, gl.c.GL_CLAMP_TO_EDGE);
try texbind.parameter(.MinFilter, gl.c.GL_LINEAR);
try texbind.parameter(.MagFilter, gl.c.GL_LINEAR);
try texbind.image2D(
0,
.rgba,
@intCast(font_group.atlas_color.size),
@intCast(font_group.atlas_color.size),
0,
.bgra,
.UnsignedByte,
font_group.atlas_color.data.ptr,
);
}
// Build our cell renderer
const cell_program = try CellProgram.init();
errdefer cell_program.deinit();
return .{
.cell_program = cell_program,
.texture = tex,
.texture_color = tex_color,
.custom = custom_state,
};
}
pub fn deinit(self: *GLState, alloc: Allocator) void {
if (self.custom) |v| v.deinit(alloc);
self.texture.destroy();
self.texture_color.destroy();
self.cell_program.deinit();
}
};