ghostty/src/renderer/generic.zig
Qwerasd 2084d5f256 font/sprite+renderer: never constrain sprite glyphs
This was creating problems with the branch drawing glyphs at some sizes.

In the future the whole "foreground modes" thing needs to be reworked,
so this is just a stopgap until that gets turned in to something nicer.
2025-06-30 11:16:47 -06:00

3228 lines
133 KiB
Zig

const std = @import("std");
const builtin = @import("builtin");
const glfw = @import("glfw");
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 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 fgMode = cellpkg.fgMode;
const isCovering = cellpkg.isCovering;
const imagepkg = @import("image.zig");
const Image = imagepkg.Image;
const ImageMap = imagepkg.ImageMap;
const ImagePlacementList = std.ArrayListUnmanaged(imagepkg.Placement);
const shadertoy = @import("shadertoy.zig");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const Terminal = terminal.Terminal;
const Health = renderer.Health;
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 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,
/// The foreground color set by an OSC 10 sequence. If unset then
/// default_foreground_color is used.
foreground_color: ?terminal.color.RGB,
/// Foreground color set in the user's config file.
default_foreground_color: terminal.color.RGB,
/// The background color set by an OSC 11 sequence. If unset then
/// default_background_color is used.
background_color: ?terminal.color.RGB,
/// Background color set in the user's config file.
default_background_color: terminal.color.RGB,
/// The cursor color set by an OSC 12 sequence. If unset then
/// default_cursor_color is used.
cursor_color: ?terminal.color.RGB,
/// Default cursor color when no color is set explicitly by an OSC 12 command.
/// This is cursor color as set in the user's config, if any. If no cursor color
/// is set in the user's config, then the cursor color is determined by the
/// current foreground color.
default_cursor_color: ?terminal.color.RGB,
/// When `cursor_color` is null, swap the foreground and background colors of
/// the cell under the cursor for the cursor color. Otherwise, use the default
/// foreground color as the cursor color.
cursor_invert: bool,
/// The 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,
/// The last viewport that we based our rebuild off of. If this changes,
/// then we do a full rebuild of the cells. The pointer values in this pin
/// are NOT SAFE to read because they may be modified, freed, etc from the
/// termio thread. We treat the pointers as integers for comparison only.
cells_viewport: ?terminal.Pin = null,
/// 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: ImageMap = .{},
image_placements: ImagePlacementList = .{},
image_bg_end: u32 = 0,
image_text_end: u32 = 0,
image_virtual: bool = false,
/// Background image, if we have one.
bg_image: ?imagepkg.Image = null,
/// Set whenever the background image changes, singalling
/// 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,
/// 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.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,
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();
return .{
.front_texture = front_texture,
.back_texture = back_texture,
.uniforms = uniforms,
};
}
pub fn deinit(self: *CustomShaderState) void {
self.front_texture.deinit();
self.back_texture.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: ?terminal.color.RGB,
cursor_invert: bool,
cursor_opacity: f64,
cursor_text: ?terminal.color.RGB,
background: terminal.color.RGB,
background_opacity: f64,
foreground: terminal.color.RGB,
selection_background: ?terminal.color.RGB,
selection_foreground: ?terminal.color.RGB,
invert_selection_fg_bg: bool,
bold_is_bright: bool,
min_contrast: f32,
padding_color: configpkg.WindowPaddingColor,
custom_shaders: configpkg.RepeatablePath,
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,
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,
);
const cursor_invert = config.@"cursor-invert-fg-bg";
return .{
.background_opacity = @max(0, @min(1, config.@"background-opacity")),
.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 = if (!cursor_invert and config.@"cursor-color" != null)
config.@"cursor-color".?.toTerminalRGB()
else
null,
.cursor_invert = cursor_invert,
.cursor_text = if (config.@"cursor-text") |txt|
txt.toTerminalRGB()
else
null,
.cursor_opacity = @max(0, @min(1, config.@"cursor-opacity")),
.background = config.background.toTerminalRGB(),
.foreground = config.foreground.toTerminalRGB(),
.invert_selection_fg_bg = config.@"selection-invert-fg-bg",
.bold_is_bright = config.@"bold-is-bright",
.min_contrast = @floatCast(config.@"minimum-contrast"),
.padding_color = config.@"window-padding-color",
.selection_background = if (config.@"selection-background") |bg|
bg.toTerminalRGB()
else
null,
.selection_foreground = if (config.@"selection-foreground") |bg|
bg.toTerminalRGB()
else
null,
.custom_shaders = custom_shaders,
.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",
.arena = arena,
};
}
pub fn deinit(self: *DerivedConfig) void {
const alloc = self.arena.allocator();
self.links.deinit(alloc);
self.arena.deinit();
}
};
/// Returns the hints that we want for this window.
pub fn glfwWindowHints(config: *const configpkg.Config) glfw.Window.Hints {
// If our graphics API provides hints, use them,
// otherwise fall back to generic hints.
if (@hasDecl(GraphicsAPI, "glfwWindowHints")) {
return GraphicsAPI.glfwWindowHints(config);
}
return .{
.client_api = .no_api,
.transparent_framebuffer = config.@"background-opacity" < 1,
};
}
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,
.foreground_color = null,
.default_foreground_color = options.config.foreground,
.background_color = null,
.default_background_color = options.config.background,
.cursor_color = null,
.default_cursor_color = options.config.cursor_color,
.cursor_invert = options.config.cursor_invert,
// 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,
@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),
.cursor_change_time = 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 {
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();
{
var it = self.images.iterator();
while (it.next()) |kv| kv.value_ptr.image.deinit(self.alloc);
self.images.deinit(self.alloc);
}
self.image_placements.deinit(self.alloc);
if (self.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 fn markDirty(self: *Self) void {
self.cells_viewport = null;
}
/// 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 {
self.focused = focus;
// 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();
}
/// 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,
) !void {
// Data we extract out of the critical area.
const Critical = struct {
bg: terminal.color.RGB,
screen: terminal.Screen,
screen_type: terminal.ScreenType,
mouse: renderer.State.Mouse,
preedit: ?renderer.State.Preedit,
cursor_style: ?renderer.CursorStyle,
color_palette: terminal.color.Palette,
/// If true, rebuild the full screen.
full_rebuild: bool,
};
// 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;
// // "[updateFrame critical time] <START us>\t<TIME_TAKEN us>"
// std.log.err("[updateFrame critical time] {}\t{}", .{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;
}
// Swap bg/fg if the terminal is reversed
const bg = self.background_color orelse self.default_background_color;
const fg = self.foreground_color orelse self.default_foreground_color;
defer {
if (self.background_color) |*c| {
c.* = bg;
} else {
self.default_background_color = bg;
}
if (self.foreground_color) |*c| {
c.* = fg;
} else {
self.default_foreground_color = fg;
}
}
if (state.terminal.modes.get(.reverse_colors)) {
if (self.background_color) |*c| {
c.* = fg;
} else {
self.default_background_color = fg;
}
if (self.foreground_color) |*c| {
c.* = bg;
} else {
self.default_foreground_color = bg;
}
}
// Get the viewport pin so that we can compare it to the current.
const viewport_pin = state.terminal.screen.pages.pin(.{ .viewport = .{} }).?;
// We used to share terminal state, but we've since learned through
// analysis that it is faster to copy the terminal state than to
// hold the lock while rebuilding GPU cells.
var screen_copy = try state.terminal.screen.clone(
self.alloc,
.{ .viewport = .{} },
null,
);
errdefer screen_copy.deinit();
// Whether to draw our cursor or not.
const cursor_style = if (state.terminal.flags.password_input)
.lock
else
renderer.cursorStyle(
state,
self.focused,
cursor_blink_visible,
);
// Get our preedit state
const preedit: ?renderer.State.Preedit = preedit: {
if (cursor_style == null) break :preedit null;
const p = state.preedit orelse break :preedit null;
break :preedit try p.clone(self.alloc);
};
errdefer if (preedit) |p| p.deinit(self.alloc);
// If we have Kitty graphics data, we enter a SLOW SLOW SLOW path.
// We only do this if the Kitty image state is dirty meaning only if
// it changes.
//
// If we have any virtual references, we must also rebuild our
// kitty state on every frame because any cell change can move
// an image.
if (state.terminal.screen.kitty_images.dirty or
self.image_virtual)
{
try self.prepKittyGraphics(state.terminal);
}
// If we have any terminal dirty flags set then we need to rebuild
// the entire screen. This can be optimized in the future.
const full_rebuild: bool = rebuild: {
{
const Int = @typeInfo(terminal.Terminal.Dirty).@"struct".backing_integer.?;
const v: Int = @bitCast(state.terminal.flags.dirty);
if (v > 0) break :rebuild true;
}
{
const Int = @typeInfo(terminal.Screen.Dirty).@"struct".backing_integer.?;
const v: Int = @bitCast(state.terminal.screen.dirty);
if (v > 0) break :rebuild true;
}
// If our viewport changed then we need to rebuild the entire
// screen because it means we scrolled. If we have no previous
// viewport then we must rebuild.
const prev_viewport = self.cells_viewport orelse break :rebuild true;
if (!prev_viewport.eql(viewport_pin)) break :rebuild true;
break :rebuild false;
};
// Reset the dirty flags in the terminal and screen. We assume
// that our rebuild will be successful since so we optimize for
// success and reset while we hold the lock. This is much easier
// than coordinating row by row or as changes are persisted.
state.terminal.flags.dirty = .{};
state.terminal.screen.dirty = .{};
{
var it = state.terminal.screen.pages.pageIterator(
.right_down,
.{ .screen = .{} },
null,
);
while (it.next()) |chunk| {
var dirty_set = chunk.node.data.dirtyBitSet();
dirty_set.unsetAll();
}
}
// Update our viewport pin
self.cells_viewport = viewport_pin;
break :critical .{
.bg = self.background_color orelse self.default_background_color,
.screen = screen_copy,
.screen_type = state.terminal.active_screen,
.mouse = state.mouse,
.preedit = preedit,
.cursor_style = cursor_style,
.color_palette = state.terminal.color_palette.colors,
.full_rebuild = full_rebuild,
};
};
defer {
critical.screen.deinit();
if (critical.preedit) |p| p.deinit(self.alloc);
}
// Build our GPU cells
try self.rebuildCells(
critical.full_rebuild,
&critical.screen,
critical.screen_type,
critical.mouse,
critical.preedit,
critical.cursor_style,
&critical.color_palette,
);
// Notify our shaper we're done for the frame. For some shapers,
// such as CoreText, this triggers off-thread cleanup logic.
self.font_shaper.endFrame();
// Acquire the draw mutex because we're modifying state here.
{
self.draw_mutex.lock();
defer self.draw_mutex.unlock();
// Update our background color
self.uniforms.bg_color = .{
critical.bg.r,
critical.bg.g,
critical.bg.b,
@intFromFloat(@round(self.config.background_opacity * 255.0)),
};
}
}
/// 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 {
// 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();
// 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.
try self.uploadKittyImages();
// Upload the background image to the GPU as necessary.
try self.uploadBackgroundImage();
// Update custom shader uniforms if necessary.
try self.updateCustomShaderUniforms();
// 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.
try self.drawImagePlacements(
&pass,
self.image_placements.items[0..self.image_bg_end],
);
// 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.
try self.drawImagePlacements(
&pass,
self.image_placements.items[self.image_bg_end..self.image_text_end],
);
// 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.
try self.drawImagePlacements(
&pass,
self.image_placements.items[self.image_text_end..],
);
}
// 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},
.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();
}
fn drawImagePlacements(
self: *Self,
pass: *RenderPass,
placements: []const imagepkg.Placement,
) !void {
if (placements.len == 0) return;
for (placements) |p| {
// Look up the image
const image = self.images.get(p.image_id) orelse {
log.warn("image not found for placement image_id={}", .{p.image_id});
return;
};
// Get the texture
const texture = switch (image.image) {
.ready => |t| t,
else => {
log.warn("image not ready for placement image_id={}", .{p.image_id});
return;
},
};
// Create our vertex buffer, which is always exactly one item.
// future(mitchellh): we can group rendering multiple instances of a single image
var buf = try Buffer(shaderpkg.Image).initFill(
self.api.imageBufferOptions(),
&.{.{
.grid_pos = .{
@as(f32, @floatFromInt(p.x)),
@as(f32, @floatFromInt(p.y)),
},
.cell_offset = .{
@as(f32, @floatFromInt(p.cell_offset_x)),
@as(f32, @floatFromInt(p.cell_offset_y)),
},
.source_rect = .{
@as(f32, @floatFromInt(p.source_x)),
@as(f32, @floatFromInt(p.source_y)),
@as(f32, @floatFromInt(p.source_width)),
@as(f32, @floatFromInt(p.source_height)),
},
.dest_size = .{
@as(f32, @floatFromInt(p.width)),
@as(f32, @floatFromInt(p.height)),
},
}},
);
defer buf.deinit();
pass.step(.{
.pipeline = self.shaders.pipelines.image,
.buffers = &.{buf.buffer},
.textures = &.{texture},
.draw = .{
.type = .triangle_strip,
.vertex_count = 4,
},
});
}
}
/// This goes through the Kitty graphic placements and accumulates the
/// placements we need to render on our viewport.
fn prepKittyGraphics(
self: *Self,
t: *terminal.Terminal,
) !void {
self.draw_mutex.lock();
defer self.draw_mutex.unlock();
const storage = &t.screen.kitty_images;
defer storage.dirty = false;
// We always clear our previous placements no matter what because
// we rebuild them from scratch.
self.image_placements.clearRetainingCapacity();
self.image_virtual = false;
// Go through our known images and if there are any that are no longer
// in use then mark them to be freed.
//
// This never conflicts with the below because a placement can't
// reference an image that doesn't exist.
{
var it = self.images.iterator();
while (it.next()) |kv| {
if (storage.imageById(kv.key_ptr.*) == null) {
kv.value_ptr.image.markForUnload();
}
}
}
// The top-left and bottom-right corners of our viewport in screen
// points. This lets us determine offsets and containment of placements.
const top = t.screen.pages.getTopLeft(.viewport);
const bot = t.screen.pages.getBottomRight(.viewport).?;
const top_y = t.screen.pages.pointFromPin(.screen, top).?.screen.y;
const bot_y = t.screen.pages.pointFromPin(.screen, bot).?.screen.y;
// Go through the placements and ensure the image is
// on the GPU or else is ready to be sent to the GPU.
var it = storage.placements.iterator();
while (it.next()) |kv| {
const p = kv.value_ptr;
// Special logic based on location
switch (p.location) {
.pin => {},
.virtual => {
// We need to mark virtual placements on our renderer so that
// we know to rebuild in more scenarios since cell changes can
// now trigger placement changes.
self.image_virtual = true;
// We also continue out because virtual placements are
// only triggered by the unicode placeholder, not by the
// placement itself.
continue;
},
}
// Get the image for the placement
const image = storage.imageById(kv.key_ptr.image_id) orelse {
log.warn(
"missing image for placement, ignoring image_id={}",
.{kv.key_ptr.image_id},
);
continue;
};
try self.prepKittyPlacement(t, top_y, bot_y, &image, p);
}
// If we have virtual placements then we need to scan for placeholders.
if (self.image_virtual) {
var v_it = terminal.kitty.graphics.unicode.placementIterator(top, bot);
while (v_it.next()) |virtual_p| try self.prepKittyVirtualPlacement(
t,
&virtual_p,
);
}
// Sort the placements by their Z value.
std.mem.sortUnstable(
imagepkg.Placement,
self.image_placements.items,
{},
struct {
fn lessThan(
ctx: void,
lhs: imagepkg.Placement,
rhs: imagepkg.Placement,
) bool {
_ = ctx;
return lhs.z < rhs.z or (lhs.z == rhs.z and lhs.image_id < rhs.image_id);
}
}.lessThan,
);
// Find our indices. The values are sorted by z so we can
// find the first placement out of bounds to find the limits.
var bg_end: ?u32 = null;
var text_end: ?u32 = null;
const bg_limit = std.math.minInt(i32) / 2;
for (self.image_placements.items, 0..) |p, i| {
if (bg_end == null and p.z >= bg_limit) {
bg_end = @intCast(i);
}
if (text_end == null and p.z >= 0) {
text_end = @intCast(i);
}
}
// If we didn't see any images with a z > the bg limit,
// then our bg end is the end of our placement list.
self.image_bg_end =
bg_end orelse @intCast(self.image_placements.items.len);
// Same idea for the image_text_end.
self.image_text_end =
text_end orelse @intCast(self.image_placements.items.len);
}
fn prepKittyVirtualPlacement(
self: *Self,
t: *terminal.Terminal,
p: *const terminal.kitty.graphics.unicode.Placement,
) !void {
const storage = &t.screen.kitty_images;
const image = storage.imageById(p.image_id) orelse {
log.warn(
"missing image for virtual placement, ignoring image_id={}",
.{p.image_id},
);
return;
};
const rp = p.renderPlacement(
storage,
&image,
self.grid_metrics.cell_width,
self.grid_metrics.cell_height,
) catch |err| {
log.warn("error rendering virtual placement err={}", .{err});
return;
};
// If our placement is zero sized then we don't do anything.
if (rp.dest_width == 0 or rp.dest_height == 0) return;
const viewport: terminal.point.Point = t.screen.pages.pointFromPin(
.viewport,
rp.top_left,
) orelse {
// This is unreachable with virtual placements because we should
// only ever be looking at virtual placements that are in our
// viewport in the renderer and virtual placements only ever take
// up one row.
unreachable;
};
// Prepare the image for the GPU and store the placement.
try self.prepKittyImage(&image);
try self.image_placements.append(self.alloc, .{
.image_id = image.id,
.x = @intCast(rp.top_left.x),
.y = @intCast(viewport.viewport.y),
.z = -1,
.width = rp.dest_width,
.height = rp.dest_height,
.cell_offset_x = rp.offset_x,
.cell_offset_y = rp.offset_y,
.source_x = rp.source_x,
.source_y = rp.source_y,
.source_width = rp.source_width,
.source_height = rp.source_height,
});
}
/// Get the viewport-relative position for this
/// placement and add it to the placements list.
fn prepKittyPlacement(
self: *Self,
t: *terminal.Terminal,
top_y: u32,
bot_y: u32,
image: *const terminal.kitty.graphics.Image,
p: *const terminal.kitty.graphics.ImageStorage.Placement,
) !void {
// Get the rect for the placement. If this placement doesn't have
// a rect then its virtual or something so skip it.
const rect = p.rect(image.*, t) orelse return;
// This is expensive but necessary.
const img_top_y = t.screen.pages.pointFromPin(.screen, rect.top_left).?.screen.y;
const img_bot_y = t.screen.pages.pointFromPin(.screen, rect.bottom_right).?.screen.y;
// If the selection isn't within our viewport then skip it.
if (img_top_y > bot_y) return;
if (img_bot_y < top_y) return;
// We need to prep this image for upload if it isn't in the
// cache OR it is in the cache but the transmit time doesn't
// match meaning this image is different.
try self.prepKittyImage(image);
// Calculate the dimensions of our image, taking in to
// account the rows / columns specified by the placement.
const dest_size = p.calculatedSize(image.*, t);
// Calculate the source rectangle
const source_x = @min(image.width, p.source_x);
const source_y = @min(image.height, p.source_y);
const source_width = if (p.source_width > 0)
@min(image.width - source_x, p.source_width)
else
image.width;
const source_height = if (p.source_height > 0)
@min(image.height - source_y, p.source_height)
else
image.height;
// Get the viewport-relative Y position of the placement.
const y_pos: i32 = @as(i32, @intCast(img_top_y)) - @as(i32, @intCast(top_y));
// Accumulate the placement
if (dest_size.width > 0 and dest_size.height > 0) {
try self.image_placements.append(self.alloc, .{
.image_id = image.id,
.x = @intCast(rect.top_left.x),
.y = y_pos,
.z = p.z,
.width = dest_size.width,
.height = dest_size.height,
.cell_offset_x = p.x_offset,
.cell_offset_y = p.y_offset,
.source_x = source_x,
.source_y = source_y,
.source_width = source_width,
.source_height = source_height,
});
}
}
/// Prepare the provided image for upload to the GPU by copying its
/// data with our allocator and setting it to the pending state.
fn prepKittyImage(
self: *Self,
image: *const terminal.kitty.graphics.Image,
) !void {
// If this image exists and its transmit time is the same we assume
// it is the identical image so we don't need to send it to the GPU.
const gop = try self.images.getOrPut(self.alloc, image.id);
if (gop.found_existing and
gop.value_ptr.transmit_time.order(image.transmit_time) == .eq)
{
return;
}
// Copy the data into the pending state.
const data = try self.alloc.dupe(u8, image.data);
errdefer self.alloc.free(data);
// Store it in the map
const pending: Image.Pending = .{
.width = image.width,
.height = image.height,
.pixel_format = switch (image.format) {
.gray => .gray,
.gray_alpha => .gray_alpha,
.rgb => .rgb,
.rgba => .rgba,
.png => unreachable, // should be decoded by now
},
.data = data.ptr,
};
const new_image: Image = .{ .pending = pending };
if (!gop.found_existing) {
gop.value_ptr.* = .{
.image = new_image,
.transmit_time = undefined,
};
} else {
try gop.value_ptr.image.markForReplace(
self.alloc,
new_image,
);
}
try gop.value_ptr.image.prepForUpload(self.alloc);
gop.value_ptr.transmit_time = image.transmit_time;
}
/// Upload any images to the GPU that need to be uploaded,
/// and remove any images that are no longer needed on the GPU.
fn uploadKittyImages(self: *Self) !void {
var image_it = self.images.iterator();
while (image_it.next()) |kv| {
const img = &kv.value_ptr.image;
if (img.isUnloading()) {
img.deinit(self.alloc);
self.images.removeByPtr(kv.key_ptr);
return;
}
if (img.isPending()) try img.upload(self.alloc, &self.api);
}
}
/// 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| {
try 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 re-used for the new font.
const font_shaper_cache = font.ShaperCache.init();
self.font_shaper_cache.deinit(self.alloc);
self.font_shaper_cache = font_shaper_cache;
// Set our new 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";
// Set our new colors
self.default_background_color = config.background;
self.default_foreground_color = config.foreground;
self.default_cursor_color = if (!config.cursor_invert) config.cursor_color else null;
self.cursor_invert = config.cursor_invert;
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.cells_viewport = null;
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 uniforms for the custom shaders, if necessary.
///
/// This should be called exactly once per frame, inside `drawFrame`.
fn updateCustomShaderUniforms(self: *Self) !void {
// We only need to do this if we have custom shaders.
if (!self.has_custom_shaders) return;
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));
self.custom_shader_uniforms.time = since_ns / std.time.ns_per_s;
const delta_ns: f32 = @floatFromInt(now.since(last_frame_time));
self.custom_shader_uniforms.time_delta = delta_ns / std.time.ns_per_s;
self.custom_shader_uniforms.frame += 1;
const screen = self.size.screen;
const padding = self.size.padding;
const cell = self.size.cell;
self.custom_shader_uniforms.resolution = .{
@floatFromInt(screen.width),
@floatFromInt(screen.height),
1,
};
self.custom_shader_uniforms.channel_resolution[0] = .{
@floatFromInt(screen.width),
@floatFromInt(screen.height),
1,
0,
};
// Update custom cursor uniforms, if we have a cursor.
if (self.cells.fg_rows.lists[0].items.len > 0) {
const cursor: shaderpkg.CellText =
self.cells.fg_rows.lists[0].items[0];
const cursor_width: f32 = @floatFromInt(cursor.glyph_size[0]);
const cursor_height: f32 = @floatFromInt(cursor.glyph_size[1]);
var pixel_x: f32 = @floatFromInt(
cursor.grid_pos[0] * cell.width + padding.left,
);
var pixel_y: f32 = @floatFromInt(
cursor.grid_pos[1] * cell.height + padding.top,
);
pixel_x += @floatFromInt(cursor.bearings[0]);
pixel_y += @floatFromInt(cursor.bearings[1]);
// If +Y is up in our shaders, we need to flip the coordinate.
if (!GraphicsAPI.custom_shader_y_is_down) {
pixel_y = @as(f32, @floatFromInt(screen.height)) - pixel_y;
// We need to add the cursor height because we need the +Y
// edge for the Y coordinate, and flipping means that it's
// the -Y edge now.
pixel_y += cursor_height;
}
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 uniforms = &self.custom_shader_uniforms;
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;
}
}
}
/// 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.
fn rebuildCells(
self: *Self,
wants_rebuild: bool,
screen: *terminal.Screen,
screen_type: terminal.ScreenType,
mouse: renderer.State.Mouse,
preedit: ?renderer.State.Preedit,
cursor_style_: ?renderer.CursorStyle,
color_palette: *const terminal.color.Palette,
) !void {
self.draw_mutex.lock();
defer self.draw_mutex.unlock();
// 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});
// }
_ = screen_type; // we might use this again later so not deleting it yet
// Create an arena for all our temporary allocations while rebuilding
var arena = ArenaAllocator.init(self.alloc);
defer arena.deinit();
const arena_alloc = arena.allocator();
// Create our match set for the links.
var link_match_set: link.MatchSet = if (mouse.point) |mouse_pt| try self.config.links.matchSet(
arena_alloc,
screen,
mouse_pt,
mouse.mods,
) else .{};
// Determine our x/y range for preedit. We don't want to render anything
// here because we will render the preedit separately.
const preedit_range: ?struct {
y: terminal.size.CellCountInt,
x: [2]terminal.size.CellCountInt,
cp_offset: usize,
} = if (preedit) |preedit_v| preedit: {
const range = preedit_v.range(screen.cursor.x, screen.pages.cols - 1);
break :preedit .{
.y = screen.cursor.y,
.x = .{ range.start, range.end },
.cp_offset = range.cp_offset,
};
} else null;
const grid_size_diff =
self.cells.size.rows != screen.pages.rows or
self.cells.size.columns != screen.pages.cols;
if (grid_size_diff) {
var new_size = self.cells.size;
new_size.rows = screen.pages.rows;
new_size.columns = screen.pages.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 = wants_rebuild 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,
};
},
}
}
// We rebuild the cells row-by-row because we
// do font shaping and dirty tracking by row.
var row_it = screen.pages.rowIterator(.left_up, .{ .viewport = .{} }, null);
// 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.
var y: terminal.size.CellCountInt = @min(
screen.pages.rows,
self.cells.size.rows,
);
while (row_it.next()) |row| {
// The viewport may have more rows than our cell contents,
// so we need to break from the loop early if we hit y = 0.
if (y == 0) break;
y -= 1;
if (!rebuild) {
// Only rebuild if we are doing a full rebuild or this row is dirty.
if (!row.isDirty()) continue;
// Clear the cells if the row is dirty
self.cells.clear(y);
}
// True if we want to do font shaping around the cursor.
// We want to do font shaping as long as the cursor is enabled.
const shape_cursor = screen.viewportIsBottom() and
y == screen.cursor.y;
// We need to get this row's selection, if
// there is one, for proper run splitting.
const row_selection = sel: {
const sel = screen.selection orelse break :sel null;
const pin = screen.pages.pin(.{ .viewport = .{ .y = y } }) orelse
break :sel null;
break :sel sel.containedRow(screen, pin) orelse null;
};
// On primary screen, we still apply vertical padding
// extension under certain conditions we feel are safe.
//
// This helps make some scenarios look better while
// avoiding scenarios we know do NOT look good.
switch (self.config.padding_color) {
// These already have the correct values set above.
.background, .@"extend-always" => {},
// Apply heuristics for padding extension.
.extend => if (y == 0) {
self.uniforms.padding_extend.up = !row.neverExtendBg(
color_palette,
self.background_color orelse self.default_background_color,
);
} else if (y == self.cells.size.rows - 1) {
self.uniforms.padding_extend.down = !row.neverExtendBg(
color_palette,
self.background_color orelse self.default_background_color,
);
},
}
// Iterator of runs for shaping.
var run_iter_opts: font.shape.RunOptions = .{
.grid = self.font_grid,
.screen = screen,
.row = row,
.selection = row_selection,
.cursor_x = if (shape_cursor) screen.cursor.x else null,
};
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;
const row_cells_all = row.cells(.all);
// If our viewport is wider than our cell contents buffer,
// we still only process cells up to the width of the buffer.
const row_cells = row_cells_all[0..@min(row_cells_all.len, self.cells.size.columns)];
for (row_cells, 0..) |*cell, x| {
// If this cell falls within our preedit range then we
// skip this because preedits are setup separately.
if (preedit_range) |range| preedit: {
// We're not on the preedit line, no actions necessary.
if (range.y != y) break :preedit;
// We're before the preedit range, no actions necessary.
if (x < range.x[0]) break :preedit;
// We're in the preedit range, skip this cell.
if (x <= range.x[1]) continue;
// After exiting the preedit range we need to catch
// the run position up because of the missed cells.
// In all other cases, no action is necessary.
if (x != range.x[1] + 1) break :preedit;
// Step the run iterator until we find a run that ends
// after the current cell, which will be the soonest run
// that might contain glyphs for our cell.
while (shaper_run) |run| {
if (run.offset + run.cells > x) break;
shaper_run = try run_iter.next(self.alloc);
shaper_cells = null;
shaper_cells_i = 0;
}
const run = shaper_run orelse break :preedit;
// If we haven't shaped this run, do so now.
shaper_cells = shaper_cells orelse
// Try to read the cells from the shaping cache if we can.
self.font_shaper_cache.get(run) orelse
cache: {
// Otherwise we have to shape them.
const cells = try self.font_shaper.shape(run);
// Try to cache them. If caching fails for any reason we
// continue because it is just a performance optimization,
// not a correctness issue.
self.font_shaper_cache.put(
self.alloc,
run,
cells,
) catch |err| {
log.warn(
"error caching font shaping results err={}",
.{err},
);
};
// The cells we get from direct shaping are always owned
// by the shaper and valid until the next shaping call so
// we can safely use them.
break :cache cells;
};
// Advance our index until we reach or pass
// our current x position in the shaper cells.
while (shaper_cells.?[shaper_cells_i].x < x) {
shaper_cells_i += 1;
}
}
const wide = cell.wide;
const style = row.style(cell);
const cell_pin: terminal.Pin = cell: {
var copy = row;
copy.x = @intCast(x);
break :cell copy;
};
// True if this cell is selected
const selected: bool = if (screen.selection) |sel|
sel.contains(screen, .{
.node = row.node,
.y = row.y,
.x = @intCast(
// Spacer tails should show the selection
// state of the wide cell they belong to.
if (wide == .spacer_tail)
x -| 1
else
x,
),
})
else
false;
const bg_style = style.bg(cell, color_palette);
const fg_style = style.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color;
// The final background color for the cell.
const bg = bg: {
if (selected) {
break :bg if (self.config.invert_selection_fg_bg)
if (style.flags.inverse)
// Cell is selected with invert selection fg/bg
// enabled, and the cell has the inverse style
// flag, so they cancel out and we get the normal
// bg color.
bg_style
else
// If it doesn't have the inverse style
// flag then we use the fg color instead.
fg_style
else
// If we don't have invert selection fg/bg set then we
// just use the selection background if set, otherwise
// the default fg color.
break :bg self.config.selection_background orelse self.foreground_color orelse self.default_foreground_color;
}
// Not selected
break :bg if (style.flags.inverse != isCovering(cell.codepoint()))
// Two cases cause us to invert (use the fg color as the bg)
// - The "inverse" style flag.
// - A "covering" glyph; we use fg for bg in that
// case to help make sure that padding extension
// works correctly.
//
// If one of these is true (but not the other)
// then we use the fg style color for the bg.
fg_style
else
// Otherwise they cancel out.
bg_style;
};
const fg = fg: {
if (selected and !self.config.invert_selection_fg_bg) {
// If we don't have invert selection fg/bg set
// then we just use the selection foreground if
// set, otherwise the default bg color.
break :fg self.config.selection_foreground orelse self.background_color orelse self.default_background_color;
}
// Whether we need to use the bg color as our fg color:
// - Cell is inverted and not selected
// - Cell is selected and not inverted
// Note: if selected then invert sel fg / bg must be
// false since we separately handle it if true above.
break :fg if (style.flags.inverse != selected)
bg_style orelse self.background_color orelse self.default_background_color
else
fg_style;
};
// Foreground alpha for this cell.
const alpha: u8 = if (style.flags.faint) 175 else 255;
// Set the cell's background color.
{
const rgb = bg orelse self.background_color orelse self.default_background_color;
// 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) break :bg_alpha default;
// Cells that are reversed should be fully opaque.
if (style.flags.inverse) break :bg_alpha default;
// 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 = if (link_match_set.contains(screen, cell_pin))
if (style.flags.underline == .single)
.double
else
.single
else
style.flags.underline;
// We draw underlines first so that they layer underneath text.
// This improves readability when a colored underline is used
// which intersects parts of the text (descenders).
if (underline != .none) self.addUnderline(
@intCast(x),
@intCast(y),
underline,
style.underlineColor(color_palette) orelse fg,
alpha,
) 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 cells = try self.font_shaper.shape(run);
// Try to cache them. If caching fails for any reason we
// continue because it is just a performance optimization,
// not a correctness issue.
self.font_shaper_cache.put(
self.alloc,
run,
cells,
) catch |err| {
log.warn(
"error caching font shaping results err={}",
.{err},
);
};
// The cells we get from direct shaping are always owned
// by the shaper and valid until the next shaping call so
// we can safely use them.
break :cache cells;
};
const cells = shaper_cells orelse break :glyphs;
// If there are no shaper cells for this run, ignore it.
// This can occur for runs of empty cells, and is fine.
if (cells.len == 0) break :glyphs;
// If we encounter a shaper cell to the left of the current
// cell then we have some problems. This logic relies on x
// position monotonically increasing.
assert(cells[shaper_cells_i].x >= x);
// NOTE: An assumption is made here that a single cell will never
// be present in more than one shaper run. If that assumption is
// violated, this logic breaks.
while (shaper_cells_i < cells.len and cells[shaper_cells_i].x == x) : ({
shaper_cells_i += 1;
}) {
self.addGlyph(
@intCast(x),
@intCast(y),
cell_pin,
cells[shaper_cells_i],
shaper_run.?,
fg,
alpha,
) 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 },
);
};
}
}
// Setup our cursor rendering information.
cursor: {
// By default, we don't handle cursor inversion on the shader.
self.cells.setCursor(null);
self.uniforms.cursor_pos = .{
std.math.maxInt(u16),
std.math.maxInt(u16),
};
// If we have preedit text, we don't setup a cursor
if (preedit != null) break :cursor;
// Prepare the cursor cell contents.
const style = cursor_style_ orelse break :cursor;
const cursor_color = self.cursor_color orelse self.default_cursor_color orelse color: {
if (self.cursor_invert) {
// Use the foreground color from the cell under the cursor, if any.
const sty = screen.cursor.page_pin.style(screen.cursor.page_cell);
break :color if (sty.flags.inverse)
// If the cell is reversed, use background color instead.
(sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color orelse self.default_background_color)
else
(sty.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color);
} else {
break :color self.foreground_color orelse self.default_foreground_color;
}
};
self.addCursor(screen, style, cursor_color);
// If the cursor is visible then we set our uniforms.
if (style == .block and screen.viewportIsBottom()) {
const wide = screen.cursor.page_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 => screen.cursor.x,
.spacer_tail => screen.cursor.x -| 1,
},
screen.cursor.y,
};
self.uniforms.bools.cursor_wide = switch (wide) {
.narrow, .spacer_head => false,
.wide, .spacer_tail => true,
};
const uniform_color = if (self.cursor_invert) blk: {
// Use the background color from the cell under the cursor, if any.
const sty = screen.cursor.page_pin.style(screen.cursor.page_cell);
break :blk if (sty.flags.inverse)
// If the cell is reversed, use foreground color instead.
(sty.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color)
else
(sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color orelse self.default_background_color);
} else if (self.config.cursor_text) |txt|
txt
else
self.background_color orelse self.default_background_color;
self.uniforms.cursor_color = .{
uniform_color.r,
uniform_color.g,
uniform_color.b,
255,
};
}
}
// Setup our preedit text.
if (preedit) |preedit_v| {
const range = preedit_range.?;
var x = range.x[0];
for (preedit_v.codepoints[range.cp_offset..]) |cp| {
self.addPreeditCell(cp, .{ .x = x, .y = range.y }) catch |err| {
log.warn("error building preedit cell, will be invalid x={} y={}, err={}", .{
x,
range.y,
err,
});
};
x += if (cp.wide) 2 else 1;
}
}
// Update that our cells rebuilt
self.cells_rebuilt = true;
// Log some things
// log.debug("rebuildCells complete cached_runs={}", .{
// self.font_shaper_cache.count(),
// });
}
/// 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, .{
.mode = .fg,
.grid_pos = .{ @intCast(x), @intCast(y) },
.constraint_width = 1,
.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, .{
.mode = .fg,
.grid_pos = .{ @intCast(x), @intCast(y) },
.constraint_width = 1,
.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, .{
.mode = .fg,
.grid_pos = .{ @intCast(x), @intCast(y) },
.constraint_width = 1,
.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,
cell_pin: terminal.Pin,
shaper_cell: font.shape.Cell,
shaper_run: font.shape.TextRun,
color: terminal.color.RGB,
alpha: u8,
) !void {
const rac = cell_pin.rowAndCell();
const cell = rac.cell;
// Render
const render = try self.font_grid.renderGlyph(
self.alloc,
shaper_run.font_index,
shaper_cell.glyph_index,
.{
.grid_metrics = self.grid_metrics,
.thicken = self.config.font_thicken,
.thicken_strength = self.config.font_thicken_strength,
},
);
// 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;
}
// We always use fg mode for sprite glyphs, since we know we never
// need to constrain them, and we don't have any color sprites.
//
// Otherwise we defer to `fgMode`.
const mode: shaderpkg.CellText.Mode =
if (render.glyph.sprite)
.fg
else switch (fgMode(
render.presentation,
cell_pin,
)) {
.normal => .fg,
.color => .fg_color,
.constrained => .fg_constrained,
.powerline => .fg_powerline,
};
try self.cells.add(self.alloc, .text, .{
.mode = mode,
.grid_pos = .{ @intCast(x), @intCast(y) },
.constraint_width = cell.gridWidth(),
.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,
screen: *terminal.Screen,
cursor_style: renderer.CursorStyle,
cursor_color: terminal.color.RGB,
) void {
// Add the cursor. We render the cursor over the wide character if
// we're on the wide character tail.
const wide, const x = cell: {
// The cursor goes over the screen cursor position.
const cell = screen.cursor.page_cell;
if (cell.wide != .spacer_tail or screen.cursor.x == 0)
break :cell .{ cell.wide == .wide, screen.cursor.x };
// If we're part of a wide character, we move the cursor back to
// the actual character.
const prev_cell = screen.cursorCellLeft(1);
break :cell .{ prev_cell.wide == .wide, screen.cursor.x - 1 };
};
const alpha: u8 = if (!self.focused) 255 else alpha: {
const alpha = 255 * self.config.cursor_opacity;
break :alpha @intFromFloat(@ceil(alpha));
};
const render = switch (cursor_style) {
.block,
.block_hollow,
.bar,
.underline,
=> render: {
const sprite: font.Sprite = switch (cursor_style) {
.block => .cursor_rect,
.block_hollow => .cursor_hollow_rect,
.bar => .cursor_bar,
.underline => .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(.{
.mode = .cursor,
.grid_pos = .{ x, screen.cursor.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),
},
});
}
fn addPreeditCell(
self: *Self,
cp: renderer.State.Preedit.Codepoint,
coord: terminal.Coordinate,
) !void {
// Preedit is rendered inverted
const bg = self.foreground_color orelse self.default_foreground_color;
const fg = self.background_color orelse self.default_background_color;
// Render the glyph for our preedit text
const render_ = self.font_grid.renderCodepoint(
self.alloc,
@intCast(cp.codepoint),
.regular,
.text,
.{ .grid_metrics = self.grid_metrics },
) catch |err| {
log.warn("error rendering preedit glyph err={}", .{err});
return;
};
const render = render_ orelse {
log.warn("failed to find font for preedit codepoint={X}", .{cp.codepoint});
return;
};
// Add our opaque background cell
self.cells.bgCell(coord.y, coord.x).* = .{
bg.r, bg.g, bg.b, 255,
};
if (cp.wide and coord.x < self.cells.size.columns - 1) {
self.cells.bgCell(coord.y, coord.x + 1).* = .{
bg.r, bg.g, bg.b, 255,
};
}
// Add our text
try self.cells.add(self.alloc, .text, .{
.mode = .fg,
.grid_pos = .{ @intCast(coord.x), @intCast(coord.y) },
.color = .{ fg.r, fg.g, 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),
},
});
}
/// 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);
}
};
}