//! 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 link = @import("link.zig"); const isCovering = @import("cell.zig").isCovering; const fgMode = @import("cell.zig").fgMode; 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 math = @import("../math.zig"); const Surface = @import("../Surface.zig"); const CellProgram = @import("opengl/CellProgram.zig"); const ImageProgram = @import("opengl/ImageProgram.zig"); const gl_image = @import("opengl/image.zig"); const custom = @import("opengl/custom.zig"); const Image = gl_image.Image; const ImageMap = gl_image.ImageMap; const ImagePlacementList = std.ArrayListUnmanaged(gl_image.Placement); 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 font metrics defining our grid. grid_metrics: font.face.Metrics, /// The size of everything. size: renderer.Size, /// 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 last viewport that we based our rebuild off of. If this changes, /// then we do a full rebuild of the cells. The pointer values in this pin /// are NOT SAFE to read because they may be modified, freed, etc from the /// termio thread. We treat the pointers as integers for comparison only. cells_viewport: ?terminal.Pin = null, /// 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_grid: *font.SharedGrid, font_shaper: font.Shaper, font_shaper_cache: font.ShaperCache, texture_grayscale_modified: usize = 0, texture_grayscale_resized: usize = 0, texture_color_modified: usize = 0, texture_color_resized: usize = 0, /// 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, /// When `cursor_color` is null, swap the foreground and background colors of /// the cell under the cursor for the cursor color. Otherwise, use the default /// foreground color as the cursor color. cursor_invert: bool, /// 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, deferred_config: ?SetConfig = 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, /// Whether we're doing padding extension for vertical sides. padding_extend_top: bool = true, padding_extend_bottom: bool = true, /// The images that we may render. images: ImageMap = .{}, image_placements: ImagePlacementList = .{}, image_bg_end: u32 = 0, image_text_end: u32 = 0, image_virtual: bool = false, /// Defererred OpenGL operation to update the screen size. const SetScreenSize = struct { size: renderer.Size, 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 grid_size = self.size.grid(); const terminal_size = self.size.terminal(); // Blank space around the grid. const blank: renderer.Padding = switch (r.config.padding_color) { // We can use zero padding because the background color is our // clear color. .background => .{}, .extend, .@"extend-always" => self.size.screen.blankPadding( self.size.padding, grid_size, self.size.cell, ).add(self.size.padding), }; // 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.screen.width), @intCast(self.size.screen.height), ); // Update the projection uniform within our shader inline for (.{ "cell_program", "image_program" }) |name| { const program = @field(gl_state, name); const bind = try program.program.use(); defer bind.unbind(); try program.program.setUniform( "projection", // 2D orthographic projection with the full w/h math.ortho2d( -1 * @as(f32, @floatFromInt(self.size.padding.left)), @floatFromInt(terminal_size.width + self.size.padding.right), @floatFromInt(terminal_size.height + self.size.padding.bottom), -1 * @as(f32, @floatFromInt(self.size.padding.top)), ), ); } // Setup our grid padding { const program = gl_state.cell_program; const bind = try program.program.use(); defer bind.unbind(); try program.program.setUniform( "grid_padding", @Vector(4, f32){ @floatFromInt(blank.top), @floatFromInt(blank.right), @floatFromInt(blank.bottom), @floatFromInt(blank.left), }, ); try program.program.setUniform( "grid_size", @Vector(2, f32){ @floatFromInt(grid_size.columns), @floatFromInt(grid_size.rows), }, ); } // 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; inline for (.{ "cell_program", "image_program" }) |name| { const program = @field(gl_state, name); const bind = try program.program.use(); defer bind.unbind(); try program.program.setUniform( "cell_size", @Vector(2, f32){ @floatFromInt(self.metrics.cell_width), @floatFromInt(self.metrics.cell_height), }, ); } } }; const SetConfig = struct { fn apply(self: SetConfig, r: *const OpenGL) !void { _ = self; const gl_state = r.gl_state orelse return error.OpenGLUninitialized; const bind = try gl_state.cell_program.program.use(); defer bind.unbind(); try gl_state.cell_program.program.setUniform( "min_contrast", r.config.min_contrast, ); } }; /// 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([:0]const u8), font_styles: font.CodepointResolver.StyleStatus, cursor_color: ?terminal.color.RGB, cursor_invert: bool, 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, bold_is_bright: bool, min_contrast: f32, padding_color: configpkg.WindowPaddingColor, custom_shaders: configpkg.RepeatablePath, links: link.Set, 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".clone(alloc); // Copy our font features const font_features = try config.@"font-feature".clone(alloc); // Get our font styles var font_styles = font.CodepointResolver.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); // Our link configs const links = try link.Set.fromConfig( alloc, config.link.links.items, ); const cursor_invert = config.@"cursor-invert-fg-bg"; return .{ .background_opacity = @max(0, @min(1, config.@"background-opacity")), .font_thicken = config.@"font-thicken", .font_features = font_features.list, .font_styles = font_styles, .cursor_color = if (!cursor_invert and config.@"cursor-color" != null) config.@"cursor-color".?.toTerminalRGB() else null, .cursor_invert = cursor_invert, .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", .bold_is_bright = config.@"bold-is-bright", .min_contrast = @floatCast(config.@"minimum-contrast"), .padding_color = config.@"window-padding-color", .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, .links = links, .arena = arena, }; } pub fn deinit(self: *DerivedConfig) void { const alloc = self.arena.allocator(); self.links.deinit(alloc); 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(); // For the remainder of the setup we lock our font grid data because // we're reading it. const grid = options.font_grid; grid.lock.lockShared(); defer grid.lock.unlockShared(); var gl_state = try GLState.init(alloc, options.config, grid); errdefer gl_state.deinit(); return OpenGL{ .alloc = alloc, .config = options.config, .cells_bg = .{}, .cells = .{}, .grid_metrics = grid.metrics, .size = options.size, .gl_state = gl_state, .font_grid = grid, .font_shaper = shaper, .font_shaper_cache = font.ShaperCache.init(), .draw_background = options.config.background, .focused = true, .foreground_color = options.config.foreground, .background_color = options.config.background, .cursor_color = options.config.cursor_color, .cursor_invert = options.config.cursor_invert, .surface_mailbox = options.surface_mailbox, .deferred_font_size = .{ .metrics = grid.metrics }, .deferred_config = .{}, }; } pub fn deinit(self: *OpenGL) void { self.font_shaper.deinit(); self.font_shaper_cache.deinit(self.alloc); { var it = self.images.iterator(); while (it.next()) |kv| kv.value_ptr.image.deinit(self.alloc); self.images.deinit(self.alloc); } self.image_placements.deinit(self.alloc); 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); const major = gl.glad.versionMajor(@intCast(version)); const minor = gl.glad.versionMinor(@intCast(version)); errdefer gl.glad.unload(); log.info("loaded OpenGL {}.{}", .{ major, minor }); // We require at least OpenGL 3.3 if (major < 3 or (major == 3 and minor < 3)) { log.warn("OpenGL version is too old. Ghostty requires OpenGL 3.3", .{}); return error.OpenGLOutdated; } }, apprt.glfw => try self.threadEnter(surface), 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; // 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(); // Make our new state var gl_state = gl_state: { self.font_grid.lock.lockShared(); defer self.font_grid.lock.unlockShared(); break :gl_state try GLState.init( self.alloc, self.config, self.font_grid, ); }; 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.texture_grayscale_modified = 0; self.texture_color_modified = 0; self.texture_grayscale_resized = 0; self.texture_color_resized = 0; // We need to reset our uniforms self.deferred_screen_size = .{ .size = self.size }; self.deferred_font_size = .{ .metrics = self.grid_metrics }; self.deferred_config = .{}; } /// 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)), }); }, 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.glfw => { gl.glad.unload(); glfw.makeContextCurrent(null); }, apprt.embedded => { // TODO: see threadEnter }, } } /// 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; } /// See Metal pub fn hasVsync(self: *const OpenGL) bool { _ = self; // OpenGL currently never has vsync return false; } /// See Metal. pub fn markDirty(self: *OpenGL) void { // Do nothing, we don't have dirty tracking yet. _ = self; } /// 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; } /// Callback when the window is visible or occluded. /// /// Must be called on the render thread. pub fn setVisible(self: *OpenGL, visible: bool) void { _ = self; _ = visible; } /// Set the new font grid. /// /// Must be called on the render thread. pub fn setFontGrid(self: *OpenGL, grid: *font.SharedGrid) void { if (single_threaded_draw) self.draw_mutex.lock(); defer if (single_threaded_draw) self.draw_mutex.unlock(); // Reset our font grid self.font_grid = grid; self.grid_metrics = grid.metrics; self.texture_grayscale_modified = 0; self.texture_grayscale_resized = 0; self.texture_color_modified = 0; self.texture_color_resized = 0; // Reset our shaper cache. If our font changed (not just the size) then // the data in the shaper cache may be invalid and cannot be used, so we // always clear the cache just in case. const font_shaper_cache = font.ShaperCache.init(); self.font_shaper_cache.deinit(self.alloc); self.font_shaper_cache = font_shaper_cache; // Update our screen size because the font grid can affect grid // metrics which update uniforms. self.deferred_screen_size = .{ .size = self.size }; // Defer our GPU updates self.deferred_font_size = .{ .metrics = grid.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 { full_rebuild: bool, gl_bg: terminal.color.RGB, screen: terminal.Screen, screen_type: terminal.ScreenType, mouse: renderer.State.Mouse, preedit: ?renderer.State.Preedit, cursor_style: ?renderer.CursorStyle, color_palette: terminal.color.Palette, }; // Update all our data as tightly as possible within the mutex. var critical: Critical = critical: { const grid_size = self.size.grid(); 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; } // If our terminal screen size doesn't match our expected renderer // size then we skip a frame. This can happen if the terminal state // is resized between when the renderer mailbox is drained and when // the state mutex is acquired inside this function. // // For some reason this doesn't seem to cause any significant issues // with flickering while resizing. '\_('-')_/' if (grid_size.rows != state.terminal.rows or grid_size.columns != state.terminal.cols) { return; } // Get the viewport pin so that we can compare it to the current. const viewport_pin = state.terminal.screen.pages.pin(.{ .viewport = .{} }).?; // 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. var screen_copy = try state.terminal.screen.clone( self.alloc, .{ .viewport = .{} }, null, ); errdefer screen_copy.deinit(); // Whether to draw our cursor or not. const cursor_style = if (state.terminal.flags.password_input) .lock else renderer.cursorStyle( state, self.focused, cursor_blink_visible, ); // Get our preedit state const preedit: ?renderer.State.Preedit = preedit: { if (cursor_style == null) break :preedit null; const p = state.preedit orelse break :preedit null; break :preedit try p.clone(self.alloc); }; errdefer if (preedit) |p| p.deinit(self.alloc); // If we have Kitty graphics data, we enter a SLOW SLOW SLOW path. // We only do this if the Kitty image state is dirty meaning only if // it changes. // // If we have any virtual references, we must also rebuild our // kitty state on every frame because any cell change can move // an image. if (state.terminal.screen.kitty_images.dirty or self.image_virtual) { // prepKittyGraphics touches self.images which is also used // in drawFrame so if we're drawing on a separate thread we need // to lock this. if (single_threaded_draw) self.draw_mutex.lock(); defer if (single_threaded_draw) self.draw_mutex.unlock(); try self.prepKittyGraphics(state.terminal); } // If we have any terminal dirty flags set then we need to rebuild // the entire screen. This can be optimized in the future. const full_rebuild: bool = rebuild: { { const Int = @typeInfo(terminal.Terminal.Dirty).Struct.backing_integer.?; const v: Int = @bitCast(state.terminal.flags.dirty); if (v > 0) break :rebuild true; } { const Int = @typeInfo(terminal.Screen.Dirty).Struct.backing_integer.?; const v: Int = @bitCast(state.terminal.screen.dirty); if (v > 0) break :rebuild true; } // If our viewport changed then we need to rebuild the entire // screen because it means we scrolled. If we have no previous // viewport then we must rebuild. const prev_viewport = self.cells_viewport orelse break :rebuild true; if (!prev_viewport.eql(viewport_pin)) break :rebuild true; break :rebuild false; }; // Reset the dirty flags in the terminal and screen. We assume // that our rebuild will be successful since so we optimize for // success and reset while we hold the lock. This is much easier // than coordinating row by row or as changes are persisted. state.terminal.flags.dirty = .{}; state.terminal.screen.dirty = .{}; { var it = state.terminal.screen.pages.pageIterator( .right_down, .{ .screen = .{} }, null, ); while (it.next()) |chunk| { var dirty_set = chunk.node.data.dirtyBitSet(); dirty_set.unsetAll(); } } // Update our viewport pin for dirty tracking self.cells_viewport = viewport_pin; break :critical .{ .full_rebuild = full_rebuild, .gl_bg = self.background_color, .screen = screen_copy, .screen_type = state.terminal.active_screen, .mouse = state.mouse, .preedit = preedit, .cursor_style = cursor_style, .color_palette = state.terminal.color_palette.colors, }; }; defer { critical.screen.deinit(); if (critical.preedit) |p| p.deinit(self.alloc); } // 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.full_rebuild, &critical.screen, critical.screen_type, critical.mouse, critical.preedit, critical.cursor_style, &critical.color_palette, ); // Notify our shaper we're done for the frame. For some shapers like // CoreText this triggers off-thread cleanup logic. self.font_shaper.endFrame(); } } /// This goes through the Kitty graphic placements and accumulates the /// placements we need to render on our viewport. It also ensures that /// the visible images are loaded on the GPU. fn prepKittyGraphics( self: *OpenGL, t: *terminal.Terminal, ) !void { const storage = &t.screen.kitty_images; defer storage.dirty = false; // We always clear our previous placements no matter what because // we rebuild them from scratch. self.image_placements.clearRetainingCapacity(); self.image_virtual = false; // Go through our known images and if there are any that are no longer // in use then mark them to be freed. // // This never conflicts with the below because a placement can't // reference an image that doesn't exist. { var it = self.images.iterator(); while (it.next()) |kv| { if (storage.imageById(kv.key_ptr.*) == null) { kv.value_ptr.image.markForUnload(); } } } // The top-left and bottom-right corners of our viewport in screen // points. This lets us determine offsets and containment of placements. const top = t.screen.pages.getTopLeft(.viewport); const bot = t.screen.pages.getBottomRight(.viewport).?; // Go through the placements and ensure the image is loaded on the GPU. var it = storage.placements.iterator(); while (it.next()) |kv| { // Find the image in storage const p = kv.value_ptr; // Special logic based on location switch (p.location) { .pin => {}, .virtual => { // We need to mark virtual placements on our renderer so that // we know to rebuild in more scenarios since cell changes can // now trigger placement changes. self.image_virtual = true; // We also continue out because virtual placements are // only triggered by the unicode placeholder, not by the // placement itself. continue; }, } const image = storage.imageById(kv.key_ptr.image_id) orelse { log.warn( "missing image for placement, ignoring image_id={}", .{kv.key_ptr.image_id}, ); continue; }; try self.prepKittyPlacement(t, &top, &bot, &image, p); } // If we have virtual placements then we need to scan for placeholders. if (self.image_virtual) { var v_it = terminal.kitty.graphics.unicode.placementIterator(top, bot); while (v_it.next()) |virtual_p| try self.prepKittyVirtualPlacement( t, &virtual_p, ); } // Sort the placements by their Z value. std.mem.sortUnstable( gl_image.Placement, self.image_placements.items, {}, struct { fn lessThan( ctx: void, lhs: gl_image.Placement, rhs: gl_image.Placement, ) bool { _ = ctx; return lhs.z < rhs.z or (lhs.z == rhs.z and lhs.image_id < rhs.image_id); } }.lessThan, ); // Find our indices self.image_bg_end = 0; self.image_text_end = 0; const bg_limit = std.math.minInt(i32) / 2; for (self.image_placements.items, 0..) |p, i| { if (self.image_bg_end == 0 and p.z >= bg_limit) { self.image_bg_end = @intCast(i); } if (self.image_text_end == 0 and p.z >= 0) { self.image_text_end = @intCast(i); } } if (self.image_text_end == 0) { self.image_text_end = @intCast(self.image_placements.items.len); } } fn prepKittyVirtualPlacement( self: *OpenGL, t: *terminal.Terminal, p: *const terminal.kitty.graphics.unicode.Placement, ) !void { const storage = &t.screen.kitty_images; const image = storage.imageById(p.image_id) orelse { log.warn( "missing image for virtual placement, ignoring image_id={}", .{p.image_id}, ); return; }; const rp = p.renderPlacement( storage, &image, self.grid_metrics.cell_width, self.grid_metrics.cell_height, ) catch |err| { log.warn("error rendering virtual placement err={}", .{err}); return; }; // If our placement is zero sized then we don't do anything. if (rp.dest_width == 0 or rp.dest_height == 0) return; const viewport: terminal.point.Point = t.screen.pages.pointFromPin( .viewport, rp.top_left, ) orelse { // This is unreachable with virtual placements because we should // only ever be looking at virtual placements that are in our // viewport in the renderer and virtual placements only ever take // up one row. unreachable; }; // Send our image to the GPU and store the placement for rendering. try self.prepKittyImage(&image); try self.image_placements.append(self.alloc, .{ .image_id = image.id, .x = @intCast(rp.top_left.x), .y = @intCast(viewport.viewport.y), .z = -1, .width = rp.dest_width, .height = rp.dest_height, .cell_offset_x = rp.offset_x, .cell_offset_y = rp.offset_y, .source_x = rp.source_x, .source_y = rp.source_y, .source_width = rp.source_width, .source_height = rp.source_height, }); } fn prepKittyPlacement( self: *OpenGL, t: *terminal.Terminal, top: *const terminal.Pin, bot: *const terminal.Pin, image: *const terminal.kitty.graphics.Image, p: *const terminal.kitty.graphics.ImageStorage.Placement, ) !void { // Get the rect for the placement. If this placement doesn't have // a rect then its virtual or something so skip it. const rect = p.rect(image.*, t) orelse return; // If the selection isn't within our viewport then skip it. if (bot.before(rect.top_left)) return; if (rect.bottom_right.before(top.*)) return; // If the top left is outside the viewport we need to calc an offset // so that we render (0, 0) with some offset for the texture. const offset_y: u32 = if (rect.top_left.before(top.*)) offset_y: { const vp_y = t.screen.pages.pointFromPin(.screen, top.*).?.screen.y; const img_y = t.screen.pages.pointFromPin(.screen, rect.top_left).?.screen.y; const offset_cells = vp_y - img_y; const offset_pixels = offset_cells * self.grid_metrics.cell_height; break :offset_y @intCast(offset_pixels); } else 0; // We need to prep this image for upload if it isn't in the cache OR // it is in the cache but the transmit time doesn't match meaning this // image is different. try self.prepKittyImage(image); // Convert our screen point to a viewport point const viewport: terminal.point.Point = t.screen.pages.pointFromPin( .viewport, rect.top_left, ) orelse .{ .viewport = .{} }; // Calculate the source rectangle const source_x = @min(image.width, p.source_x); const source_y = @min(image.height, p.source_y + offset_y); const source_width = if (p.source_width > 0) @min(image.width - source_x, p.source_width) else image.width; const source_height = if (p.source_height > 0) @min(image.height, p.source_height) else image.height -| source_y; // Calculate the width/height of our image. const dest_width = if (p.columns > 0) p.columns * self.grid_metrics.cell_width else source_width; const dest_height = if (p.rows > 0) p.rows * self.grid_metrics.cell_height else source_height; // Accumulate the placement if (image.width > 0 and image.height > 0) { try self.image_placements.append(self.alloc, .{ .image_id = image.id, .x = @intCast(rect.top_left.x), .y = @intCast(viewport.viewport.y), .z = p.z, .width = dest_width, .height = dest_height, .cell_offset_x = p.x_offset, .cell_offset_y = p.y_offset, .source_x = source_x, .source_y = source_y, .source_width = source_width, .source_height = source_height, }); } } fn prepKittyImage( self: *OpenGL, image: *const terminal.kitty.graphics.Image, ) !void { // We need to prep this image for upload if it isn't in the cache OR // it is in the cache but the transmit time doesn't match meaning this // image is different. const gop = try self.images.getOrPut(self.alloc, image.id); if (gop.found_existing and gop.value_ptr.transmit_time.order(image.transmit_time) == .eq) { return; } // Copy the data into the pending state. const data = try self.alloc.dupe(u8, image.data); errdefer self.alloc.free(data); // Store it in the map const pending: Image.Pending = .{ .width = image.width, .height = image.height, .data = data.ptr, }; const new_image: Image = switch (image.format) { .gray => .{ .pending_gray = pending }, .gray_alpha => .{ .pending_gray_alpha = pending }, .rgb => .{ .pending_rgb = pending }, .rgba => .{ .pending_rgba = pending }, .png => unreachable, // should be decoded by now }; if (!gop.found_existing) { gop.value_ptr.* = .{ .image = new_image, .transmit_time = undefined, }; } else { try gop.value_ptr.image.markForReplace( self.alloc, new_image, ); } gop.value_ptr.transmit_time = image.transmit_time; } /// 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, rebuild: bool, screen: *terminal.Screen, screen_type: terminal.ScreenType, mouse: renderer.State.Mouse, preedit: ?renderer.State.Preedit, cursor_style_: ?renderer.CursorStyle, color_palette: *const terminal.color.Palette, ) !void { _ = screen_type; // Bg cells at most will need space for the visible screen size self.cells_bg.clearRetainingCapacity(); self.cells.clearRetainingCapacity(); // Create an arena for all our temporary allocations while rebuilding var arena = ArenaAllocator.init(self.alloc); defer arena.deinit(); const arena_alloc = arena.allocator(); // We've written no data to the GPU, refresh it all self.gl_cells_written = 0; // Create our match set for the links. var link_match_set: link.MatchSet = if (mouse.point) |mouse_pt| try self.config.links.matchSet( arena_alloc, screen, mouse_pt, mouse.mods, ) else .{}; // 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: terminal.size.CellCountInt, x: [2]terminal.size.CellCountInt, cp_offset: usize, } = if (preedit) |preedit_v| preedit: { const range = preedit_v.range(screen.cursor.x, screen.pages.cols - 1); break :preedit .{ .y = screen.cursor.y, .x = .{ range.start, range.end }, .cp_offset = range.cp_offset, }; } else null; // These are all the foreground cells underneath the cursor. // // We keep track of these so that we can invert the colors and move them // in front of the block cursor so that the character remains visible. // // We init with a capacity of 4 to account for decorations such // as underline and strikethrough, as well as combining chars. var cursor_cells = try std.ArrayListUnmanaged(CellProgram.Cell).initCapacity(arena_alloc, 4); defer cursor_cells.deinit(arena_alloc); if (rebuild) { switch (self.config.padding_color) { .background => {}, .extend, .@"extend-always" => { self.padding_extend_top = true; self.padding_extend_bottom = true; }, } } // Build each cell var row_it = screen.pages.rowIterator(.left_up, .{ .viewport = .{} }, null); var y: terminal.size.CellCountInt = screen.pages.rows; while (row_it.next()) |row| { y -= 1; // 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 (shape_cursor and cursor_style_ == .block) { const x = screen.cursor.x; const wide = row.cells(.all)[x].wide; const min_x = switch (wide) { .narrow, .spacer_head, .wide => x, .spacer_tail => x -| 1, }; const max_x = switch (wide) { .narrow, .spacer_head, .spacer_tail => x, .wide => x +| 1, }; for (self.cells.items[start_i..]) |cell| { if (cell.grid_col < min_x or cell.grid_col > max_x) continue; if (cell.mode.isFg()) { cursor_cells.append(arena_alloc, cell) catch { // We silently ignore if this fails because // worst case scenario some combining glyphs // aren't visible under the cursor '\_('-')_/' }; } } }; // We need to get this row's selection if there is one for proper // run splitting. const row_selection = sel: { const sel = screen.selection orelse break :sel null; const pin = screen.pages.pin(.{ .viewport = .{ .y = y } }) orelse break :sel null; break :sel sel.containedRow(screen, pin) orelse null; }; // On primary screen, we still apply vertical padding extension // under certain conditions we feel are safe. This helps make some // scenarios look better while avoiding scenarios we know do NOT look // good. switch (self.config.padding_color) { // These already have the correct values set above. .background, .@"extend-always" => {}, // Apply heuristics for padding extension. .extend => if (y == 0) { self.padding_extend_top = !row.neverExtendBg( color_palette, self.background_color, ); } else if (y == self.size.grid().rows - 1) { self.padding_extend_bottom = !row.neverExtendBg( color_palette, self.background_color, ); }, } // Iterator of runs for shaping. var run_iter = self.font_shaper.runIterator( self.font_grid, screen, row, row_selection, if (shape_cursor) screen.cursor.x else null, ); var shaper_run: ?font.shape.TextRun = try run_iter.next(self.alloc); var shaper_cells: ?[]const font.shape.Cell = null; var shaper_cells_i: usize = 0; const row_cells = row.cells(.all); for (row_cells, 0..) |*cell, x| { // If this cell falls within our preedit range then we // skip this because preedits are setup separately. if (preedit_range) |range| preedit: { // We're not on the preedit line, no actions necessary. if (range.y != y) break :preedit; // We're before the preedit range, no actions necessary. if (x < range.x[0]) break :preedit; // We're in the preedit range, skip this cell. if (x <= range.x[1]) continue; // After exiting the preedit range we need to catch // the run position up because of the missed cells. // In all other cases, no action is necessary. if (x != range.x[1] + 1) break :preedit; // Step the run iterator until we find a run that ends // after the current cell, which will be the soonest run // that might contain glyphs for our cell. while (shaper_run) |run| { if (run.offset + run.cells > x) break; shaper_run = try run_iter.next(self.alloc); shaper_cells = null; shaper_cells_i = 0; } const run = shaper_run orelse break :preedit; // If we haven't shaped this run, do so now. shaper_cells = shaper_cells orelse // Try to read the cells from the shaping cache if we can. self.font_shaper_cache.get(run) orelse cache: { // Otherwise we have to shape them. const cells = try self.font_shaper.shape(run); // Try to cache them. If caching fails for any reason we // continue because it is just a performance optimization, // not a correctness issue. self.font_shaper_cache.put( self.alloc, run, cells, ) catch |err| { log.warn( "error caching font shaping results err={}", .{err}, ); }; // The cells we get from direct shaping are always owned // by the shaper and valid until the next shaping call so // we can safely use them. break :cache cells; }; // Advance our index until we reach or pass // our current x position in the shaper cells. while (shaper_cells.?[shaper_cells_i].x < x) { shaper_cells_i += 1; } } const wide = cell.wide; const style = row.style(cell); const cell_pin: terminal.Pin = cell: { var copy = row; copy.x = @intCast(x); break :cell copy; }; // True if this cell is selected const selected: bool = if (screen.selection) |sel| sel.contains(screen, .{ .node = row.node, .y = row.y, .x = @intCast( // Spacer tails should show the selection // state of the wide cell they belong to. if (wide == .spacer_tail) x -| 1 else x, ), }) else false; const bg_style = style.bg(cell, color_palette); const fg_style = style.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color; // The final background color for the cell. const bg = bg: { if (selected) { break :bg if (self.config.invert_selection_fg_bg) if (style.flags.inverse) // Cell is selected with invert selection fg/bg // enabled, and the cell has the inverse style // flag, so they cancel out and we get the normal // bg color. bg_style else // If it doesn't have the inverse style // flag then we use the fg color instead. fg_style else // If we don't have invert selection fg/bg set then we // just use the selection background if set, otherwise // the default fg color. break :bg self.config.selection_background orelse self.foreground_color; } // Not selected break :bg if (style.flags.inverse != isCovering(cell.codepoint())) // Two cases cause us to invert (use the fg color as the bg) // - The "inverse" style flag. // - A "covering" glyph; we use fg for bg in that case to // help make sure that padding extension works correctly. // If one of these is true (but not the other) // then we use the fg style color for the bg. fg_style else // Otherwise they cancel out. bg_style; }; const fg = fg: { if (selected and !self.config.invert_selection_fg_bg) { // If we don't have invert selection fg/bg set // then we just use the selection foreground if // set, otherwise the default bg color. break :fg self.config.selection_foreground orelse self.background_color; } // Whether we need to use the bg color as our fg color: // - Cell is inverted and not selected // - Cell is selected and not inverted // Note: if selected then invert sel fg / bg must be // false since we separately handle it if true above. break :fg if (style.flags.inverse != selected) bg_style orelse self.background_color else fg_style; }; // Foreground alpha for this cell. const alpha: u8 = if (style.flags.faint) 175 else 255; // If the cell has a background color, set it. const bg_color: [4]u8 = if (bg) |rgb| bg: { // 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 (style.flags.inverse) break :bg_alpha default; // If we have a background and its not the default background // then we apply background opacity if (style.bg(cell, color_palette) != null and !rgb.eql(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); }; try self.cells_bg.append(self.alloc, .{ .mode = .bg, .grid_col = @intCast(x), .grid_row = @intCast(y), .grid_width = cell.gridWidth(), .glyph_x = 0, .glyph_y = 0, .glyph_width = 0, .glyph_height = 0, .glyph_offset_x = 0, .glyph_offset_y = 0, .r = rgb.r, .g = rgb.g, .b = rgb.b, .a = bg_alpha, .bg_r = 0, .bg_g = 0, .bg_b = 0, .bg_a = 0, }); break :bg .{ rgb.r, rgb.g, rgb.b, bg_alpha, }; } else .{ self.draw_background.r, self.draw_background.g, self.draw_background.b, @intFromFloat(@max(0, @min(255, @round(self.config.background_opacity * 255)))), }; // If the invisible flag is set on this cell then we // don't need to render any foreground elements, so // we just skip all glyphs with this x coordinate. // // NOTE: This behavior matches xterm. Some other terminal // emulators, e.g. Alacritty, still render text decorations // and only make the text itself invisible. The decision // has been made here to match xterm's behavior for this. if (style.flags.invisible) { continue; } // Give links a single underline, unless they already have // an underline, in which case use a double underline to // distinguish them. const underline: terminal.Attribute.Underline = if (link_match_set.contains(screen, cell_pin)) if (style.flags.underline == .single) .double else .single else style.flags.underline; // We draw underlines first so that they layer underneath text. // This improves readability when a colored underline is used // which intersects parts of the text (descenders). if (underline != .none) self.addUnderline( @intCast(x), @intCast(y), underline, style.underlineColor(color_palette) orelse fg, alpha, bg_color, ) catch |err| { log.warn( "error adding underline to cell, will be invalid x={} y={}, err={}", .{ x, y, err }, ); }; if (style.flags.overline) self.addOverline( @intCast(x), @intCast(y), fg, alpha, bg_color, ) catch |err| { log.warn( "error adding overline to cell, will be invalid x={} y={}, err={}", .{ x, y, err }, ); }; // If we're at or past the end of our shaper run then // we need to get the next run from the run iterator. if (shaper_cells != null and shaper_cells_i >= shaper_cells.?.len) { shaper_run = try run_iter.next(self.alloc); shaper_cells = null; shaper_cells_i = 0; } if (shaper_run) |run| glyphs: { // If we haven't shaped this run yet, do so. shaper_cells = shaper_cells orelse // Try to read the cells from the shaping cache if we can. self.font_shaper_cache.get(run) orelse cache: { // Otherwise we have to shape them. const cells = try self.font_shaper.shape(run); // Try to cache them. If caching fails for any reason we // continue because it is just a performance optimization, // not a correctness issue. self.font_shaper_cache.put( self.alloc, run, cells, ) catch |err| { log.warn( "error caching font shaping results err={}", .{err}, ); }; // The cells we get from direct shaping are always owned // by the shaper and valid until the next shaping call so // we can safely use them. break :cache cells; }; const cells = shaper_cells orelse break :glyphs; // If there are no shaper cells for this run, ignore it. // This can occur for runs of empty cells, and is fine. if (cells.len == 0) break :glyphs; // If we encounter a shaper cell to the left of the current // cell then we have some problems. This logic relies on x // position monotonically increasing. assert(cells[shaper_cells_i].x >= x); // NOTE: An assumption is made here that a single cell will never // be present in more than one shaper run. If that assumption is // violated, this logic breaks. while (shaper_cells_i < cells.len and cells[shaper_cells_i].x == x) : ({ shaper_cells_i += 1; }) { self.addGlyph( @intCast(x), @intCast(y), cell_pin, cells[shaper_cells_i], shaper_run.?, fg, alpha, bg_color, ) catch |err| { log.warn( "error adding glyph to cell, will be invalid x={} y={}, err={}", .{ x, y, err }, ); }; } } // Finally, draw a strikethrough if necessary. if (style.flags.strikethrough) self.addStrikethrough( @intCast(x), @intCast(y), fg, alpha, bg_color, ) catch |err| { log.warn( "error adding strikethrough to cell, will be invalid x={} y={}, err={}", .{ x, y, err }, ); }; } } // 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[range.cp_offset..]) |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; } const cursor_color = self.cursor_color orelse color: { if (self.cursor_invert) { const sty = screen.cursor.page_pin.style(screen.cursor.page_cell); break :color sty.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color; } else { break :color self.foreground_color; } }; _ = try self.addCursor(screen, cursor_style, cursor_color); for (cursor_cells.items) |*cell| { if (cell.mode.isFg() and cell.mode != .fg_color) { const cell_color = if (self.cursor_invert) blk: { const sty = screen.cursor.page_pin.style(screen.cursor.page_cell); break :blk sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color; } else if (self.config.cursor_text) |txt| txt else self.background_color; cell.r = cell_color.r; cell.g = cell_color.g; cell.b = cell_color.b; cell.a = 255; } try self.cells.append(self.alloc, 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; // Render the glyph for our preedit text const render_ = self.font_grid.renderCodepoint( self.alloc, @intCast(cp.codepoint), .regular, .text, .{ .grid_metrics = self.grid_metrics }, ) catch |err| { log.warn("error rendering preedit glyph err={}", .{err}); return; }; const render = render_ orelse { log.warn("failed to find font for preedit codepoint={X}", .{cp.codepoint}); return; }; // Add our opaque background cell try self.cells_bg.append(self.alloc, .{ .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, .r = bg.r, .g = bg.g, .b = bg.b, .a = 255, .bg_r = 0, .bg_g = 0, .bg_b = 0, .bg_a = 0, }); // Add our text try self.cells.append(self.alloc, .{ .mode = .fg, .grid_col = @intCast(x), .grid_row = @intCast(y), .grid_width = if (cp.wide) 2 else 1, .glyph_x = render.glyph.atlas_x, .glyph_y = render.glyph.atlas_y, .glyph_width = render.glyph.width, .glyph_height = render.glyph.height, .glyph_offset_x = render.glyph.offset_x, .glyph_offset_y = render.glyph.offset_y, .r = fg.r, .g = fg.g, .b = fg.b, .a = 255, .bg_r = bg.r, .bg_g = bg.g, .bg_b = bg.b, .bg_a = 255, }); } fn addCursor( self: *OpenGL, screen: *terminal.Screen, cursor_style: renderer.CursorStyle, cursor_color: terminal.color.RGB, ) !?*const CellProgram.Cell { // Add the cursor. We render the cursor over the wide character if // we're on the wide character tail. const wide, const x = cell: { // The cursor goes over the screen cursor position. const cell = screen.cursor.page_cell; if (cell.wide != .spacer_tail or screen.cursor.x == 0) break :cell .{ cell.wide == .wide, screen.cursor.x }; // If we're part of a wide character, we move the cursor back to // the actual character. const prev_cell = screen.cursorCellLeft(1); break :cell .{ prev_cell.wide == .wide, screen.cursor.x - 1 }; }; const alpha: u8 = if (!self.focused) 255 else alpha: { const alpha = 255 * self.config.cursor_opacity; break :alpha @intFromFloat(@ceil(alpha)); }; const render = switch (cursor_style) { .block, .block_hollow, .bar, .underline, => render: { const sprite: font.Sprite = switch (cursor_style) { .block => .cursor_rect, .block_hollow => .cursor_hollow_rect, .bar => .cursor_bar, .underline => .underline, .lock => unreachable, }; break :render self.font_grid.renderGlyph( self.alloc, font.sprite_index, @intFromEnum(sprite), .{ .cell_width = if (wide) 2 else 1, .grid_metrics = self.grid_metrics, }, ) catch |err| { log.warn("error rendering cursor glyph err={}", .{err}); return null; }; }, .lock => self.font_grid.renderCodepoint( self.alloc, 0xF023, // lock symbol .regular, .text, .{ .cell_width = if (wide) 2 else 1, .grid_metrics = self.grid_metrics, }, ) catch |err| { log.warn("error rendering cursor glyph err={}", .{err}); return null; } orelse { // This should never happen because we embed nerd // fonts so we just log and return instead of fallback. log.warn("failed to find lock symbol for cursor codepoint=0xF023", .{}); return null; }, }; try self.cells.append(self.alloc, .{ .mode = .fg, .grid_col = @intCast(x), .grid_row = @intCast(screen.cursor.y), .grid_width = if (wide) 2 else 1, .r = cursor_color.r, .g = cursor_color.g, .b = cursor_color.b, .a = alpha, .bg_r = 0, .bg_g = 0, .bg_b = 0, .bg_a = 0, .glyph_x = render.glyph.atlas_x, .glyph_y = render.glyph.atlas_y, .glyph_width = render.glyph.width, .glyph_height = render.glyph.height, .glyph_offset_x = render.glyph.offset_x, .glyph_offset_y = render.glyph.offset_y, }); return &self.cells.items[self.cells.items.len - 1]; } /// Add an underline decoration to the specified cell fn addUnderline( self: *OpenGL, x: terminal.size.CellCountInt, y: terminal.size.CellCountInt, style: terminal.Attribute.Underline, color: terminal.color.RGB, alpha: u8, bg: [4]u8, ) !void { const sprite: font.Sprite = switch (style) { .none => unreachable, .single => .underline, .double => .underline_double, .dotted => .underline_dotted, .dashed => .underline_dashed, .curly => .underline_curly, }; const render = try self.font_grid.renderGlyph( self.alloc, font.sprite_index, @intFromEnum(sprite), .{ .cell_width = 1, .grid_metrics = self.grid_metrics, }, ); try self.cells.append(self.alloc, .{ .mode = .fg, .grid_col = @intCast(x), .grid_row = @intCast(y), .grid_width = 1, .glyph_x = render.glyph.atlas_x, .glyph_y = render.glyph.atlas_y, .glyph_width = render.glyph.width, .glyph_height = render.glyph.height, .glyph_offset_x = render.glyph.offset_x, .glyph_offset_y = render.glyph.offset_y, .r = color.r, .g = color.g, .b = color.b, .a = alpha, .bg_r = bg[0], .bg_g = bg[1], .bg_b = bg[2], .bg_a = bg[3], }); } /// Add an overline decoration to the specified cell fn addOverline( self: *OpenGL, x: terminal.size.CellCountInt, y: terminal.size.CellCountInt, color: terminal.color.RGB, alpha: u8, bg: [4]u8, ) !void { const render = try self.font_grid.renderGlyph( self.alloc, font.sprite_index, @intFromEnum(font.Sprite.overline), .{ .cell_width = 1, .grid_metrics = self.grid_metrics, }, ); try self.cells.append(self.alloc, .{ .mode = .fg, .grid_col = @intCast(x), .grid_row = @intCast(y), .grid_width = 1, .glyph_x = render.glyph.atlas_x, .glyph_y = render.glyph.atlas_y, .glyph_width = render.glyph.width, .glyph_height = render.glyph.height, .glyph_offset_x = render.glyph.offset_x, .glyph_offset_y = render.glyph.offset_y, .r = color.r, .g = color.g, .b = color.b, .a = alpha, .bg_r = bg[0], .bg_g = bg[1], .bg_b = bg[2], .bg_a = bg[3], }); } /// Add a strikethrough decoration to the specified cell fn addStrikethrough( self: *OpenGL, x: terminal.size.CellCountInt, y: terminal.size.CellCountInt, color: terminal.color.RGB, alpha: u8, bg: [4]u8, ) !void { const render = try self.font_grid.renderGlyph( self.alloc, font.sprite_index, @intFromEnum(font.Sprite.strikethrough), .{ .cell_width = 1, .grid_metrics = self.grid_metrics, }, ); try self.cells.append(self.alloc, .{ .mode = .fg, .grid_col = @intCast(x), .grid_row = @intCast(y), .grid_width = 1, .glyph_x = render.glyph.atlas_x, .glyph_y = render.glyph.atlas_y, .glyph_width = render.glyph.width, .glyph_height = render.glyph.height, .glyph_offset_x = render.glyph.offset_x, .glyph_offset_y = render.glyph.offset_y, .r = color.r, .g = color.g, .b = color.b, .a = alpha, .bg_r = bg[0], .bg_g = bg[1], .bg_b = bg[2], .bg_a = bg[3], }); } // Add a glyph to the specified cell. fn addGlyph( self: *OpenGL, x: terminal.size.CellCountInt, y: terminal.size.CellCountInt, cell_pin: terminal.Pin, shaper_cell: font.shape.Cell, shaper_run: font.shape.TextRun, color: terminal.color.RGB, alpha: u8, bg: [4]u8, ) !void { const rac = cell_pin.rowAndCell(); const cell = rac.cell; // Render const render = try self.font_grid.renderGlyph( self.alloc, shaper_run.font_index, shaper_cell.glyph_index, .{ .grid_metrics = self.grid_metrics, .thicken = self.config.font_thicken, }, ); // If the glyph is 0 width or height, it will be invisible // when drawn, so don't bother adding it to the buffer. if (render.glyph.width == 0 or render.glyph.height == 0) { return; } // If we're rendering a color font, we use the color atlas const mode: CellProgram.CellMode = switch (try fgMode( render.presentation, cell_pin, )) { .normal => .fg, .color => .fg_color, .constrained => .fg_constrained, .powerline => .fg_powerline, }; try self.cells.append(self.alloc, .{ .mode = mode, .grid_col = @intCast(x), .grid_row = @intCast(y), .grid_width = cell.gridWidth(), .glyph_x = render.glyph.atlas_x, .glyph_y = render.glyph.atlas_y, .glyph_width = render.glyph.width, .glyph_height = render.glyph.height, .glyph_offset_x = render.glyph.offset_x + shaper_cell.x_offset, .glyph_offset_y = render.glyph.offset_y + shaper_cell.y_offset, .r = color.r, .g = color.g, .b = color.b, .a = alpha, .bg_r = bg[0], .bg_g = bg[1], .bg_b = bg[2], .bg_a = bg[3], }); } /// Update the configuration. pub fn changeConfig(self: *OpenGL, config: *DerivedConfig) !void { // 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; } // We also need to reset the shaper cache so shaper info // from the previous font isn't re-used for the new font. const font_shaper_cache = font.ShaperCache.init(); self.font_shaper_cache.deinit(self.alloc); self.font_shaper_cache = font_shaper_cache; // Set our new colors self.background_color = config.background; self.foreground_color = config.foreground; self.cursor_invert = config.cursor_invert; self.cursor_color = if (!config.cursor_invert) config.cursor_color else null; // Update our uniforms self.deferred_config = .{}; 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, size: renderer.Size, ) !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.size = size; // Defer our OpenGL updates self.deferred_screen_size = .{ .size = size }; log.debug("screen size size={}", .{size}); } /// Updates the font texture atlas if it is dirty. fn flushAtlas(self: *OpenGL) !void { const gl_state = self.gl_state orelse return; try flushAtlasSingle( &self.font_grid.lock, gl_state.texture, &self.font_grid.atlas_grayscale, &self.texture_grayscale_modified, &self.texture_grayscale_resized, .red, .red, ); try flushAtlasSingle( &self.font_grid.lock, gl_state.texture_color, &self.font_grid.atlas_color, &self.texture_color_modified, &self.texture_color_resized, .rgba, .bgra, ); } /// Flush a single atlas, grabbing all necessary locks, checking for /// changes, etc. fn flushAtlasSingle( lock: *std.Thread.RwLock, texture: gl.Texture, atlas: *font.Atlas, modified: *usize, resized: *usize, internal_format: gl.Texture.InternalFormat, format: gl.Texture.Format, ) !void { // If the texture isn't modified we do nothing const new_modified = atlas.modified.load(.monotonic); if (new_modified <= modified.*) return; // If it is modified we need to grab a read-lock lock.lockShared(); defer lock.unlockShared(); var texbind = try texture.bind(.@"2D"); defer texbind.unbind(); const new_resized = atlas.resized.load(.monotonic); if (new_resized > resized.*) { try texbind.image2D( 0, internal_format, @intCast(atlas.size), @intCast(atlas.size), 0, format, .UnsignedByte, atlas.data.ptr, ); // Only update the resized number after successful resize resized.* = new_resized; } else { try texbind.subImage2D( 0, 0, 0, @intCast(atlas.size), @intCast(atlas.size), format, .UnsignedByte, atlas.data.ptr, ); } // Update our modified tracker after successful update modified.* = atlas.modified.load(.monotonic); } /// Render renders the current cell state. This will not modify any of /// the cells. pub fn drawFrame(self: *OpenGL, surface: *apprt.Surface) !void { // 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; // Go through our images and see if we need to setup any textures. { var image_it = self.images.iterator(); while (image_it.next()) |kv| { switch (kv.value_ptr.image) { .ready => {}, .pending_gray, .pending_gray_alpha, .pending_rgb, .pending_rgba, .replace_gray, .replace_gray_alpha, .replace_rgb, .replace_rgba, => try kv.value_ptr.image.upload(self.alloc), .unload_pending, .unload_replace, .unload_ready, => { kv.value_ptr.image.deinit(self.alloc); self.images.removeByPtr(kv.key_ptr); }, } } } // In the "OpenGL Programming Guide for Mac" it explains that: "When you // use an NSOpenGLView object with OpenGL calls that are issued from a // thread other than the main one, you must set up mutex locking." // This locks the context and avoids crashes that can happen due to // races with the underlying Metal layer that Apple is using to // implement OpenGL. const is_darwin = builtin.target.isDarwin(); const ogl = if (comptime is_darwin) @cImport({ @cInclude("OpenGL/OpenGL.h"); }) else {}; const cgl_ctx = if (comptime is_darwin) ogl.CGLGetCurrentContext(); if (comptime is_darwin) _ = ogl.CGLLockContext(cgl_ctx); defer _ = if (comptime is_darwin) ogl.CGLUnlockContext(cgl_ctx); // 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 => {}, apprt.embedded => {}, else => @compileError("unsupported runtime"), } } /// Draw the custom shaders. 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 bind.draw(); } } /// 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); // 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; } if (self.deferred_config) |v| { try v.apply(self); self.deferred_config = null; } // Apply our padding extension fields { const program = gl_state.cell_program; const bind = try program.program.use(); defer bind.unbind(); try program.program.setUniform( "padding_vertical_top", self.padding_extend_top, ); try program.program.setUniform( "padding_vertical_bottom", self.padding_extend_bottom, ); } // Draw background images first try self.drawImages( gl_state, self.image_placements.items[0..self.image_bg_end], ); // Draw our background try self.drawCells(gl_state, self.cells_bg); // Then draw images under text try self.drawImages( gl_state, self.image_placements.items[self.image_bg_end..self.image_text_end], ); // Drag foreground try self.drawCells(gl_state, self.cells); // Draw remaining images try self.drawImages( gl_state, self.image_placements.items[self.image_text_end..], ); } /// Runs the image program to draw images. fn drawImages( self: *OpenGL, gl_state: *const GLState, placements: []const gl_image.Placement, ) !void { if (placements.len == 0) return; // Bind our image program const bind = try gl_state.image_program.bind(); defer bind.unbind(); // For each placement we need to bind the texture for (placements) |p| { // Get the image and image texture const image = self.images.get(p.image_id) orelse { log.warn("image not found for placement image_id={}", .{p.image_id}); continue; }; const texture = switch (image.image) { .ready => |t| t, else => { log.warn("image not ready for placement image_id={}", .{p.image_id}); continue; }, }; // Bind the texture try gl.Texture.active(gl.c.GL_TEXTURE0); var texbind = try texture.bind(.@"2D"); defer texbind.unbind(); // Setup our data try bind.vbo.setData(ImageProgram.Input{ .grid_col = @intCast(p.x), .grid_row = @intCast(p.y), .cell_offset_x = p.cell_offset_x, .cell_offset_y = p.cell_offset_y, .source_x = p.source_x, .source_y = p.source_y, .source_width = p.source_width, .source_height = p.source_height, .dest_width = p.width, .dest_height = p.height, }, .static_draw); try gl.drawElementsInstanced( gl.c.GL_TRIANGLES, 6, gl.c.GL_UNSIGNED_BYTE, 1, ); } } /// 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, gl_state: *const GLState, 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; // 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(); // 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 bind.vbo.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 bind.vbo.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, image_program: ImageProgram, texture: gl.Texture, texture_color: gl.Texture, custom: ?custom.State, pub fn init( alloc: Allocator, config: DerivedConfig, font_grid: *font.SharedGrid, ) !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, .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_grid.atlas_grayscale.size), @intCast(font_grid.atlas_grayscale.size), 0, .red, .UnsignedByte, font_grid.atlas_grayscale.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_grid.atlas_color.size), @intCast(font_grid.atlas_color.size), 0, .bgra, .UnsignedByte, font_grid.atlas_color.data.ptr, ); } // Build our cell renderer const cell_program = try CellProgram.init(); errdefer cell_program.deinit(); // Build our image renderer const image_program = try ImageProgram.init(); errdefer image_program.deinit(); return .{ .cell_program = cell_program, .image_program = image_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.image_program.deinit(); self.cell_program.deinit(); } };