aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/renderer/generic.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-04 19:22:04 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-04-05 01:23:06 +0300
commit41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch)
tree3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/renderer/generic.zig
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/renderer/generic.zig')
-rw-r--r--vendor/libghostty-vt/src/renderer/generic.zig3374
1 files changed, 3374 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/renderer/generic.zig b/vendor/libghostty-vt/src/renderer/generic.zig
new file mode 100644
index 00000000..0f4a294b
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/generic.zig
@@ -0,0 +1,3374 @@
+const std = @import("std");
+const builtin = @import("builtin");
+const xev = @import("xev");
+const wuffs = @import("wuffs");
+const apprt = @import("../apprt.zig");
+const configpkg = @import("../config.zig");
+const font = @import("../font/main.zig");
+const inputpkg = @import("../input.zig");
+const os = @import("../os/main.zig");
+const terminal = @import("../terminal/main.zig");
+const renderer = @import("../renderer.zig");
+const math = @import("../math.zig");
+const Surface = @import("../Surface.zig");
+const link = @import("link.zig");
+const cellpkg = @import("cell.zig");
+const noMinContrast = cellpkg.noMinContrast;
+const constraintWidth = cellpkg.constraintWidth;
+const isCovering = cellpkg.isCovering;
+const rowNeverExtendBg = @import("row.zig").neverExtendBg;
+const Overlay = @import("Overlay.zig");
+const imagepkg = @import("image.zig");
+const ImageState = imagepkg.State;
+const shadertoy = @import("shadertoy.zig");
+const assert = @import("../quirks.zig").inlineAssert;
+const Allocator = std.mem.Allocator;
+const ArenaAllocator = std.heap.ArenaAllocator;
+const Terminal = terminal.Terminal;
+const Health = renderer.Health;
+
+const getConstraint = @import("../font/nerd_font_attributes.zig").getConstraint;
+
+const FileType = @import("../file_type.zig").FileType;
+
+const macos = switch (builtin.os.tag) {
+ .macos => @import("macos"),
+ else => void,
+};
+
+const DisplayLink = switch (builtin.os.tag) {
+ .macos => *macos.video.DisplayLink,
+ else => void,
+};
+
+const log = std.log.scoped(.generic_renderer);
+
+/// Create a renderer type with the provided graphics API wrapper.
+///
+/// The graphics API wrapper must provide the interface outlined below.
+/// Specific details for the interfaces are documented on the existing
+/// implementations (`Metal` and `OpenGL`).
+///
+/// Hierarchy of graphics abstractions:
+///
+/// [ GraphicsAPI ] - Responsible for configuring the runtime surface
+/// | | and providing render `Target`s that draw to it,
+/// | | as well as `Frame`s and `Pipeline`s.
+/// | V
+/// | [ Target ] - Represents an abstract target for rendering, which
+/// | could be a surface directly but is also used as an
+/// | abstraction for off-screen frame buffers.
+/// V
+/// [ Frame ] - Represents the context for drawing a given frame,
+/// | provides `RenderPass`es for issuing draw commands
+/// | to, and reports the frame health when complete.
+/// V
+/// [ RenderPass ] - Represents a render pass in a frame, consisting of
+/// : one or more `Step`s applied to the same target(s),
+/// [ Step ] - - - - each describing the input buffers and textures and
+/// : the vertex/fragment functions and geometry to use.
+/// :_ _ _ _ _ _ _ _ _ _/
+/// v
+/// [ Pipeline ] - Describes a vertex and fragment function to be used
+/// for a `Step`; the `GraphicsAPI` is responsible for
+/// these and they should be constructed and cached
+/// ahead of time.
+///
+/// [ Buffer ] - An abstraction over a GPU buffer.
+///
+/// [ Texture ] - An abstraction over a GPU texture.
+///
+pub fn Renderer(comptime GraphicsAPI: type) type {
+ return struct {
+ const Self = @This();
+
+ pub const API = GraphicsAPI;
+
+ const Target = GraphicsAPI.Target;
+ const Buffer = GraphicsAPI.Buffer;
+ const Sampler = GraphicsAPI.Sampler;
+ const Texture = GraphicsAPI.Texture;
+ const RenderPass = GraphicsAPI.RenderPass;
+
+ const shaderpkg = GraphicsAPI.shaders;
+ const Shaders = shaderpkg.Shaders;
+
+ /// Allocator that can be used
+ alloc: std.mem.Allocator,
+
+ /// This mutex must be held whenever any state used in `drawFrame` is
+ /// being modified, and also when it's being accessed in `drawFrame`.
+ draw_mutex: std.Thread.Mutex = .{},
+
+ /// The configuration we need derived from the main config.
+ config: DerivedConfig,
+
+ /// The mailbox for communicating with the window.
+ surface_mailbox: apprt.surface.Mailbox,
+
+ /// Current font metrics defining our grid.
+ grid_metrics: font.Metrics,
+
+ /// The size of everything.
+ size: renderer.Size,
+
+ /// True if the window is focused
+ focused: bool,
+
+ /// Flag to indicate that our focus state changed for custom
+ /// shaders to update their state.
+ custom_shader_focused_changed: bool = false,
+
+ /// The most recent scrollbar state. We use this as a cache to
+ /// determine if we need to notify the apprt that there was a
+ /// scrollbar change.
+ scrollbar: terminal.Scrollbar,
+ scrollbar_dirty: bool,
+
+ /// Tracks the last bottom-right pin of the screen to detect new output.
+ /// When the final line changes (node or y differs), new content was added.
+ /// Used for scroll-to-bottom on output feature.
+ last_bottom_node: ?usize,
+ last_bottom_y: terminal.size.CellCountInt,
+
+ /// The most recent viewport matches so that we can render search
+ /// matches in the visible frame. This is provided asynchronously
+ /// from the search thread so we have the dirty flag to also note
+ /// if we need to rebuild our cells to include search highlights.
+ ///
+ /// Note that the selections MAY BE INVALID (point to PageList nodes
+ /// that do not exist anymore). These must be validated prior to use.
+ search_matches: ?renderer.Message.SearchMatches,
+ search_selected_match: ?renderer.Message.SearchMatch,
+ search_matches_dirty: bool,
+
+ /// The current set of cells to render. This is rebuilt on every frame
+ /// but we keep this around so that we don't reallocate. Each set of
+ /// cells goes into a separate shader.
+ cells: cellpkg.Contents,
+
+ /// Set to true after rebuildCells is called. This can be used
+ /// to determine if any possible changes have been made to the
+ /// cells for the draw call.
+ cells_rebuilt: bool = false,
+
+ /// The current GPU uniform values.
+ uniforms: shaderpkg.Uniforms,
+
+ /// Custom shader uniform values.
+ custom_shader_uniforms: shadertoy.Uniforms,
+
+ /// Timestamp we rendered out first frame.
+ ///
+ /// This is used when updating custom shader uniforms.
+ first_frame_time: ?std.time.Instant = null,
+
+ /// Timestamp when we rendered out more recent frame.
+ ///
+ /// This is used when updating custom shader uniforms.
+ last_frame_time: ?std.time.Instant = null,
+
+ /// The font structures.
+ font_grid: *font.SharedGrid,
+ font_shaper: font.Shaper,
+ font_shaper_cache: font.ShaperCache,
+
+ /// The images that we may render.
+ images: ImageState = .empty,
+
+ /// Background image, if we have one.
+ bg_image: ?imagepkg.Image = null,
+ /// Set whenever the background image changes, signalling
+ /// that the new background image needs to be uploaded to
+ /// the GPU.
+ ///
+ /// This is initialized as true so that we load the image
+ /// on renderer initialization, not just on config change.
+ bg_image_changed: bool = true,
+ /// Background image vertex buffer.
+ bg_image_buffer: shaderpkg.BgImage,
+ /// This value is used to force-update the swap chain copy
+ /// of the background image buffer whenever we change it.
+ bg_image_buffer_modified: usize = 0,
+
+ /// Graphics API state.
+ api: GraphicsAPI,
+
+ /// The CVDisplayLink used to drive the rendering loop in
+ /// sync with the display. This is void on platforms that
+ /// don't support a display link.
+ display_link: ?DisplayLink = null,
+
+ /// Health of the most recently completed frame.
+ health: std.atomic.Value(Health) = .{ .raw = .healthy },
+
+ /// Our swap chain (multiple buffering)
+ swap_chain: SwapChain,
+
+ /// This value is used to force-update swap chain targets in the
+ /// event of a config change that requires it (such as blending mode).
+ target_config_modified: usize = 0,
+
+ /// If something happened that requires us to reinitialize our shaders,
+ /// this is set to true so that we can do that whenever possible.
+ reinitialize_shaders: bool = false,
+
+ /// Whether or not we have custom shaders.
+ has_custom_shaders: bool = false,
+
+ /// Our shader pipelines.
+ shaders: Shaders,
+
+ /// The render state we update per loop.
+ terminal_state: terminal.RenderState = .empty,
+
+ /// The number of frames since the last terminal state reset.
+ /// We reset the terminal state after ~100,000 frames (about 10 to
+ /// 15 minutes at 120Hz) to prevent wasted memory buildup from
+ /// a large screen.
+ terminal_state_frame_count: usize = 0,
+
+ /// Our overlay state, if any.
+ overlay: ?Overlay = null,
+
+ const HighlightTag = enum(u8) {
+ search_match,
+ search_match_selected,
+ };
+ /// Swap chain which maintains multiple copies of the state needed to
+ /// render a frame, so that we can start building the next frame while
+ /// the previous frame is still being processed on the GPU.
+ const SwapChain = struct {
+ // The count of buffers we use for double/triple buffering.
+ // If this is one then we don't do any double+ buffering at all.
+ // This is comptime because there isn't a good reason to change
+ // this at runtime and there is a lot of complexity to support it.
+ const buf_count = GraphicsAPI.swap_chain_count;
+
+ /// `buf_count` structs that can hold the
+ /// data needed by the GPU to draw a frame.
+ frames: [buf_count]FrameState,
+ /// Index of the most recently used frame state struct.
+ frame_index: std.math.IntFittingRange(0, buf_count) = 0,
+ /// Semaphore that we wait on to make sure we have an available
+ /// frame state struct so we can start working on a new frame.
+ frame_sema: std.Thread.Semaphore = .{ .permits = buf_count },
+
+ /// Set to true when deinited, if you try to deinit a defunct
+ /// swap chain it will just be ignored, to prevent double-free.
+ ///
+ /// This is required because of `displayUnrealized`, since it
+ /// `deinits` the swapchain, which leads to a double-free if
+ /// the renderer is deinited after that.
+ defunct: bool = false,
+
+ pub fn init(api: GraphicsAPI, custom_shaders: bool) !SwapChain {
+ var result: SwapChain = .{ .frames = undefined };
+
+ // Initialize all of our frame state.
+ for (&result.frames) |*frame| {
+ frame.* = try FrameState.init(api, custom_shaders);
+ }
+
+ return result;
+ }
+
+ pub fn deinit(self: *SwapChain) void {
+ if (self.defunct) return;
+ self.defunct = true;
+
+ // Wait for all of our inflight draws to complete
+ // so that we can cleanly deinit our GPU state.
+ for (0..buf_count) |_| self.frame_sema.wait();
+ for (&self.frames) |*frame| frame.deinit();
+ }
+
+ /// Get the next frame state to draw to. This will wait on the
+ /// semaphore to ensure that the frame is available. This must
+ /// always be paired with a call to releaseFrame.
+ pub fn nextFrame(self: *SwapChain) error{Defunct}!*FrameState {
+ if (self.defunct) return error.Defunct;
+
+ self.frame_sema.wait();
+ errdefer self.frame_sema.post();
+ self.frame_index = (self.frame_index + 1) % buf_count;
+ return &self.frames[self.frame_index];
+ }
+
+ /// This should be called when the frame has completed drawing.
+ pub fn releaseFrame(self: *SwapChain) void {
+ self.frame_sema.post();
+ }
+ };
+
+ /// State we need duplicated for every frame. Any state that could be
+ /// in a data race between the GPU and CPU while a frame is being drawn
+ /// should be in this struct.
+ ///
+ /// While a draw is in-process, we "lock" the state (via a semaphore)
+ /// and prevent the CPU from updating the state until our graphics API
+ /// reports that the frame is complete.
+ ///
+ /// This is used to implement double/triple buffering.
+ const FrameState = struct {
+ uniforms: UniformBuffer,
+ cells: CellTextBuffer,
+ cells_bg: CellBgBuffer,
+
+ grayscale: Texture,
+ grayscale_modified: usize = 0,
+ color: Texture,
+ color_modified: usize = 0,
+
+ target: Target,
+ /// See property of same name on Renderer for explanation.
+ target_config_modified: usize = 0,
+
+ /// Buffer with the vertex data for our background image.
+ ///
+ /// TODO: Make this an optional and only create it
+ /// if we actually have a background image.
+ bg_image_buffer: BgImageBuffer,
+ /// See property of same name on Renderer for explanation.
+ bg_image_buffer_modified: usize = 0,
+
+ /// Custom shader state, this is null if we have no custom shaders.
+ custom_shader_state: ?CustomShaderState = null,
+
+ const UniformBuffer = Buffer(shaderpkg.Uniforms);
+ const CellBgBuffer = Buffer(shaderpkg.CellBg);
+ const CellTextBuffer = Buffer(shaderpkg.CellText);
+ const BgImageBuffer = Buffer(shaderpkg.BgImage);
+
+ pub fn init(api: GraphicsAPI, custom_shaders: bool) !FrameState {
+ // Uniform buffer contains exactly 1 uniform struct. The
+ // uniform data will be undefined so this must be set before
+ // a frame is drawn.
+ var uniforms = try UniformBuffer.init(api.uniformBufferOptions(), 1);
+ errdefer uniforms.deinit();
+
+ // Create GPU buffers for our cells.
+ //
+ // We start them off with a size of 1, which will of course be
+ // too small, but they will be resized as needed. This is a bit
+ // wasteful but since it's a one-time thing it's not really a
+ // huge concern.
+ var cells = try CellTextBuffer.init(api.fgBufferOptions(), 1);
+ errdefer cells.deinit();
+ var cells_bg = try CellBgBuffer.init(api.bgBufferOptions(), 1);
+ errdefer cells_bg.deinit();
+
+ // Create a GPU buffer for our background image info.
+ var bg_image_buffer = try BgImageBuffer.init(
+ api.bgImageBufferOptions(),
+ 1,
+ );
+ errdefer bg_image_buffer.deinit();
+
+ // Initialize our textures for our font atlas.
+ //
+ // As with the buffers above, we start these off as small
+ // as possible since they'll inevitably be resized anyway.
+ const grayscale = try api.initAtlasTexture(&.{
+ .data = undefined,
+ .size = 1,
+ .format = .grayscale,
+ });
+ errdefer grayscale.deinit();
+ const color = try api.initAtlasTexture(&.{
+ .data = undefined,
+ .size = 1,
+ .format = .bgra,
+ });
+ errdefer color.deinit();
+
+ var custom_shader_state =
+ if (custom_shaders)
+ try CustomShaderState.init(api)
+ else
+ null;
+ errdefer if (custom_shader_state) |*state| state.deinit();
+
+ // Initialize the target. Just as with the other resources,
+ // start it off as small as we can since it'll be resized.
+ const target = try api.initTarget(1, 1);
+
+ return .{
+ .uniforms = uniforms,
+ .cells = cells,
+ .cells_bg = cells_bg,
+ .bg_image_buffer = bg_image_buffer,
+ .grayscale = grayscale,
+ .color = color,
+ .target = target,
+ .custom_shader_state = custom_shader_state,
+ };
+ }
+
+ pub fn deinit(self: *FrameState) void {
+ self.target.deinit();
+ self.uniforms.deinit();
+ self.cells.deinit();
+ self.cells_bg.deinit();
+ self.grayscale.deinit();
+ self.color.deinit();
+ self.bg_image_buffer.deinit();
+ if (self.custom_shader_state) |*state| state.deinit();
+ }
+
+ pub fn resize(
+ self: *FrameState,
+ api: GraphicsAPI,
+ width: usize,
+ height: usize,
+ ) !void {
+ if (self.custom_shader_state) |*state| {
+ try state.resize(api, width, height);
+ }
+ const target = try api.initTarget(width, height);
+ self.target.deinit();
+ self.target = target;
+ }
+ };
+
+ /// State relevant to our custom shaders if we have any.
+ const CustomShaderState = struct {
+ /// When we have a custom shader state, we maintain a front
+ /// and back texture which we use as a swap chain to render
+ /// between when multiple custom shaders are defined.
+ front_texture: Texture,
+ back_texture: Texture,
+
+ /// Shadertoy uses a sampler for accessing the various channel
+ /// textures. In Metal, we need to explicitly create these since
+ /// the glslang-to-msl compiler doesn't do it for us (as we
+ /// normally would in hand-written MSL). To keep it clean and
+ /// consistent, we just force all rendering APIs to provide an
+ /// explicit sampler.
+ ///
+ /// Samplers are immutable and describe sampling properties so
+ /// we can share the sampler across front/back textures (although
+ /// we only need it for the source texture at a time, we don't
+ /// need to "swap" it).
+ sampler: Sampler,
+
+ uniforms: UniformBuffer,
+
+ const UniformBuffer = Buffer(shadertoy.Uniforms);
+
+ /// Swap the front and back textures.
+ pub fn swap(self: *CustomShaderState) void {
+ std.mem.swap(Texture, &self.front_texture, &self.back_texture);
+ }
+
+ pub fn init(api: GraphicsAPI) !CustomShaderState {
+ // Create a GPU buffer to hold our uniforms.
+ var uniforms = try UniformBuffer.init(api.uniformBufferOptions(), 1);
+ errdefer uniforms.deinit();
+
+ // Initialize the front and back textures at 1x1 px, this
+ // is slightly wasteful but it's only done once so whatever.
+ const front_texture = try Texture.init(
+ api.textureOptions(),
+ 1,
+ 1,
+ null,
+ );
+ errdefer front_texture.deinit();
+ const back_texture = try Texture.init(
+ api.textureOptions(),
+ 1,
+ 1,
+ null,
+ );
+ errdefer back_texture.deinit();
+
+ const sampler = try Sampler.init(api.samplerOptions());
+ errdefer sampler.deinit();
+
+ return .{
+ .front_texture = front_texture,
+ .back_texture = back_texture,
+ .sampler = sampler,
+ .uniforms = uniforms,
+ };
+ }
+
+ pub fn deinit(self: *CustomShaderState) void {
+ self.front_texture.deinit();
+ self.back_texture.deinit();
+ self.sampler.deinit();
+ self.uniforms.deinit();
+ }
+
+ pub fn resize(
+ self: *CustomShaderState,
+ api: GraphicsAPI,
+ width: usize,
+ height: usize,
+ ) !void {
+ const front_texture = try Texture.init(
+ api.textureOptions(),
+ @intCast(width),
+ @intCast(height),
+ null,
+ );
+ errdefer front_texture.deinit();
+ const back_texture = try Texture.init(
+ api.textureOptions(),
+ @intCast(width),
+ @intCast(height),
+ null,
+ );
+ errdefer back_texture.deinit();
+
+ self.front_texture.deinit();
+ self.back_texture.deinit();
+
+ self.front_texture = front_texture;
+ self.back_texture = back_texture;
+ }
+ };
+
+ /// 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_thicken_strength: u8,
+ font_features: std.ArrayListUnmanaged([:0]const u8),
+ font_styles: font.CodepointResolver.StyleStatus,
+ font_shaping_break: configpkg.FontShapingBreak,
+ cursor_color: ?configpkg.Config.TerminalColor,
+ cursor_opacity: f64,
+ cursor_text: ?configpkg.Config.TerminalColor,
+ background: terminal.color.RGB,
+ background_opacity: f64,
+ background_opacity_cells: bool,
+ foreground: terminal.color.RGB,
+ selection_background: ?configpkg.Config.TerminalColor,
+ selection_foreground: ?configpkg.Config.TerminalColor,
+ search_background: configpkg.Config.TerminalColor,
+ search_foreground: configpkg.Config.TerminalColor,
+ search_selected_background: configpkg.Config.TerminalColor,
+ search_selected_foreground: configpkg.Config.TerminalColor,
+ bold_color: ?terminal.Style.BoldColor,
+ faint_opacity: u8,
+ min_contrast: f32,
+ padding_color: configpkg.WindowPaddingColor,
+ custom_shaders: configpkg.RepeatablePath,
+ bg_image: ?configpkg.Path,
+ bg_image_opacity: f32,
+ bg_image_position: configpkg.BackgroundImagePosition,
+ bg_image_fit: configpkg.BackgroundImageFit,
+ bg_image_repeat: bool,
+ links: link.Set,
+ vsync: bool,
+ colorspace: configpkg.Config.WindowColorspace,
+ blending: configpkg.Config.AlphaBlending,
+ background_blur: configpkg.Config.BackgroundBlur,
+ scroll_to_bottom_on_output: bool,
+
+ 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 background image
+ const bg_image =
+ if (config.@"background-image") |bg|
+ try bg.clone(alloc)
+ else
+ null;
+
+ // 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,
+ );
+
+ return .{
+ .background_opacity = @max(0, @min(1, config.@"background-opacity")),
+ .background_opacity_cells = config.@"background-opacity-cells",
+ .font_thicken = config.@"font-thicken",
+ .font_thicken_strength = config.@"font-thicken-strength",
+ .font_features = font_features.list,
+ .font_styles = font_styles,
+ .font_shaping_break = config.@"font-shaping-break",
+
+ .cursor_color = config.@"cursor-color",
+ .cursor_text = config.@"cursor-text",
+ .cursor_opacity = @max(0, @min(1, config.@"cursor-opacity")),
+
+ .background = config.background.toTerminalRGB(),
+ .foreground = config.foreground.toTerminalRGB(),
+ .bold_color = if (config.@"bold-color") |b| b.toTerminal() else null,
+ .faint_opacity = @intFromFloat(@ceil(config.@"faint-opacity" * 255)),
+
+ .min_contrast = @floatCast(config.@"minimum-contrast"),
+ .padding_color = config.@"window-padding-color",
+
+ .selection_background = config.@"selection-background",
+ .selection_foreground = config.@"selection-foreground",
+ .search_background = config.@"search-background",
+ .search_foreground = config.@"search-foreground",
+ .search_selected_background = config.@"search-selected-background",
+ .search_selected_foreground = config.@"search-selected-foreground",
+
+ .custom_shaders = custom_shaders,
+ .bg_image = bg_image,
+ .bg_image_opacity = config.@"background-image-opacity",
+ .bg_image_position = config.@"background-image-position",
+ .bg_image_fit = config.@"background-image-fit",
+ .bg_image_repeat = config.@"background-image-repeat",
+ .links = links,
+ .vsync = config.@"window-vsync",
+ .colorspace = config.@"window-colorspace",
+ .blending = config.@"alpha-blending",
+ .background_blur = config.@"background-blur",
+ .scroll_to_bottom_on_output = config.@"scroll-to-bottom".output,
+ .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) !Self {
+ // Initialize our graphics API wrapper, this will prepare the
+ // surface provided by the apprt and set up any API-specific
+ // GPU resources.
+ var api = try GraphicsAPI.init(alloc, options);
+ errdefer api.deinit();
+
+ const has_custom_shaders = options.config.custom_shaders.value.items.len > 0;
+
+ // Prepare our swap chain
+ var swap_chain = try SwapChain.init(
+ api,
+ has_custom_shaders,
+ );
+ errdefer swap_chain.deinit();
+
+ // Create the font shaper.
+ var font_shaper = try font.Shaper.init(alloc, .{
+ .features = options.config.font_features.items,
+ });
+ errdefer font_shaper.deinit();
+
+ // Initialize all the data that requires a critical font section.
+ const font_critical: struct {
+ metrics: font.Metrics,
+ } = font_critical: {
+ const grid: *font.SharedGrid = options.font_grid;
+ grid.lock.lockShared();
+ defer grid.lock.unlockShared();
+ break :font_critical .{
+ .metrics = grid.metrics,
+ };
+ };
+
+ const display_link: ?DisplayLink = switch (builtin.os.tag) {
+ .macos => if (options.config.vsync)
+ try macos.video.DisplayLink.createWithActiveCGDisplays()
+ else
+ null,
+ else => null,
+ };
+ errdefer if (display_link) |v| v.release();
+
+ var result: Self = .{
+ .alloc = alloc,
+ .config = options.config,
+ .surface_mailbox = options.surface_mailbox,
+ .grid_metrics = font_critical.metrics,
+ .size = options.size,
+ .focused = true,
+ .scrollbar = .zero,
+ .scrollbar_dirty = false,
+ .last_bottom_node = null,
+ .last_bottom_y = 0,
+ .search_matches = null,
+ .search_selected_match = null,
+ .search_matches_dirty = false,
+
+ // Render state
+ .cells = .{},
+ .uniforms = .{
+ .projection_matrix = undefined,
+ .cell_size = undefined,
+ .grid_size = undefined,
+ .grid_padding = undefined,
+ .screen_size = undefined,
+ .padding_extend = .{},
+ .min_contrast = options.config.min_contrast,
+ .cursor_pos = .{ std.math.maxInt(u16), std.math.maxInt(u16) },
+ .cursor_color = undefined,
+ .bg_color = .{
+ options.config.background.r,
+ options.config.background.g,
+ options.config.background.b,
+ // Note that if we're on macOS with glass effects
+ // we'll disable background opacity but we handle
+ // that in updateFrame.
+ @intFromFloat(@round(options.config.background_opacity * 255.0)),
+ },
+ .bools = .{
+ .cursor_wide = false,
+ .use_display_p3 = options.config.colorspace == .@"display-p3",
+ .use_linear_blending = options.config.blending.isLinear(),
+ .use_linear_correction = options.config.blending == .@"linear-corrected",
+ },
+ },
+ .custom_shader_uniforms = .{
+ .resolution = .{ 0, 0, 1 },
+ .time = 0,
+ .time_delta = 0,
+ .frame_rate = 60, // not currently updated
+ .frame = 0,
+ .channel_time = @splat(@splat(0)), // not currently updated
+ .channel_resolution = @splat(@splat(0)),
+ .mouse = @splat(0), // not currently updated
+ .date = @splat(0), // not currently updated
+ .sample_rate = 0, // N/A, we don't have any audio
+ .current_cursor = @splat(0),
+ .previous_cursor = @splat(0),
+ .current_cursor_color = @splat(0),
+ .previous_cursor_color = @splat(0),
+ .current_cursor_style = 0,
+ .previous_cursor_style = 0,
+ .cursor_visible = 0,
+ .cursor_change_time = 0,
+ .time_focus = 0,
+ .focus = 1, // assume focused initially
+ .palette = @splat(@splat(0)),
+ .background_color = @splat(0),
+ .foreground_color = @splat(0),
+ .cursor_color = @splat(0),
+ .cursor_text = @splat(0),
+ .selection_background_color = @splat(0),
+ .selection_foreground_color = @splat(0),
+ },
+ .bg_image_buffer = undefined,
+
+ // Fonts
+ .font_grid = options.font_grid,
+ .font_shaper = font_shaper,
+ .font_shaper_cache = font.ShaperCache.init(),
+
+ // Shaders (initialized below)
+ .shaders = undefined,
+
+ // Graphics API stuff
+ .api = api,
+ .swap_chain = swap_chain,
+ .display_link = display_link,
+ };
+
+ try result.initShaders();
+
+ // Ensure our undefined values above are correctly initialized.
+ result.updateFontGridUniforms();
+ result.updateScreenSizeUniforms();
+ result.updateBgImageBuffer();
+ try result.prepBackgroundImage();
+
+ return result;
+ }
+
+ pub fn deinit(self: *Self) void {
+ if (self.overlay) |*overlay| overlay.deinit(self.alloc);
+ self.terminal_state.deinit(self.alloc);
+ if (self.search_selected_match) |*m| m.arena.deinit();
+ if (self.search_matches) |*m| m.arena.deinit();
+ self.swap_chain.deinit();
+
+ if (DisplayLink != void) {
+ if (self.display_link) |display_link| {
+ display_link.stop() catch {};
+ display_link.release();
+ }
+ }
+
+ self.cells.deinit(self.alloc);
+
+ self.font_shaper.deinit();
+ self.font_shaper_cache.deinit(self.alloc);
+
+ self.config.deinit();
+
+ self.images.deinit(self.alloc);
+
+ if (self.bg_image) |img| img.deinit(self.alloc);
+
+ self.deinitShaders();
+
+ self.api.deinit();
+
+ self.* = undefined;
+ }
+
+ fn deinitShaders(self: *Self) void {
+ self.shaders.deinit(self.alloc);
+ }
+
+ fn initShaders(self: *Self) !void {
+ var arena = ArenaAllocator.init(self.alloc);
+ defer arena.deinit();
+ const arena_alloc = arena.allocator();
+
+ // Load our custom shaders
+ const custom_shaders: []const [:0]const u8 = shadertoy.loadFromFiles(
+ arena_alloc,
+ self.config.custom_shaders,
+ GraphicsAPI.custom_shader_target,
+ ) catch |err| err: {
+ log.warn("error loading custom shaders err={}", .{err});
+ break :err &.{};
+ };
+
+ const has_custom_shaders = custom_shaders.len > 0;
+
+ var shaders = try self.api.initShaders(
+ self.alloc,
+ custom_shaders,
+ );
+ errdefer shaders.deinit(self.alloc);
+
+ self.shaders = shaders;
+ self.has_custom_shaders = has_custom_shaders;
+ }
+
+ /// This is called early right after surface creation.
+ pub fn surfaceInit(surface: *apprt.Surface) !void {
+ // If our API has to do things here, let it.
+ if (@hasDecl(GraphicsAPI, "surfaceInit")) {
+ try GraphicsAPI.surfaceInit(surface);
+ }
+ }
+
+ /// This is called just prior to spinning up the renderer thread for
+ /// final main thread setup requirements.
+ pub fn finalizeSurfaceInit(self: *Self, surface: *apprt.Surface) !void {
+ // If our API has to do things to finalize surface init, let it.
+ if (@hasDecl(GraphicsAPI, "finalizeSurfaceInit")) {
+ try self.api.finalizeSurfaceInit(surface);
+ }
+ }
+
+ /// Callback called by renderer.Thread when it begins.
+ pub fn threadEnter(self: *const Self, surface: *apprt.Surface) !void {
+ // If our API has to do things on thread enter, let it.
+ if (@hasDecl(GraphicsAPI, "threadEnter")) {
+ try self.api.threadEnter(surface);
+ }
+ }
+
+ /// Callback called by renderer.Thread when it exits.
+ pub fn threadExit(self: *const Self) void {
+ // If our API has to do things on thread exit, let it.
+ if (@hasDecl(GraphicsAPI, "threadExit")) {
+ self.api.threadExit();
+ }
+ }
+
+ /// Called by renderer.Thread when it starts the main loop.
+ pub fn loopEnter(self: *Self, thr: *renderer.Thread) !void {
+ // If our API has to do things on loop enter, let it.
+ if (@hasDecl(GraphicsAPI, "loopEnter")) {
+ self.api.loopEnter();
+ }
+
+ // If we don't support a display link we have no work to do.
+ if (comptime DisplayLink == void) return;
+
+ // This is when we know our "self" pointer is stable so we can
+ // setup the display link. To setup the display link we set our
+ // callback and we can start it immediately.
+ const display_link = self.display_link orelse return;
+ try display_link.setOutputCallback(
+ xev.Async,
+ &displayLinkCallback,
+ &thr.draw_now,
+ );
+ display_link.start() catch {};
+ }
+
+ /// Called by renderer.Thread when it exits the main loop.
+ pub fn loopExit(self: *Self) void {
+ // If our API has to do things on loop exit, let it.
+ if (@hasDecl(GraphicsAPI, "loopExit")) {
+ self.api.loopExit();
+ }
+
+ // If we don't support a display link we have no work to do.
+ if (comptime DisplayLink == void) return;
+
+ // Stop our display link. If this fails its okay it just means
+ // that we either never started it or the view its attached to
+ // is gone which is fine.
+ const display_link = self.display_link orelse return;
+ display_link.stop() catch {};
+ }
+
+ /// This is called by the GTK apprt after the surface is
+ /// reinitialized due to any of the events mentioned in
+ /// the doc comment for `displayUnrealized`.
+ pub fn displayRealized(self: *Self) !void {
+ // If our API has to do things on realize, let it.
+ if (@hasDecl(GraphicsAPI, "displayRealized")) {
+ self.api.displayRealized();
+ }
+
+ // Lock the draw mutex so that we can
+ // safely reinitialize our GPU resources.
+ self.draw_mutex.lock();
+ defer self.draw_mutex.unlock();
+
+ // We assume that the swap chain was deinited in
+ // `displayUnrealized`, in which case it should be
+ // marked defunct. If not, we have a problem.
+ assert(self.swap_chain.defunct);
+
+ // We reinitialize our shaders and our swap chain.
+ try self.initShaders();
+ self.swap_chain = try SwapChain.init(
+ self.api,
+ self.has_custom_shaders,
+ );
+ self.reinitialize_shaders = false;
+ self.target_config_modified = 1;
+ }
+
+ /// This is called by the GTK apprt when the surface is being destroyed.
+ /// This can happen because the surface is being closed but also when
+ /// moving the window between displays or splitting.
+ pub fn displayUnrealized(self: *Self) void {
+ // If our API has to do things on unrealize, let it.
+ if (@hasDecl(GraphicsAPI, "displayUnrealized")) {
+ self.api.displayUnrealized();
+ }
+
+ // Lock the draw mutex so that we can
+ // safely deinitialize our GPU resources.
+ self.draw_mutex.lock();
+ defer self.draw_mutex.unlock();
+
+ // We deinit our swap chain and shaders.
+ //
+ // This will mark them as defunct so that they
+ // can't be double-freed or used in draw calls.
+ self.swap_chain.deinit();
+ self.shaders.deinit(self.alloc);
+ }
+
+ fn displayLinkCallback(
+ _: *macos.video.DisplayLink,
+ ud: ?*xev.Async,
+ ) void {
+ const draw_now = ud orelse return;
+ draw_now.notify() catch |err| {
+ log.err("error notifying draw_now err={}", .{err});
+ };
+ }
+
+ /// Mark the full screen as dirty so that we redraw everything.
+ pub inline fn markDirty(self: *Self) void {
+ self.terminal_state.dirty = .full;
+ }
+
+ /// Called when we get an updated display ID for our display link.
+ pub fn setMacOSDisplayID(self: *Self, id: u32) !void {
+ if (comptime DisplayLink == void) return;
+ const display_link = self.display_link orelse return;
+ log.info("updating display link display id={}", .{id});
+ display_link.setCurrentCGDisplay(id) catch |err| {
+ log.warn("error setting display link display id err={}", .{err});
+ };
+ }
+
+ /// True if our renderer has animations so that a higher frequency
+ /// timer is used.
+ pub fn hasAnimations(self: *const Self) bool {
+ return self.has_custom_shaders;
+ }
+
+ /// True if our renderer is using vsync. If true, the renderer or apprt
+ /// is responsible for triggering draw_now calls to the render thread.
+ /// That is the only way to trigger a drawFrame.
+ pub fn hasVsync(self: *const Self) bool {
+ if (comptime DisplayLink == void) return false;
+ const display_link = self.display_link orelse return false;
+ return display_link.isRunning();
+ }
+
+ /// Callback when the focus changes for the terminal this is rendering.
+ ///
+ /// Must be called on the render thread.
+ pub fn setFocus(self: *Self, focus: bool) !void {
+ assert(self.focused != focus);
+
+ self.focused = focus;
+
+ // Flag that we need to update our custom shaders
+ self.custom_shader_focused_changed = true;
+
+ // If we're not focused, then we want to stop the display link
+ // because it is a waste of resources and we can move to pure
+ // change-driven updates.
+ if (comptime DisplayLink != void) link: {
+ const display_link = self.display_link orelse break :link;
+ if (focus) {
+ display_link.start() catch {};
+ } else {
+ display_link.stop() catch {};
+ }
+ }
+ }
+
+ /// Callback when the window is visible or occluded.
+ ///
+ /// Must be called on the render thread.
+ pub fn setVisible(self: *Self, visible: bool) void {
+ // If we're not visible, then we want to stop the display link
+ // because it is a waste of resources and we can move to pure
+ // change-driven updates.
+ if (comptime DisplayLink != void) link: {
+ const display_link = self.display_link orelse break :link;
+ if (visible and self.focused) {
+ display_link.start() catch {};
+ } else {
+ display_link.stop() catch {};
+ }
+ }
+ }
+
+ /// Set the new font grid.
+ ///
+ /// Must be called on the render thread.
+ pub fn setFontGrid(self: *Self, grid: *font.SharedGrid) void {
+ self.draw_mutex.lock();
+ defer self.draw_mutex.unlock();
+
+ // Update our grid
+ self.font_grid = grid;
+
+ // Update all our textures so that they sync on the next frame.
+ // We can modify this without a lock because the GPU does not
+ // touch this data.
+ for (&self.swap_chain.frames) |*frame| {
+ frame.grayscale_modified = 0;
+ frame.color_modified = 0;
+ }
+
+ // Get our metrics from the grid. This doesn't require a lock because
+ // the metrics are never recalculated.
+ const metrics = grid.metrics;
+ self.grid_metrics = metrics;
+
+ // 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 cell size.
+ self.size.cell = .{
+ .width = metrics.cell_width,
+ .height = metrics.cell_height,
+ };
+
+ // Update relevant uniforms
+ self.updateFontGridUniforms();
+
+ // Force a full rebuild, because cached rows may still reference
+ // an outdated atlas from the old grid and this can cause garbage
+ // to be rendered.
+ self.markDirty();
+ }
+
+ /// Update uniforms that are based on the font grid.
+ ///
+ /// Caller must hold the draw mutex.
+ fn updateFontGridUniforms(self: *Self) void {
+ self.uniforms.cell_size = .{
+ @floatFromInt(self.grid_metrics.cell_width),
+ @floatFromInt(self.grid_metrics.cell_height),
+ };
+ }
+
+ /// Update the frame data.
+ pub fn updateFrame(
+ self: *Self,
+ state: *renderer.State,
+ cursor_blink_visible: bool,
+ ) Allocator.Error!void {
+ // const start = std.time.Instant.now() catch unreachable;
+ // const start_micro = std.time.microTimestamp();
+ // defer {
+ // const end = std.time.Instant.now() catch unreachable;
+ // log.warn(
+ // "[updateFrame time] start_micro={} duration={}ns",
+ // .{ start_micro, end.since(start) / std.time.ns_per_us },
+ // );
+ // }
+
+ // We fully deinit and reset the terminal state every so often
+ // so that a particularly large terminal state doesn't cause
+ // the renderer to hold on to retained memory.
+ //
+ // Frame count is ~12 minutes at 120Hz.
+ const max_terminal_state_frame_count = 100_000;
+ if (self.terminal_state_frame_count >= max_terminal_state_frame_count) {
+ self.terminal_state.deinit(self.alloc);
+ self.terminal_state = .empty;
+ }
+ self.terminal_state_frame_count += 1;
+
+ // Create an arena for all our temporary allocations while rebuilding
+ var arena = ArenaAllocator.init(self.alloc);
+ defer arena.deinit();
+ const arena_alloc = arena.allocator();
+
+ // Data we extract out of the critical area.
+ const Critical = struct {
+ links: terminal.RenderState.CellSet,
+ mouse: renderer.State.Mouse,
+ preedit: ?renderer.State.Preedit,
+ scrollbar: terminal.Scrollbar,
+ overlay_features: []const Overlay.Feature,
+ };
+
+ // Update all our data as tightly as possible within the mutex.
+ var critical: Critical = critical: {
+ // const start = try std.time.Instant.now();
+ // const start_micro = std.time.microTimestamp();
+ // defer {
+ // const end = std.time.Instant.now() catch unreachable;
+ // std.log.err("[updateFrame critical time] start={}\tduration={} us", .{ start_micro, end.since(start) / std.time.ns_per_us });
+ // }
+
+ 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;
+ }
+
+ // If scroll-to-bottom on output is enabled, check if the final line
+ // changed by comparing the bottom-right pin. If the node pointer or
+ // y offset changed, new content was added to the screen.
+ // Update this BEFORE we update our render state so we can
+ // draw the new scrolled data immediately.
+ if (self.config.scroll_to_bottom_on_output) scroll: {
+ const br = state.terminal.screens.active.pages.getBottomRight(.screen) orelse break :scroll;
+
+ // If the pin hasn't changed, then don't scroll.
+ if (self.last_bottom_node == @intFromPtr(br.node) and
+ self.last_bottom_y == br.y) break :scroll;
+
+ // Update tracked pin state for next frame
+ self.last_bottom_node = @intFromPtr(br.node);
+ self.last_bottom_y = br.y;
+
+ // Scroll
+ state.terminal.scrollViewport(.bottom);
+ }
+
+ // Update our terminal state
+ try self.terminal_state.update(self.alloc, state.terminal);
+
+ // If our terminal state is dirty at all we need to redo
+ // the viewport search.
+ if (self.terminal_state.dirty != .false) {
+ state.terminal.flags.search_viewport_dirty = true;
+ }
+
+ // Get our scrollbar out of the terminal. We synchronize
+ // the scrollbar read with frame data updates because this
+ // naturally limits the number of calls to this method (it
+ // can be expensive) and also makes it so we don't need another
+ // cross-thread mailbox message within the IO path.
+ const scrollbar = state.terminal.screens.active.pages.scrollbar();
+
+ // Get our preedit state
+ const preedit: ?renderer.State.Preedit = preedit: {
+ const p = state.preedit orelse break :preedit null;
+ break :preedit try p.clone(arena_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 (self.images.kittyRequiresUpdate(state.terminal)) {
+ // We need to grab the draw mutex since this updates
+ // our image state that drawFrame uses.
+ self.draw_mutex.lock();
+ defer self.draw_mutex.unlock();
+ self.images.kittyUpdate(
+ self.alloc,
+ state.terminal,
+ .{
+ .width = self.grid_metrics.cell_width,
+ .height = self.grid_metrics.cell_height,
+ },
+ );
+ }
+
+ // Get our OSC8 links we're hovering if we have a mouse.
+ // This requires terminal state because of URLs.
+ const links: terminal.RenderState.CellSet = osc8: {
+ // If our mouse isn't hovering, we have no links.
+ const vp = state.mouse.point orelse break :osc8 .empty;
+
+ // If the right mods aren't pressed, then we can't match.
+ if (!state.mouse.mods.equal(inputpkg.ctrlOrSuper(.{})))
+ break :osc8 .empty;
+
+ break :osc8 self.terminal_state.linkCells(
+ arena_alloc,
+ vp,
+ ) catch |err| {
+ log.warn("error searching for OSC8 links err={}", .{err});
+ break :osc8 .empty;
+ };
+ };
+
+ const overlay_features: []const Overlay.Feature = overlay: {
+ const insp = state.inspector orelse break :overlay &.{};
+ const renderer_info = insp.rendererInfo();
+ break :overlay renderer_info.overlayFeatures(
+ arena_alloc,
+ ) catch &.{};
+ };
+
+ break :critical .{
+ .links = links,
+ .mouse = state.mouse,
+ .preedit = preedit,
+ .scrollbar = scrollbar,
+ .overlay_features = overlay_features,
+ };
+ };
+
+ // Outside the critical area we can update our links to contain
+ // our regex results.
+ self.config.links.renderCellMap(
+ arena_alloc,
+ &critical.links,
+ &self.terminal_state,
+ state.mouse.point,
+ state.mouse.mods,
+ ) catch |err| {
+ log.warn("error searching for regex links err={}", .{err});
+ };
+
+ // Clear our highlight state and update.
+ if (self.search_matches_dirty or self.terminal_state.dirty != .false) {
+ self.search_matches_dirty = false;
+
+ // Clear the prior highlights
+ const row_data = self.terminal_state.row_data.slice();
+ var any_dirty: bool = false;
+ for (
+ row_data.items(.highlights),
+ row_data.items(.dirty),
+ ) |*highlights, *dirty| {
+ if (highlights.items.len > 0) {
+ highlights.clearRetainingCapacity();
+ dirty.* = true;
+ any_dirty = true;
+ }
+ }
+ if (any_dirty and self.terminal_state.dirty == .false) {
+ self.terminal_state.dirty = .partial;
+ }
+
+ // NOTE: The order below matters. Highlights added earlier
+ // will take priority.
+
+ if (self.search_selected_match) |m| {
+ self.terminal_state.updateHighlightsFlattened(
+ self.alloc,
+ @intFromEnum(HighlightTag.search_match_selected),
+ &.{m.match},
+ ) catch |err| {
+ // Not a critical error, we just won't show highlights.
+ log.warn("error updating search selected highlight err={}", .{err});
+ };
+ }
+
+ if (self.search_matches) |m| {
+ self.terminal_state.updateHighlightsFlattened(
+ self.alloc,
+ @intFromEnum(HighlightTag.search_match),
+ m.matches,
+ ) catch |err| {
+ // Not a critical error, we just won't show highlights.
+ log.warn("error updating search highlights err={}", .{err});
+ };
+ }
+ }
+
+ // From this point forward no more errors.
+ errdefer comptime unreachable;
+
+ // Reset our dirty state after updating.
+ defer self.terminal_state.dirty = .false;
+
+ // Rebuild the overlay image if we have one. We can do this
+ // outside of any critical areas.
+ self.rebuildOverlay(
+ critical.overlay_features,
+ ) catch |err| {
+ log.warn(
+ "error rebuilding overlay surface err={}",
+ .{err},
+ );
+ };
+
+ // Acquire the draw mutex for all remaining state updates.
+ {
+ self.draw_mutex.lock();
+ defer self.draw_mutex.unlock();
+
+ // Build our GPU cells
+ self.rebuildCells(
+ critical.preedit,
+ renderer.cursorStyle(&self.terminal_state, .{
+ .preedit = critical.preedit != null,
+ .focused = self.focused,
+ .blink_visible = cursor_blink_visible,
+ }),
+ &critical.links,
+ ) catch |err| {
+ // This means we weren't able to allocate our buffer
+ // to update the cells. In this case, we continue with
+ // our old buffer (frozen contents) and log it.
+ comptime assert(@TypeOf(err) == error{OutOfMemory});
+ log.warn("error rebuilding GPU cells err={}", .{err});
+ };
+
+ // The scrollbar is only emitted during draws so we also
+ // check the scrollbar cache here and update if needed.
+ // This is pretty fast.
+ if (!self.scrollbar.eql(critical.scrollbar)) {
+ self.scrollbar = critical.scrollbar;
+ self.scrollbar_dirty = true;
+ }
+
+ // Update our background color
+ self.uniforms.bg_color = .{
+ self.terminal_state.colors.background.r,
+ self.terminal_state.colors.background.g,
+ self.terminal_state.colors.background.b,
+ @intFromFloat(@round(self.config.background_opacity * 255.0)),
+ };
+
+ // If we're on macOS and have glass styles, we remove
+ // the background opacity because the glass effect handles
+ // it.
+ if (comptime builtin.os.tag == .macos) switch (self.config.background_blur) {
+ .@"macos-glass-regular",
+ .@"macos-glass-clear",
+ => self.uniforms.bg_color[3] = 0,
+
+ else => {},
+ };
+
+ // Prepare our overlay image for upload (or unload). This
+ // has to use our general allocator since it modifies
+ // state that survives frames.
+ self.images.overlayUpdate(
+ self.alloc,
+ self.overlay,
+ ) catch |err| {
+ log.warn("error updating overlay images err={}", .{err});
+ };
+
+ // Update custom shader uniforms that depend on terminal state.
+ self.updateCustomShaderUniformsFromState();
+ }
+
+ // Notify our shaper we're done for the frame. For some shapers,
+ // such as CoreText, this triggers off-thread cleanup logic.
+ self.font_shaper.endFrame();
+ }
+
+ /// Draw the frame to the screen.
+ ///
+ /// If `sync` is true, this will synchronously block until
+ /// the frame is finished drawing and has been presented.
+ pub fn drawFrame(
+ self: *Self,
+ sync: bool,
+ ) !void {
+ // const start = std.time.Instant.now() catch unreachable;
+ // const start_micro = std.time.microTimestamp();
+ // defer {
+ // const end = std.time.Instant.now() catch unreachable;
+ // log.warn(
+ // "[drawFrame time] start_micro={} duration={}ns",
+ // .{ start_micro, end.since(start) / std.time.ns_per_us },
+ // );
+ // }
+
+ // We hold a the draw mutex to prevent changes to any
+ // data we access while we're in the middle of drawing.
+ self.draw_mutex.lock();
+ defer self.draw_mutex.unlock();
+
+ // After the graphics API is complete (so we defer) we want to
+ // update our scrollbar state.
+ defer if (self.scrollbar_dirty) {
+ // Fail instantly if the surface mailbox if full, we'll just
+ // get it on the next frame.
+ if (self.surface_mailbox.push(.{
+ .scrollbar = self.scrollbar,
+ }, .instant) > 0) self.scrollbar_dirty = false;
+ };
+
+ // Let our graphics API do any bookkeeping, etc.
+ // that it needs to do before / after `drawFrame`.
+ self.api.drawFrameStart();
+ defer self.api.drawFrameEnd();
+
+ // Retrieve the most up-to-date surface size from the Graphics API
+ const surface_size = try self.api.surfaceSize();
+
+ // If either of our surface dimensions is zero
+ // then drawing is absurd, so we just return.
+ if (surface_size.width == 0 or surface_size.height == 0) return;
+
+ const size_changed =
+ self.size.screen.width != surface_size.width or
+ self.size.screen.height != surface_size.height;
+
+ // Conditions under which we need to draw the frame, otherwise we
+ // don't need to since the previous frame should be identical.
+ const needs_redraw =
+ size_changed or
+ self.cells_rebuilt or
+ self.hasAnimations() or
+ sync;
+
+ if (!needs_redraw) {
+ // We still need to present the last target again, because the
+ // apprt may be swapping buffers and display an outdated frame
+ // if we don't draw something new.
+ try self.api.presentLastTarget();
+ return;
+ }
+ self.cells_rebuilt = false;
+
+ // Wait for a frame to be available.
+ const frame = try self.swap_chain.nextFrame();
+ errdefer self.swap_chain.releaseFrame();
+ // log.debug("drawing frame index={}", .{self.swap_chain.frame_index});
+
+ // If we need to reinitialize our shaders, do so.
+ if (self.reinitialize_shaders) {
+ self.reinitialize_shaders = false;
+ self.shaders.deinit(self.alloc);
+ try self.initShaders();
+ }
+
+ // Our shaders should not be defunct at this point.
+ assert(!self.shaders.defunct);
+
+ // If we have custom shaders, make sure we have the
+ // custom shader state in our frame state, otherwise
+ // if we have a state but don't need it we remove it.
+ if (self.has_custom_shaders) {
+ if (frame.custom_shader_state == null) {
+ frame.custom_shader_state = try .init(self.api);
+ try frame.custom_shader_state.?.resize(
+ self.api,
+ surface_size.width,
+ surface_size.height,
+ );
+ }
+ } else if (frame.custom_shader_state) |*state| {
+ state.deinit();
+ frame.custom_shader_state = null;
+ }
+
+ // If our stored size doesn't match the
+ // surface size we need to update it.
+ if (size_changed) {
+ self.size.screen = .{
+ .width = surface_size.width,
+ .height = surface_size.height,
+ };
+ self.updateScreenSizeUniforms();
+ }
+
+ // If this frame's target isn't the correct size, or the target
+ // config has changed (such as when the blending mode changes),
+ // remove it and replace it with a new one with the right values.
+ if (frame.target.width != self.size.screen.width or
+ frame.target.height != self.size.screen.height or
+ frame.target_config_modified != self.target_config_modified)
+ {
+ try frame.resize(
+ self.api,
+ self.size.screen.width,
+ self.size.screen.height,
+ );
+ frame.target_config_modified = self.target_config_modified;
+ }
+
+ // Upload images to the GPU as necessary.
+ _ = self.images.upload(self.alloc, &self.api);
+
+ // Upload the background image to the GPU as necessary.
+ try self.uploadBackgroundImage();
+
+ // Update per-frame custom shader uniforms.
+ try self.updateCustomShaderUniformsForFrame();
+
+ // Setup our frame data
+ try frame.uniforms.sync(&.{self.uniforms});
+ try frame.cells_bg.sync(self.cells.bg_cells);
+ const fg_count = try frame.cells.syncFromArrayLists(self.cells.fg_rows.lists);
+
+ // If our background image buffer has changed, sync it.
+ if (frame.bg_image_buffer_modified != self.bg_image_buffer_modified) {
+ try frame.bg_image_buffer.sync(&.{self.bg_image_buffer});
+
+ frame.bg_image_buffer_modified = self.bg_image_buffer_modified;
+ }
+
+ // If our font atlas changed, sync the texture data
+ texture: {
+ const modified = self.font_grid.atlas_grayscale.modified.load(.monotonic);
+ if (modified <= frame.grayscale_modified) break :texture;
+ self.font_grid.lock.lockShared();
+ defer self.font_grid.lock.unlockShared();
+ frame.grayscale_modified = self.font_grid.atlas_grayscale.modified.load(.monotonic);
+ try self.syncAtlasTexture(&self.font_grid.atlas_grayscale, &frame.grayscale);
+ }
+ texture: {
+ const modified = self.font_grid.atlas_color.modified.load(.monotonic);
+ if (modified <= frame.color_modified) break :texture;
+ self.font_grid.lock.lockShared();
+ defer self.font_grid.lock.unlockShared();
+ frame.color_modified = self.font_grid.atlas_color.modified.load(.monotonic);
+ try self.syncAtlasTexture(&self.font_grid.atlas_color, &frame.color);
+ }
+
+ // Get a frame context from the graphics API.
+ var frame_ctx = try self.api.beginFrame(self, &frame.target);
+ defer frame_ctx.complete(sync);
+
+ {
+ var pass = frame_ctx.renderPass(&.{.{
+ .target = if (frame.custom_shader_state) |state|
+ .{ .texture = state.back_texture }
+ else
+ .{ .target = frame.target },
+ .clear_color = .{ 0.0, 0.0, 0.0, 0.0 },
+ }});
+ defer pass.complete();
+
+ // First we draw our background image, if we have one.
+ // The bg image shader also draws the main bg color.
+ //
+ // Otherwise, if we don't have a background image, we
+ // draw the background color by itself in its own step.
+ //
+ // NOTE: We don't use the clear_color for this because that
+ // would require us to do color space conversion on the
+ // CPU-side. In the future when we have utilities for
+ // that we should remove this step and use clear_color.
+ if (self.bg_image) |img| switch (img) {
+ .ready => |texture| pass.step(.{
+ .pipeline = self.shaders.pipelines.bg_image,
+ .uniforms = frame.uniforms.buffer,
+ .buffers = &.{frame.bg_image_buffer.buffer},
+ .textures = &.{texture},
+ .draw = .{ .type = .triangle, .vertex_count = 3 },
+ }),
+ else => {},
+ } else {
+ pass.step(.{
+ .pipeline = self.shaders.pipelines.bg_color,
+ .uniforms = frame.uniforms.buffer,
+ .buffers = &.{ null, frame.cells_bg.buffer },
+ .draw = .{ .type = .triangle, .vertex_count = 3 },
+ });
+ }
+
+ // Then we draw any kitty images that need
+ // to be behind text AND cell backgrounds.
+ self.images.draw(
+ &self.api,
+ self.shaders.pipelines.image,
+ &pass,
+ .kitty_below_bg,
+ );
+
+ // Then we draw any opaque cell backgrounds.
+ pass.step(.{
+ .pipeline = self.shaders.pipelines.cell_bg,
+ .uniforms = frame.uniforms.buffer,
+ .buffers = &.{ null, frame.cells_bg.buffer },
+ .draw = .{ .type = .triangle, .vertex_count = 3 },
+ });
+
+ // Kitty images between cell backgrounds and text.
+ self.images.draw(
+ &self.api,
+ self.shaders.pipelines.image,
+ &pass,
+ .kitty_below_text,
+ );
+
+ // Text.
+ pass.step(.{
+ .pipeline = self.shaders.pipelines.cell_text,
+ .uniforms = frame.uniforms.buffer,
+ .buffers = &.{
+ frame.cells.buffer,
+ frame.cells_bg.buffer,
+ },
+ .textures = &.{
+ frame.grayscale,
+ frame.color,
+ },
+ .draw = .{
+ .type = .triangle_strip,
+ .vertex_count = 4,
+ .instance_count = fg_count,
+ },
+ });
+
+ // Kitty images in front of text.
+ self.images.draw(
+ &self.api,
+ self.shaders.pipelines.image,
+ &pass,
+ .kitty_above_text,
+ );
+
+ // Debug overlay. We do this before any custom shader state
+ // because our debug overlay is aligned with the grid.
+ if (self.overlay != null) self.images.draw(
+ &self.api,
+ self.shaders.pipelines.image,
+ &pass,
+ .overlay,
+ );
+ }
+
+ // If we have custom shaders, then we render them.
+ if (frame.custom_shader_state) |*state| {
+ // Sync our uniforms.
+ try state.uniforms.sync(&.{self.custom_shader_uniforms});
+
+ for (self.shaders.post_pipelines, 0..) |pipeline, i| {
+ defer state.swap();
+
+ var pass = frame_ctx.renderPass(&.{.{
+ .target = if (i < self.shaders.post_pipelines.len - 1)
+ .{ .texture = state.front_texture }
+ else
+ .{ .target = frame.target },
+ .clear_color = .{ 0.0, 0.0, 0.0, 0.0 },
+ }});
+ defer pass.complete();
+
+ pass.step(.{
+ .pipeline = pipeline,
+ .uniforms = state.uniforms.buffer,
+ .textures = &.{state.back_texture},
+ .samplers = &.{state.sampler},
+ .draw = .{
+ .type = .triangle,
+ .vertex_count = 3,
+ },
+ });
+ }
+ }
+ }
+
+ // Callback from the graphics API when a frame is completed.
+ pub fn frameCompleted(
+ self: *Self,
+ health: Health,
+ ) void {
+ // If our health value hasn't changed, then we do nothing. We don't
+ // do a cmpxchg here because strict atomicity isn't important.
+ if (self.health.load(.seq_cst) != health) {
+ self.health.store(health, .seq_cst);
+
+ // Our health value changed, so we notify the surface so that it
+ // can do something about it.
+ _ = self.surface_mailbox.push(.{
+ .renderer_health = health,
+ }, .{ .forever = {} });
+ }
+
+ // Always release our semaphore
+ self.swap_chain.releaseFrame();
+ }
+
+ /// Call this any time the background image path changes.
+ ///
+ /// Caller must hold the draw mutex.
+ fn prepBackgroundImage(self: *Self) !void {
+ // Then we try to load the background image if we have a path.
+ if (self.config.bg_image) |p| load_background: {
+ const path = switch (p) {
+ .required, .optional => |slice| slice,
+ };
+
+ // Open the file
+ var file = std.fs.openFileAbsolute(path, .{}) catch |err| {
+ log.warn(
+ "error opening background image file \"{s}\": {}",
+ .{ path, err },
+ );
+ break :load_background;
+ };
+ defer file.close();
+
+ // Read it
+ const contents = file.readToEndAlloc(
+ self.alloc,
+ std.math.maxInt(u32), // Max size of 4 GiB, for now.
+ ) catch |err| {
+ log.warn(
+ "error reading background image file \"{s}\": {}",
+ .{ path, err },
+ );
+ break :load_background;
+ };
+ defer self.alloc.free(contents);
+
+ // Figure out what type it probably is.
+ const file_type = switch (FileType.detect(contents)) {
+ .unknown => FileType.guessFromExtension(
+ std.fs.path.extension(path),
+ ),
+ else => |t| t,
+ };
+
+ // Decode it if we know how.
+ const image_data = switch (file_type) {
+ .png => try wuffs.png.decode(self.alloc, contents),
+ .jpeg => try wuffs.jpeg.decode(self.alloc, contents),
+ .unknown => {
+ log.warn(
+ "Cannot determine file type for background image file \"{s}\"!",
+ .{path},
+ );
+ break :load_background;
+ },
+ else => |f| {
+ log.warn(
+ "Unsupported file type {} for background image file \"{s}\"!",
+ .{ f, path },
+ );
+ break :load_background;
+ },
+ };
+
+ const image: imagepkg.Image = .{
+ .pending = .{
+ .width = image_data.width,
+ .height = image_data.height,
+ .pixel_format = .rgba,
+ .data = image_data.data.ptr,
+ },
+ };
+
+ // If we have an existing background image, replace it.
+ // Otherwise, set this as our background image directly.
+ if (self.bg_image) |*img| {
+ img.markForReplace(self.alloc, image);
+ } else {
+ self.bg_image = image;
+ }
+ } else {
+ // If we don't have a background image path, mark our
+ // background image for unload if we currently have one.
+ if (self.bg_image) |*img| img.markForUnload();
+ }
+ }
+
+ fn uploadBackgroundImage(self: *Self) !void {
+ // Make sure our bg image is uploaded if it needs to be.
+ if (self.bg_image) |*bg| {
+ if (bg.isUnloading()) {
+ bg.deinit(self.alloc);
+ self.bg_image = null;
+ return;
+ }
+ if (bg.isPending()) try bg.upload(self.alloc, &self.api);
+ }
+ }
+
+ /// Update the configuration.
+ pub fn changeConfig(self: *Self, config: *DerivedConfig) !void {
+ self.draw_mutex.lock();
+ defer self.draw_mutex.unlock();
+
+ // 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 reused 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 minimum contrast
+ self.uniforms.min_contrast = config.min_contrast;
+
+ // Set our new color space and blending
+ self.uniforms.bools.use_display_p3 = config.colorspace == .@"display-p3";
+ self.uniforms.bools.use_linear_blending = config.blending.isLinear();
+ self.uniforms.bools.use_linear_correction = config.blending == .@"linear-corrected";
+
+ const bg_image_config_changed =
+ self.config.bg_image_fit != config.bg_image_fit or
+ self.config.bg_image_position != config.bg_image_position or
+ self.config.bg_image_repeat != config.bg_image_repeat or
+ self.config.bg_image_opacity != config.bg_image_opacity;
+
+ const bg_image_changed =
+ if (self.config.bg_image) |old|
+ if (config.bg_image) |new|
+ !old.equal(new)
+ else
+ true
+ else
+ config.bg_image != null;
+
+ const old_blending = self.config.blending;
+ const custom_shaders_changed = !self.config.custom_shaders.equal(config.custom_shaders);
+
+ self.config.deinit();
+ self.config = config.*;
+
+ // If our background image path changed, prepare the new bg image.
+ if (bg_image_changed) try self.prepBackgroundImage();
+
+ // If our background image config changed, update the vertex buffer.
+ if (bg_image_config_changed) self.updateBgImageBuffer();
+
+ // Reset our viewport to force a rebuild, in case of a font change.
+ self.markDirty();
+
+ const blending_changed = old_blending != config.blending;
+
+ if (blending_changed) {
+ // We update our API's blending mode.
+ self.api.blending = config.blending;
+ // And indicate that we need to reinitialize our shaders.
+ self.reinitialize_shaders = true;
+ // And indicate that our swap chain targets need to
+ // be re-created to account for the new blending mode.
+ self.target_config_modified +%= 1;
+ }
+
+ if (custom_shaders_changed) {
+ self.reinitialize_shaders = true;
+ }
+ }
+
+ /// Resize the screen.
+ pub fn setScreenSize(
+ self: *Self,
+ size: renderer.Size,
+ ) void {
+ self.draw_mutex.lock();
+ defer self.draw_mutex.unlock();
+
+ // We only actually need the padding from this,
+ // everything else is derived elsewhere.
+ self.size.padding = size.padding;
+
+ self.updateScreenSizeUniforms();
+
+ log.debug("screen size size={}", .{size});
+ }
+
+ /// Update uniforms that are based on the screen size.
+ ///
+ /// Caller must hold the draw mutex.
+ fn updateScreenSizeUniforms(self: *Self) void {
+ const terminal_size = self.size.terminal();
+
+ // Blank space around the grid.
+ const blank: renderer.Padding = self.size.screen.blankPadding(
+ self.size.padding,
+ .{
+ .columns = self.cells.size.columns,
+ .rows = self.cells.size.rows,
+ },
+ .{
+ .width = self.grid_metrics.cell_width,
+ .height = self.grid_metrics.cell_height,
+ },
+ ).add(self.size.padding);
+
+ // Setup our uniforms
+ self.uniforms.projection_matrix = 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)),
+ );
+ self.uniforms.grid_padding = .{
+ @floatFromInt(blank.top),
+ @floatFromInt(blank.right),
+ @floatFromInt(blank.bottom),
+ @floatFromInt(blank.left),
+ };
+ self.uniforms.screen_size = .{
+ @floatFromInt(self.size.screen.width),
+ @floatFromInt(self.size.screen.height),
+ };
+ }
+
+ /// Update the background image vertex buffer (CPU-side).
+ ///
+ /// This should be called if and when configs change that
+ /// could affect the background image.
+ ///
+ /// Caller must hold the draw mutex.
+ fn updateBgImageBuffer(self: *Self) void {
+ self.bg_image_buffer = .{
+ .opacity = self.config.bg_image_opacity,
+ .info = .{
+ .position = switch (self.config.bg_image_position) {
+ .@"top-left" => .tl,
+ .@"top-center" => .tc,
+ .@"top-right" => .tr,
+ .@"center-left" => .ml,
+ .@"center-center", .center => .mc,
+ .@"center-right" => .mr,
+ .@"bottom-left" => .bl,
+ .@"bottom-center" => .bc,
+ .@"bottom-right" => .br,
+ },
+ .fit = switch (self.config.bg_image_fit) {
+ .contain => .contain,
+ .cover => .cover,
+ .stretch => .stretch,
+ .none => .none,
+ },
+ .repeat = self.config.bg_image_repeat,
+ },
+ };
+ // Signal that the buffer was modified.
+ self.bg_image_buffer_modified +%= 1;
+ }
+
+ /// Update custom shader uniforms that depend on terminal state.
+ ///
+ /// This should be called in `updateFrame` when terminal state changes.
+ fn updateCustomShaderUniformsFromState(self: *Self) void {
+ // We only need to do this if we have custom shaders.
+ if (!self.has_custom_shaders) return;
+
+ // Only update when terminal state is dirty.
+ if (self.terminal_state.dirty == .false) return;
+
+ const uniforms: *shadertoy.Uniforms = &self.custom_shader_uniforms;
+ const colors: *const terminal.RenderState.Colors = &self.terminal_state.colors;
+
+ // 256-color palette
+ for (colors.palette, 0..) |color, i| {
+ uniforms.palette[i] = .{
+ @as(f32, @floatFromInt(color.r)) / 255.0,
+ @as(f32, @floatFromInt(color.g)) / 255.0,
+ @as(f32, @floatFromInt(color.b)) / 255.0,
+ 1.0,
+ };
+ }
+
+ // Background color
+ uniforms.background_color = .{
+ @as(f32, @floatFromInt(colors.background.r)) / 255.0,
+ @as(f32, @floatFromInt(colors.background.g)) / 255.0,
+ @as(f32, @floatFromInt(colors.background.b)) / 255.0,
+ 1.0,
+ };
+
+ // Foreground color
+ uniforms.foreground_color = .{
+ @as(f32, @floatFromInt(colors.foreground.r)) / 255.0,
+ @as(f32, @floatFromInt(colors.foreground.g)) / 255.0,
+ @as(f32, @floatFromInt(colors.foreground.b)) / 255.0,
+ 1.0,
+ };
+
+ // Cursor color
+ if (colors.cursor) |cursor_color| {
+ uniforms.cursor_color = .{
+ @as(f32, @floatFromInt(cursor_color.r)) / 255.0,
+ @as(f32, @floatFromInt(cursor_color.g)) / 255.0,
+ @as(f32, @floatFromInt(cursor_color.b)) / 255.0,
+ 1.0,
+ };
+ }
+
+ // NOTE: the following could be optimized to follow a change in
+ // config for a slight optimization however this is only 12 bytes
+ // each being updated and likely isn't a cause for concern
+
+ // Cursor text color
+ if (self.config.cursor_text) |cursor_text| {
+ uniforms.cursor_text = .{
+ @as(f32, @floatFromInt(cursor_text.color.r)) / 255.0,
+ @as(f32, @floatFromInt(cursor_text.color.g)) / 255.0,
+ @as(f32, @floatFromInt(cursor_text.color.b)) / 255.0,
+ 1.0,
+ };
+ }
+
+ // Selection background color
+ if (self.config.selection_background) |selection_bg| {
+ uniforms.selection_background_color = .{
+ @as(f32, @floatFromInt(selection_bg.color.r)) / 255.0,
+ @as(f32, @floatFromInt(selection_bg.color.g)) / 255.0,
+ @as(f32, @floatFromInt(selection_bg.color.b)) / 255.0,
+ 1.0,
+ };
+ }
+
+ // Selection foreground color
+ if (self.config.selection_foreground) |selection_fg| {
+ uniforms.selection_foreground_color = .{
+ @as(f32, @floatFromInt(selection_fg.color.r)) / 255.0,
+ @as(f32, @floatFromInt(selection_fg.color.g)) / 255.0,
+ @as(f32, @floatFromInt(selection_fg.color.b)) / 255.0,
+ 1.0,
+ };
+ }
+
+ // Cursor visibility
+ uniforms.cursor_visible = @intFromBool(self.terminal_state.cursor.visible);
+
+ // Cursor style
+ const cursor_style: renderer.CursorStyle = .fromTerminal(self.terminal_state.cursor.visual_style);
+ uniforms.previous_cursor_style = uniforms.current_cursor_style;
+ uniforms.current_cursor_style = @as(i32, @intFromEnum(cursor_style));
+ }
+
+ /// Update per-frame custom shader uniforms.
+ ///
+ /// This should be called exactly once per frame, inside `drawFrame`.
+ fn updateCustomShaderUniformsForFrame(self: *Self) !void {
+ // We only need to do this if we have custom shaders.
+ if (!self.has_custom_shaders) return;
+
+ const uniforms: *shadertoy.Uniforms = &self.custom_shader_uniforms;
+
+ const now = try std.time.Instant.now();
+ defer self.last_frame_time = now;
+ const first_frame_time = self.first_frame_time orelse t: {
+ self.first_frame_time = now;
+ break :t now;
+ };
+ const last_frame_time = self.last_frame_time orelse now;
+
+ const since_ns: f32 = @floatFromInt(now.since(first_frame_time));
+ uniforms.time = since_ns / std.time.ns_per_s;
+
+ const delta_ns: f32 = @floatFromInt(now.since(last_frame_time));
+ uniforms.time_delta = delta_ns / std.time.ns_per_s;
+
+ uniforms.frame += 1;
+
+ const screen = self.size.screen;
+ const padding = self.size.padding;
+ const cell = self.size.cell;
+
+ uniforms.resolution = .{
+ @floatFromInt(screen.width),
+ @floatFromInt(screen.height),
+ 1,
+ };
+ uniforms.channel_resolution[0] = .{
+ @floatFromInt(screen.width),
+ @floatFromInt(screen.height),
+ 1,
+ 0,
+ };
+
+ if (self.cells.getCursorGlyph()) |cursor| {
+ const cursor_width: f32 = @floatFromInt(cursor.glyph_size[0]);
+ const cursor_height: f32 = @floatFromInt(cursor.glyph_size[1]);
+
+ // Left edge of the cell the cursor is in.
+ var pixel_x: f32 = @floatFromInt(
+ cursor.grid_pos[0] * cell.width + padding.left,
+ );
+ // Top edge, relative to the top of the
+ // screen, of the cell the cursor is in.
+ var pixel_y: f32 = @floatFromInt(
+ cursor.grid_pos[1] * cell.height + padding.top,
+ );
+
+ // If +Y is up in our shaders, we need to flip the coordinate
+ // so that it's instead the top edge of the cell relative to
+ // the *bottom* of the screen.
+ if (!GraphicsAPI.custom_shader_y_is_down) {
+ pixel_y = @as(f32, @floatFromInt(screen.height)) - pixel_y;
+ }
+
+ // Add the X bearing to get the -X (left) edge of the cursor.
+ pixel_x += @floatFromInt(cursor.bearings[0]);
+
+ // How we deal with the Y bearing depends on which direction
+ // is "up", since we want our final `pixel_y` value to be the
+ // +Y edge of the cursor.
+ if (GraphicsAPI.custom_shader_y_is_down) {
+ // As a reminder, the Y bearing is the distance from the
+ // bottom of the cell to the top of the glyph, so to get
+ // the +Y edge we need to add the cell height, subtract
+ // the Y bearing, and add the glyph height to get the +Y
+ // (bottom) edge of the cursor.
+ pixel_y += @floatFromInt(cell.height);
+ pixel_y -= @floatFromInt(cursor.bearings[1]);
+ pixel_y += @floatFromInt(cursor.glyph_size[1]);
+ } else {
+ // If the Y direction is reversed though, we instead want
+ // the *top* edge of the cursor, which means we just need
+ // to subtract the cell height and add the Y bearing.
+ pixel_y -= @floatFromInt(cell.height);
+ pixel_y += @floatFromInt(cursor.bearings[1]);
+ }
+
+ const new_cursor: [4]f32 = .{
+ pixel_x,
+ pixel_y,
+ cursor_width,
+ cursor_height,
+ };
+ const cursor_color: [4]f32 = .{
+ @as(f32, @floatFromInt(cursor.color[0])) / 255.0,
+ @as(f32, @floatFromInt(cursor.color[1])) / 255.0,
+ @as(f32, @floatFromInt(cursor.color[2])) / 255.0,
+ @as(f32, @floatFromInt(cursor.color[3])) / 255.0,
+ };
+
+ const cursor_changed: bool =
+ !std.meta.eql(new_cursor, uniforms.current_cursor) or
+ !std.meta.eql(cursor_color, uniforms.current_cursor_color);
+
+ if (cursor_changed) {
+ uniforms.previous_cursor = uniforms.current_cursor;
+ uniforms.previous_cursor_color = uniforms.current_cursor_color;
+ uniforms.current_cursor = new_cursor;
+ uniforms.current_cursor_color = cursor_color;
+ uniforms.cursor_change_time = uniforms.time;
+ }
+ }
+
+ // Update focus uniforms
+ uniforms.focus = @intFromBool(self.focused);
+
+ // If we need to update the time our focus state changed
+ // then update it to our current frame time. This may not be
+ // exactly correct since it is frame time, not exact focus
+ // time, but focus time on its own isn't exactly correct anyways
+ // since it comes async from a message.
+ if (self.custom_shader_focused_changed and self.focused) {
+ uniforms.time_focus = uniforms.time;
+ self.custom_shader_focused_changed = false;
+ }
+ }
+
+ /// Build the overlay as configured. Returns null if there is no
+ /// overlay currently configured.
+ fn rebuildOverlay(
+ self: *Self,
+ features: []const Overlay.Feature,
+ ) Overlay.InitError!void {
+ // const start = std.time.Instant.now() catch unreachable;
+ // const start_micro = std.time.microTimestamp();
+ // defer {
+ // const end = std.time.Instant.now() catch unreachable;
+ // log.warn(
+ // "[rebuildOverlay time] start_micro={} duration={}ns",
+ // .{ start_micro, end.since(start) / std.time.ns_per_us },
+ // );
+ // }
+
+ const alloc = self.alloc;
+
+ // If we have no features enabled, don't build an overlay.
+ // If we had a previous overlay, deallocate it.
+ if (features.len == 0) {
+ if (self.overlay) |*old| {
+ old.deinit(alloc);
+ self.overlay = null;
+ }
+
+ return;
+ }
+
+ // If we had a previous overlay, clear it. Otherwise, init.
+ const overlay: *Overlay = overlay: {
+ if (self.overlay) |*v| existing: {
+ // Verify that our overlay size matches our screen
+ // size as we know it now. If not, deinit and reinit.
+ // Note: these intCasts are always safe because z2d
+ // stores as i32 but we always init with a u32.
+ const width: u32 = @intCast(v.surface.getWidth());
+ const height: u32 = @intCast(v.surface.getHeight());
+ const term_size = self.size.terminal();
+ if (width != term_size.width or
+ height != term_size.height) break :existing;
+
+ // We also depend on cell size.
+ if (v.cell_size.width != self.size.cell.width or
+ v.cell_size.height != self.size.cell.height) break :existing;
+
+ // Everything matches, so we can just reset the surface
+ // and redraw.
+ v.reset();
+ break :overlay v;
+ }
+
+ // If we reached this point we want to reset our overlay.
+ if (self.overlay) |*v| {
+ v.deinit(alloc);
+ self.overlay = null;
+ }
+
+ assert(self.overlay == null);
+ const new: Overlay = try .init(alloc, self.size);
+ self.overlay = new;
+ break :overlay &self.overlay.?;
+ };
+ overlay.applyFeatures(
+ alloc,
+ &self.terminal_state,
+ features,
+ );
+ }
+
+ const PreeditRange = struct {
+ y: terminal.size.CellCountInt,
+ x: [2]terminal.size.CellCountInt,
+ cp_offset: usize,
+ };
+
+ /// Convert the terminal state to GPU cells stored in CPU memory. These
+ /// are then synced to the GPU in the next frame. This only updates CPU
+ /// memory and doesn't touch the GPU.
+ ///
+ /// This requires the draw mutex.
+ ///
+ /// Dirty state on terminal state won't be reset by this.
+ fn rebuildCells(
+ self: *Self,
+ preedit: ?renderer.State.Preedit,
+ cursor_style_: ?renderer.CursorStyle,
+ links: *const terminal.RenderState.CellSet,
+ ) Allocator.Error!void {
+ const state: *terminal.RenderState = &self.terminal_state;
+
+ // const start = try std.time.Instant.now();
+ // const start_micro = std.time.microTimestamp();
+ // defer {
+ // const end = std.time.Instant.now() catch unreachable;
+ // // "[rebuildCells time] <START us>\t<TIME_TAKEN us>"
+ // std.log.warn("[rebuildCells time] {}\t{}", .{start_micro, end.since(start) / std.time.ns_per_us});
+ // }
+
+ const grid_size_diff =
+ self.cells.size.rows != state.rows or
+ self.cells.size.columns != state.cols;
+
+ if (grid_size_diff) {
+ var new_size = self.cells.size;
+ new_size.rows = state.rows;
+ new_size.columns = state.cols;
+ try self.cells.resize(self.alloc, new_size);
+
+ // Update our uniforms accordingly, otherwise
+ // our background cells will be out of place.
+ self.uniforms.grid_size = .{ new_size.columns, new_size.rows };
+ }
+
+ const rebuild = state.dirty == .full or grid_size_diff;
+ if (rebuild) {
+ // If we are doing a full rebuild, then we clear the entire cell buffer.
+ self.cells.reset();
+
+ // We also reset our padding extension depending on the screen type
+ switch (self.config.padding_color) {
+ .background => {},
+
+ // For extension, assume we are extending in all directions.
+ // For "extend" this may be disabled due to heuristics below.
+ .extend, .@"extend-always" => {
+ self.uniforms.padding_extend = .{
+ .up = true,
+ .down = true,
+ .left = true,
+ .right = true,
+ };
+ },
+ }
+ }
+
+ // From this point on we never fail. We produce some kind of
+ // working terminal state, even if incorrect.
+ errdefer comptime unreachable;
+
+ // Get our row data from our state
+ const row_data = state.row_data.slice();
+ const row_raws = row_data.items(.raw);
+ const row_cells = row_data.items(.cells);
+ const row_dirty = row_data.items(.dirty);
+ const row_selection = row_data.items(.selection);
+ const row_highlights = row_data.items(.highlights);
+
+ // If our cell contents buffer is shorter than the screen viewport,
+ // we render the rows that fit, starting from the bottom. If instead
+ // the viewport is shorter than the cell contents buffer, we align
+ // the top of the viewport with the top of the contents buffer.
+ const row_len: usize = @min(
+ state.rows,
+ self.cells.size.rows,
+ );
+
+ // 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: ?PreeditRange = if (preedit) |preedit_v| preedit: {
+ // We base the preedit on the position of the cursor in the
+ // viewport. If the cursor isn't visible in the viewport we
+ // don't show it.
+ const cursor_vp = state.cursor.viewport orelse
+ break :preedit null;
+
+ // If our preedit row isn't dirty then we don't need the
+ // preedit range. This also avoids an issue later where we
+ // unconditionally add preedit cells when this is set.
+ if (!rebuild and !row_dirty[cursor_vp.y]) break :preedit null;
+
+ const range = preedit_v.range(
+ cursor_vp.x,
+ state.cols - 1,
+ );
+ break :preedit .{
+ .y = @intCast(cursor_vp.y),
+ .x = .{ range.start, range.end },
+ .cp_offset = range.cp_offset,
+ };
+ } else null;
+
+ for (
+ 0..,
+ row_raws[0..row_len],
+ row_cells[0..row_len],
+ row_dirty[0..row_len],
+ row_selection[0..row_len],
+ row_highlights[0..row_len],
+ ) |y_usize, row, *cells, *dirty, selection, *highlights| {
+ const y: terminal.size.CellCountInt = @intCast(y_usize);
+
+ if (!rebuild) {
+ // Only rebuild if we are doing a full rebuild or this row is dirty.
+ if (!dirty.*) continue;
+
+ // Clear the cells if the row is dirty
+ self.cells.clear(y);
+ }
+
+ // Unmark the dirty state in our render state.
+ dirty.* = false;
+
+ self.rebuildRow(
+ y,
+ row,
+ cells,
+ preedit_range,
+ selection,
+ highlights,
+ links,
+ ) catch |err| {
+ // This should never happen except under exceptional
+ // scenarios. In this case, we don't want to corrupt
+ // our render state so just clear this row and keep
+ // trying to finish it out.
+ log.warn("error building row y={} err={}", .{ y, err });
+ self.cells.clear(y);
+ };
+ }
+
+ // Setup our cursor rendering information.
+ cursor: {
+ // Clear our cursor by default.
+ self.cells.setCursor(null, null);
+ self.uniforms.cursor_pos = .{
+ std.math.maxInt(u16),
+ std.math.maxInt(u16),
+ };
+
+ // If the cursor isn't visible on the viewport, don't show
+ // a cursor. Otherwise, get our cursor cell, because we may
+ // need it for styling.
+ const cursor_vp = state.cursor.viewport orelse break :cursor;
+ const cursor_style: terminal.Style = cursor_style: {
+ const cells = state.row_data.items(.cells);
+ const cell = cells[cursor_vp.y].get(cursor_vp.x);
+ break :cursor_style if (cell.raw.hasStyling())
+ cell.style
+ else
+ .{};
+ };
+
+ // If we have preedit text, we don't setup a cursor
+ if (preedit != null) break :cursor;
+
+ // If there isn't a cursor visual style requested then
+ // we don't render a cursor.
+ const style = cursor_style_ orelse break :cursor;
+
+ // Determine the cursor color.
+ const cursor_color = cursor_color: {
+ // If an explicit cursor color was set by OSC 12, use that.
+ if (state.colors.cursor) |v| break :cursor_color v;
+
+ // Use our configured color if specified
+ if (self.config.cursor_color) |v| switch (v) {
+ .color => |color| break :cursor_color color.toTerminalRGB(),
+
+ inline .@"cell-foreground",
+ .@"cell-background",
+ => |_, tag| {
+ const fg_style = cursor_style.fg(.{
+ .default = state.colors.foreground,
+ .palette = &state.colors.palette,
+ .bold = self.config.bold_color,
+ });
+ const bg_style = cursor_style.bg(
+ &state.cursor.cell,
+ &state.colors.palette,
+ ) orelse state.colors.background;
+
+ break :cursor_color switch (tag) {
+ .color => unreachable,
+ .@"cell-foreground" => if (cursor_style.flags.inverse)
+ bg_style
+ else
+ fg_style,
+ .@"cell-background" => if (cursor_style.flags.inverse)
+ fg_style
+ else
+ bg_style,
+ };
+ },
+ };
+
+ break :cursor_color state.colors.foreground;
+ };
+
+ self.addCursor(
+ &state.cursor,
+ style,
+ cursor_color,
+ );
+
+ // If the cursor is visible then we set our uniforms.
+ if (style == .block) {
+ const wide = state.cursor.cell.wide;
+
+ self.uniforms.cursor_pos = .{
+ // If we are a spacer tail of a wide cell, our cursor needs
+ // to move back one cell. The saturate is to ensure we don't
+ // overflow but this shouldn't happen with well-formed input.
+ switch (wide) {
+ .narrow, .spacer_head, .wide => cursor_vp.x,
+ .spacer_tail => cursor_vp.x -| 1,
+ },
+ @intCast(cursor_vp.y),
+ };
+
+ self.uniforms.bools.cursor_wide = switch (wide) {
+ .narrow, .spacer_head => false,
+ .wide, .spacer_tail => true,
+ };
+
+ const uniform_color = if (self.config.cursor_text) |txt| blk: {
+ // If cursor-text is set, then compute the correct color.
+ // Otherwise, use the background color.
+ if (txt == .color) {
+ // Use the color set by cursor-text, if any.
+ break :blk txt.color.toTerminalRGB();
+ }
+
+ const fg_style = cursor_style.fg(.{
+ .default = state.colors.foreground,
+ .palette = &state.colors.palette,
+ .bold = self.config.bold_color,
+ });
+ const bg_style = cursor_style.bg(
+ &state.cursor.cell,
+ &state.colors.palette,
+ ) orelse state.colors.background;
+
+ break :blk switch (txt) {
+ // If the cell is reversed, use the opposite cell color instead.
+ .@"cell-foreground" => if (cursor_style.flags.inverse)
+ bg_style
+ else
+ fg_style,
+ .@"cell-background" => if (cursor_style.flags.inverse)
+ fg_style
+ else
+ bg_style,
+ else => unreachable,
+ };
+ } else state.colors.background;
+
+ self.uniforms.cursor_color = .{
+ uniform_color.r,
+ uniform_color.g,
+ uniform_color.b,
+ 255,
+ };
+ }
+ }
+
+ // Setup our preedit text.
+ if (preedit) |preedit_v| preedit: {
+ const range = preedit_range orelse break :preedit;
+ var x = range.x[0];
+ for (preedit_v.codepoints[range.cp_offset..]) |cp| {
+ self.addPreeditCell(
+ cp,
+ .{ .x = x, .y = range.y },
+ state.colors.foreground,
+ ) 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;
+ }
+ }
+
+ // Update that our cells rebuilt
+ self.cells_rebuilt = true;
+
+ // Log some things
+ // log.debug("rebuildCells complete cached_runs={}", .{
+ // self.font_shaper_cache.count(),
+ // });
+ }
+
+ fn rebuildRow(
+ self: *Self,
+ y: terminal.size.CellCountInt,
+ row: terminal.page.Row,
+ cells: *std.MultiArrayList(terminal.RenderState.Cell),
+ preedit_range: ?PreeditRange,
+ selection: ?[2]terminal.size.CellCountInt,
+ highlights: *const std.ArrayList(terminal.RenderState.Highlight),
+ links: *const terminal.RenderState.CellSet,
+ ) !void {
+ const state = &self.terminal_state;
+
+ // If our viewport is wider than our cell contents buffer,
+ // we still only process cells up to the width of the buffer.
+ const cells_slice = cells.slice();
+ const cells_len = @min(cells_slice.len, self.cells.size.columns);
+ const cells_raw = cells_slice.items(.raw);
+ const cells_style = cells_slice.items(.style);
+
+ // 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.uniforms.padding_extend.up = !rowNeverExtendBg(
+ row,
+ cells_raw,
+ cells_style,
+ &state.colors.palette,
+ state.colors.background,
+ );
+ } else if (y == self.cells.size.rows - 1) {
+ self.uniforms.padding_extend.down = !rowNeverExtendBg(
+ row,
+ cells_raw,
+ cells_style,
+ &state.colors.palette,
+ state.colors.background,
+ );
+ },
+ }
+
+ // Iterator of runs for shaping.
+ var run_iter_opts: font.shape.RunOptions = .{
+ .grid = self.font_grid,
+ .cells = cells_slice,
+ .selection = if (selection) |s| s else null,
+
+ // We want to do font shaping as long as the cursor is
+ // visible on this viewport.
+ .cursor_x = cursor_x: {
+ const vp = state.cursor.viewport orelse break :cursor_x null;
+ if (vp.y != y) break :cursor_x null;
+ break :cursor_x vp.x;
+ },
+ };
+ run_iter_opts.applyBreakConfig(self.config.font_shaping_break);
+ var run_iter = self.font_shaper.runIterator(run_iter_opts);
+ 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;
+
+ for (
+ 0..,
+ cells_raw[0..cells_len],
+ cells_style[0..cells_len],
+ ) |x, *cell, *managed_style| {
+ // 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 new_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,
+ new_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 new_cells;
+ };
+
+ // Advance our index until we reach or pass
+ // our current x position in the shaper cells.
+ const shaper_cells_unwrapped = shaper_cells.?;
+ while (run.offset + shaper_cells_unwrapped[shaper_cells_i].x < x) {
+ shaper_cells_i += 1;
+ }
+ }
+
+ const wide = cell.wide;
+ const style: terminal.Style = if (cell.hasStyling())
+ managed_style.*
+ else
+ .{};
+
+ // True if this cell is selected
+ const selected: enum {
+ false,
+ selection,
+ search,
+ search_selected,
+ } = selected: {
+ // Order below matters for precedence.
+
+ // Selection should take the highest precedence.
+ const x_compare = if (wide == .spacer_tail)
+ x -| 1
+ else
+ x;
+ if (selection) |sel| {
+ if (x_compare >= sel[0] and
+ x_compare <= sel[1]) break :selected .selection;
+ }
+
+ // If we're highlighted, then we're selected. In the
+ // future we want to use a different style for this
+ // but this to get started.
+ for (highlights.items) |hl| {
+ if (x_compare >= hl.range[0] and
+ x_compare <= hl.range[1])
+ {
+ const tag: HighlightTag = @enumFromInt(hl.tag);
+ break :selected switch (tag) {
+ .search_match => .search,
+ .search_match_selected => .search_selected,
+ };
+ }
+ }
+
+ break :selected .false;
+ };
+
+ // The `_style` suffixed values are the colors based on
+ // the cell style (SGR), before applying any additional
+ // configuration, inversions, selections, etc.
+ const bg_style = style.bg(
+ cell,
+ &state.colors.palette,
+ );
+ const fg_style = style.fg(.{
+ .default = state.colors.foreground,
+ .palette = &state.colors.palette,
+ .bold = self.config.bold_color,
+ });
+
+ // The final background color for the cell.
+ const bg = switch (selected) {
+ // If we have an explicit selection background color
+ // specified in the config, use that.
+ //
+ // If no configuration, then our selection background
+ // is our foreground color.
+ .selection => if (self.config.selection_background) |v| switch (v) {
+ .color => |color| color.toTerminalRGB(),
+ .@"cell-foreground" => if (style.flags.inverse) bg_style else fg_style,
+ .@"cell-background" => if (style.flags.inverse) fg_style else bg_style,
+ } else state.colors.foreground,
+
+ .search => switch (self.config.search_background) {
+ .color => |color| color.toTerminalRGB(),
+ .@"cell-foreground" => if (style.flags.inverse) bg_style else fg_style,
+ .@"cell-background" => if (style.flags.inverse) fg_style else bg_style,
+ },
+
+ .search_selected => switch (self.config.search_selected_background) {
+ .color => |color| color.toTerminalRGB(),
+ .@"cell-foreground" => if (style.flags.inverse) bg_style else fg_style,
+ .@"cell-background" => if (style.flags.inverse) fg_style else bg_style,
+ },
+
+ // Not selected
+ .false => 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: {
+ // Our happy-path non-selection background color
+ // is our style or our configured defaults.
+ const final_bg = bg_style orelse state.colors.background;
+
+ // Whether we need to use the bg color as our fg color:
+ // - Cell is selected, inverted, and set to cell-foreground
+ // - Cell is selected, not inverted, and set to cell-background
+ // - Cell is inverted and not selected
+ break :fg switch (selected) {
+ .selection => if (self.config.selection_foreground) |v| switch (v) {
+ .color => |color| color.toTerminalRGB(),
+ .@"cell-foreground" => if (style.flags.inverse) final_bg else fg_style,
+ .@"cell-background" => if (style.flags.inverse) fg_style else final_bg,
+ } else state.colors.background,
+
+ .search => switch (self.config.search_foreground) {
+ .color => |color| color.toTerminalRGB(),
+ .@"cell-foreground" => if (style.flags.inverse) final_bg else fg_style,
+ .@"cell-background" => if (style.flags.inverse) fg_style else final_bg,
+ },
+
+ .search_selected => switch (self.config.search_selected_foreground) {
+ .color => |color| color.toTerminalRGB(),
+ .@"cell-foreground" => if (style.flags.inverse) final_bg else fg_style,
+ .@"cell-background" => if (style.flags.inverse) fg_style else final_bg,
+ },
+
+ .false => if (style.flags.inverse)
+ final_bg
+ else
+ fg_style,
+ };
+ };
+
+ // Foreground alpha for this cell.
+ const alpha: u8 = if (style.flags.faint) self.config.faint_opacity else 255;
+
+ // Set the cell's background color.
+ {
+ const rgb = bg orelse state.colors.background;
+
+ // 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;
+
+ // Cells that are selected should be fully opaque.
+ if (selected != .false) break :bg_alpha default;
+
+ // Cells that are reversed should be fully opaque.
+ if (style.flags.inverse) break :bg_alpha default;
+
+ // If the user requested to have opacity on all cells, apply it.
+ if (self.config.background_opacity_cells and bg_style != null) {
+ var opacity: f64 = @floatFromInt(default);
+ opacity *= self.config.background_opacity;
+ break :bg_alpha @intFromFloat(opacity);
+ }
+
+ // Cells that have an explicit bg color should be fully opaque.
+ if (bg_style != null) break :bg_alpha default;
+
+ // Otherwise, we won't draw the bg for this cell,
+ // we'll let the already-drawn background color
+ // show through.
+ break :bg_alpha 0;
+ };
+
+ self.cells.bgCell(y, x).* = .{
+ rgb.r, rgb.g, rgb.b, bg_alpha,
+ };
+ }
+
+ // 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 = underline: {
+ if (links.contains(.{
+ .x = @intCast(x),
+ .y = @intCast(y),
+ })) {
+ break :underline if (style.flags.underline == .single)
+ .double
+ else
+ .single;
+ }
+ break :underline 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(&state.colors.palette) orelse fg,
+ alpha,
+ ) 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) 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 new_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,
+ new_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 new_cells;
+ };
+
+ const shaped_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 (shaped_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(run.offset + shaped_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 < shaped_cells.len and
+ run.offset + shaped_cells[shaper_cells_i].x == x) : ({
+ shaper_cells_i += 1;
+ }) {
+ self.addGlyph(
+ @intCast(x),
+ @intCast(y),
+ state.cols,
+ cells_raw,
+ shaped_cells[shaper_cells_i],
+ shaper_run.?,
+ fg,
+ alpha,
+ ) 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,
+ ) catch |err| {
+ log.warn(
+ "error adding strikethrough to cell, will be invalid x={} y={}, err={}",
+ .{ x, y, err },
+ );
+ };
+ }
+ }
+
+ /// Add an underline decoration to the specified cell
+ fn addUnderline(
+ self: *Self,
+ x: terminal.size.CellCountInt,
+ y: terminal.size.CellCountInt,
+ style: terminal.Attribute.Underline,
+ color: terminal.color.RGB,
+ alpha: 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.add(self.alloc, .underline, .{
+ .atlas = .grayscale,
+ .grid_pos = .{ @intCast(x), @intCast(y) },
+ .color = .{ color.r, color.g, color.b, alpha },
+ .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
+ .glyph_size = .{ render.glyph.width, render.glyph.height },
+ .bearings = .{
+ @intCast(render.glyph.offset_x),
+ @intCast(render.glyph.offset_y),
+ },
+ });
+ }
+
+ /// Add a overline decoration to the specified cell
+ fn addOverline(
+ self: *Self,
+ x: terminal.size.CellCountInt,
+ y: terminal.size.CellCountInt,
+ color: terminal.color.RGB,
+ alpha: 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.add(self.alloc, .overline, .{
+ .atlas = .grayscale,
+ .grid_pos = .{ @intCast(x), @intCast(y) },
+ .color = .{ color.r, color.g, color.b, alpha },
+ .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
+ .glyph_size = .{ render.glyph.width, render.glyph.height },
+ .bearings = .{
+ @intCast(render.glyph.offset_x),
+ @intCast(render.glyph.offset_y),
+ },
+ });
+ }
+
+ /// Add a strikethrough decoration to the specified cell
+ fn addStrikethrough(
+ self: *Self,
+ x: terminal.size.CellCountInt,
+ y: terminal.size.CellCountInt,
+ color: terminal.color.RGB,
+ alpha: 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.add(self.alloc, .strikethrough, .{
+ .atlas = .grayscale,
+ .grid_pos = .{ @intCast(x), @intCast(y) },
+ .color = .{ color.r, color.g, color.b, alpha },
+ .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
+ .glyph_size = .{ render.glyph.width, render.glyph.height },
+ .bearings = .{
+ @intCast(render.glyph.offset_x),
+ @intCast(render.glyph.offset_y),
+ },
+ });
+ }
+
+ // Add a glyph to the specified cell.
+ fn addGlyph(
+ self: *Self,
+ x: terminal.size.CellCountInt,
+ y: terminal.size.CellCountInt,
+ cols: usize,
+ cell_raws: []const terminal.page.Cell,
+ shaper_cell: font.shape.Cell,
+ shaper_run: font.shape.TextRun,
+ color: terminal.color.RGB,
+ alpha: u8,
+ ) !void {
+ const cell = cell_raws[x];
+ const cp = cell.codepoint();
+
+ // 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,
+ .thicken_strength = self.config.font_thicken_strength,
+ .cell_width = cell.gridWidth(),
+ // If there's no Nerd Font constraint for this codepoint
+ // then, if it's a symbol, we constrain it to fit inside
+ // its cell(s), we don't modify the alignment at all.
+ .constraint = getConstraint(cp) orelse
+ if (cellpkg.isSymbol(cp)) .{
+ .size = .fit,
+ } else .none,
+ .constraint_width = constraintWidth(
+ cell_raws,
+ x,
+ cols,
+ ),
+ },
+ );
+
+ // 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;
+ }
+
+ try self.cells.add(self.alloc, .text, .{
+ .atlas = switch (render.presentation) {
+ .emoji => .color,
+ .text => .grayscale,
+ },
+ .bools = .{ .no_min_contrast = noMinContrast(cp) },
+ .grid_pos = .{ @intCast(x), @intCast(y) },
+ .color = .{ color.r, color.g, color.b, alpha },
+ .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
+ .glyph_size = .{ render.glyph.width, render.glyph.height },
+ .bearings = .{
+ @intCast(render.glyph.offset_x + shaper_cell.x_offset),
+ @intCast(render.glyph.offset_y + shaper_cell.y_offset),
+ },
+ });
+ }
+
+ fn addCursor(
+ self: *Self,
+ cursor_state: *const terminal.RenderState.Cursor,
+ cursor_style: renderer.CursorStyle,
+ cursor_color: terminal.color.RGB,
+ ) void {
+ const cursor_vp = cursor_state.viewport orelse return;
+
+ // 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.
+ if (!cursor_vp.wide_tail) break :cell .{
+ cursor_state.cell.wide == .wide,
+ cursor_vp.x,
+ };
+
+ // If we're part of a wide character, we move the cursor back
+ // to the actual character.
+ break :cell .{ true, cursor_vp.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 => .cursor_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;
+ };
+ },
+
+ .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;
+ } 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;
+ },
+ };
+
+ self.cells.setCursor(.{
+ .atlas = .grayscale,
+ .bools = .{ .is_cursor_glyph = true },
+ .grid_pos = .{ x, cursor_vp.y },
+ .color = .{ cursor_color.r, cursor_color.g, cursor_color.b, alpha },
+ .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
+ .glyph_size = .{ render.glyph.width, render.glyph.height },
+ .bearings = .{
+ @intCast(render.glyph.offset_x),
+ @intCast(render.glyph.offset_y),
+ },
+ }, cursor_style);
+ }
+
+ fn addPreeditCell(
+ self: *Self,
+ cp: renderer.State.Preedit.Codepoint,
+ coord: terminal.Coordinate,
+ screen_fg: terminal.color.RGB,
+ ) !void {
+ // 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 text
+ try self.cells.add(self.alloc, .text, .{
+ .atlas = .grayscale,
+ .grid_pos = .{ @intCast(coord.x), @intCast(coord.y) },
+ .color = .{ screen_fg.r, screen_fg.g, screen_fg.b, 255 },
+ .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
+ .glyph_size = .{ render.glyph.width, render.glyph.height },
+ .bearings = .{
+ @intCast(render.glyph.offset_x),
+ @intCast(render.glyph.offset_y),
+ },
+ });
+
+ // Add underline
+ try self.addUnderline(@intCast(coord.x), @intCast(coord.y), .single, screen_fg, 255);
+ if (cp.wide and coord.x < self.cells.size.columns - 1) {
+ try self.addUnderline(@intCast(coord.x + 1), @intCast(coord.y), .single, screen_fg, 255);
+ }
+ }
+
+ /// Sync the atlas data to the given texture. This copies the bytes
+ /// associated with the atlas to the given texture. If the atlas no
+ /// longer fits into the texture, the texture will be resized.
+ fn syncAtlasTexture(
+ self: *const Self,
+ atlas: *const font.Atlas,
+ texture: *Texture,
+ ) !void {
+ if (atlas.size > texture.width) {
+ // Free our old texture
+ texture.*.deinit();
+
+ // Reallocate
+ texture.* = try self.api.initAtlasTexture(atlas);
+ }
+
+ try texture.replaceRegion(0, 0, atlas.size, atlas.size, atlas.data);
+ }
+ };
+}