mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 00:06:09 +03:00
font: implement many rendering, caching functions for SharedGrid
This commit is contained in:
@ -15,13 +15,16 @@ const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ziglyph = @import("ziglyph");
|
||||
const font = @import("main.zig");
|
||||
const Atlas = font.Atlas;
|
||||
const CodepointMap = font.CodepointMap;
|
||||
const Collection = font.Collection;
|
||||
const Discover = font.Discover;
|
||||
const DiscoveryDescriptor = font.discovery.Descriptor;
|
||||
const Face = font.Face;
|
||||
const Glyph = font.Glyph;
|
||||
const Library = font.Library;
|
||||
const Presentation = font.Presentation;
|
||||
const RenderOptions = font.face.RenderOptions;
|
||||
const SpriteFace = font.SpriteFace;
|
||||
const Style = font.Style;
|
||||
|
||||
@ -286,6 +289,52 @@ fn getIndexCodepointOverride(
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Returns the presentation for a specific font index. This is useful for
|
||||
/// determining what atlas is needed.
|
||||
pub fn getPresentation(self: *CodepointResolver, index: Collection.Index) !Presentation {
|
||||
if (index.special()) |sp| return switch (sp) {
|
||||
.sprite => .text,
|
||||
};
|
||||
|
||||
const face = try self.collection.getFace(index);
|
||||
return face.presentation;
|
||||
}
|
||||
|
||||
/// Render a glyph by glyph index into the given font atlas and return
|
||||
/// metadata about it.
|
||||
///
|
||||
/// This performs no caching, it is up to the caller to cache calls to this
|
||||
/// if they want. This will also not resize the atlas if it is full.
|
||||
///
|
||||
/// IMPORTANT: this renders by /glyph index/ and not by /codepoint/. The caller
|
||||
/// is expected to translate codepoints to glyph indexes in some way. The most
|
||||
/// trivial way to do this is to get the Face and call glyphIndex. If you're
|
||||
/// doing text shaping, the text shaping library (i.e. HarfBuzz) will automatically
|
||||
/// determine glyph indexes for a text run.
|
||||
pub fn renderGlyph(
|
||||
self: *CodepointResolver,
|
||||
alloc: Allocator,
|
||||
atlas: *Atlas,
|
||||
index: Collection.Index,
|
||||
glyph_index: u32,
|
||||
opts: RenderOptions,
|
||||
) !Glyph {
|
||||
// Special-case fonts are rendered directly.
|
||||
if (index.special()) |sp| switch (sp) {
|
||||
.sprite => return try self.sprite.?.renderGlyph(
|
||||
alloc,
|
||||
atlas,
|
||||
glyph_index,
|
||||
opts,
|
||||
),
|
||||
};
|
||||
|
||||
const face = try self.collection.getFace(index);
|
||||
const glyph = try face.renderGlyph(alloc, atlas, glyph_index, opts);
|
||||
// log.warn("GLYPH={}", .{glyph});
|
||||
return glyph;
|
||||
}
|
||||
|
||||
/// Packed array of booleans to indicate if a style is enabled or not.
|
||||
pub const StyleStatus = std.EnumArray(Style, bool);
|
||||
|
||||
|
@ -43,7 +43,7 @@ const log = std.log.scoped(.font_shared_grid);
|
||||
codepoints: std.AutoHashMapUnmanaged(CodepointKey, ?Collection.Index) = .{},
|
||||
|
||||
/// Cache for glyph renders into the atlas.
|
||||
glyphs: std.AutoHashMapUnmanaged(GlyphKey, Glyph) = .{},
|
||||
glyphs: std.AutoHashMapUnmanaged(GlyphKey, Render) = .{},
|
||||
|
||||
/// The texture atlas to store renders in. The Glyph data in the glyphs
|
||||
/// cache is dependent on the atlas matching.
|
||||
@ -59,7 +59,9 @@ resolver: CodepointResolver,
|
||||
metrics: Metrics,
|
||||
|
||||
/// The RwLock used to protect the shared grid. Callers are expected to use
|
||||
/// this directly if they need to i.e. access the atlas directly.
|
||||
/// this directly if they need to i.e. access the atlas directly. Because
|
||||
/// callers can use this lock directly, maintainers need to be extra careful
|
||||
/// to review call sites to ensure they are using the lock correctly.
|
||||
lock: std.Thread.RwLock,
|
||||
|
||||
/// Initialize the grid.
|
||||
@ -192,6 +194,76 @@ pub fn hasCodepoint(
|
||||
);
|
||||
}
|
||||
|
||||
pub const Render = struct {
|
||||
glyph: Glyph,
|
||||
presentation: Presentation,
|
||||
};
|
||||
|
||||
/// Render a glyph. This automatically determines the correct texture
|
||||
/// atlas to use and caches the result.
|
||||
pub fn renderGlyph(
|
||||
self: *SharedGrid,
|
||||
alloc: Allocator,
|
||||
index: Collection.Index,
|
||||
glyph_index: u32,
|
||||
opts: RenderOptions,
|
||||
) !Render {
|
||||
const key: GlyphKey = .{ .index = index, .glyph = glyph_index, .opts = opts };
|
||||
|
||||
// Fast path: the cache has the value. This is almost always true and
|
||||
// only requires a read lock.
|
||||
{
|
||||
self.lock.lockShared();
|
||||
defer self.lock.unlockShared();
|
||||
if (self.glyphs.get(key)) |v| return v;
|
||||
}
|
||||
|
||||
// Slow path: we need to search this codepoint
|
||||
self.lock.lock();
|
||||
defer self.lock.unlock();
|
||||
|
||||
const gop = try self.glyphs.getOrPut(alloc, key);
|
||||
if (gop.found_existing) return gop.value_ptr.*;
|
||||
|
||||
// Get the presentation to determine what atlas to use
|
||||
const p = try self.resolver.getPresentation(index);
|
||||
const atlas: *font.Atlas = switch (p) {
|
||||
.text => &self.atlas_greyscale,
|
||||
.emoji => &self.atlas_color,
|
||||
};
|
||||
|
||||
// Render into the atlas
|
||||
const glyph = self.resolver.renderGlyph(
|
||||
alloc,
|
||||
atlas,
|
||||
index,
|
||||
glyph_index,
|
||||
opts,
|
||||
) catch |err| switch (err) {
|
||||
// If the atlas is full, we resize it
|
||||
error.AtlasFull => blk: {
|
||||
try atlas.grow(alloc, atlas.size * 2);
|
||||
break :blk try self.resolver.renderGlyph(
|
||||
alloc,
|
||||
atlas,
|
||||
index,
|
||||
glyph_index,
|
||||
opts,
|
||||
);
|
||||
},
|
||||
|
||||
else => return err,
|
||||
};
|
||||
|
||||
// Cache and return
|
||||
gop.value_ptr.* = .{
|
||||
.glyph = glyph,
|
||||
.presentation = p,
|
||||
};
|
||||
|
||||
return gop.value_ptr.*;
|
||||
}
|
||||
|
||||
const CodepointKey = struct {
|
||||
style: Style,
|
||||
codepoint: u32,
|
||||
|
@ -152,7 +152,7 @@ pub const Presentation = enum(u1) {
|
||||
};
|
||||
|
||||
/// A FontIndex that can be used to use the sprite font directly.
|
||||
pub const sprite_index = Group.FontIndex.initSpecial(.sprite);
|
||||
pub const sprite_index = Collection.Index.initSpecial(.sprite);
|
||||
|
||||
test {
|
||||
// For non-wasm we want to test everything we can
|
||||
|
@ -1874,7 +1874,7 @@ fn updateCell(
|
||||
// If the cell has a character, draw it
|
||||
if (cell.hasText()) fg: {
|
||||
// Render
|
||||
const glyph = try self.font_group.renderGlyph(
|
||||
const render = try self.font_grid.renderGlyph(
|
||||
self.alloc,
|
||||
shaper_run.font_index,
|
||||
shaper_cell.glyph_index orelse break :fg,
|
||||
@ -1885,9 +1885,8 @@ fn updateCell(
|
||||
);
|
||||
|
||||
const mode: mtl_shaders.Cell.Mode = switch (try fgMode(
|
||||
&self.font_group.group,
|
||||
render.presentation,
|
||||
cell_pin,
|
||||
shaper_run,
|
||||
)) {
|
||||
.normal => .fg,
|
||||
.color => .fg_color,
|
||||
@ -1900,11 +1899,11 @@ fn updateCell(
|
||||
.cell_width = cell.gridWidth(),
|
||||
.color = .{ colors.fg.r, colors.fg.g, colors.fg.b, alpha },
|
||||
.bg_color = bg,
|
||||
.glyph_pos = .{ glyph.atlas_x, glyph.atlas_y },
|
||||
.glyph_size = .{ glyph.width, glyph.height },
|
||||
.glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
|
||||
.glyph_size = .{ render.glyph.width, render.glyph.height },
|
||||
.glyph_offset = .{
|
||||
glyph.offset_x + shaper_cell.x_offset,
|
||||
glyph.offset_y + shaper_cell.y_offset,
|
||||
render.glyph.offset_x + shaper_cell.x_offset,
|
||||
render.glyph.offset_y + shaper_cell.y_offset,
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -1919,7 +1918,7 @@ fn updateCell(
|
||||
.curly => .underline_curly,
|
||||
};
|
||||
|
||||
const glyph = try self.font_group.renderGlyph(
|
||||
const render = try self.font_grid.renderGlyph(
|
||||
self.alloc,
|
||||
font.sprite_index,
|
||||
@intFromEnum(sprite),
|
||||
@ -1937,9 +1936,9 @@ fn updateCell(
|
||||
.cell_width = cell.gridWidth(),
|
||||
.color = .{ color.r, color.g, color.b, alpha },
|
||||
.bg_color = bg,
|
||||
.glyph_pos = .{ glyph.atlas_x, glyph.atlas_y },
|
||||
.glyph_size = .{ glyph.width, glyph.height },
|
||||
.glyph_offset = .{ glyph.offset_x, glyph.offset_y },
|
||||
.glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
|
||||
.glyph_size = .{ render.glyph.width, render.glyph.height },
|
||||
.glyph_offset = .{ render.glyph.offset_x, render.glyph.offset_y },
|
||||
});
|
||||
}
|
||||
|
||||
@ -1988,7 +1987,7 @@ fn addCursor(
|
||||
.underline => .underline,
|
||||
};
|
||||
|
||||
const glyph = self.font_group.renderGlyph(
|
||||
const render = self.font_grid.renderGlyph(
|
||||
self.alloc,
|
||||
font.sprite_index,
|
||||
@intFromEnum(sprite),
|
||||
@ -2010,9 +2009,9 @@ fn addCursor(
|
||||
.cell_width = if (wide) 2 else 1,
|
||||
.color = .{ color.r, color.g, color.b, alpha },
|
||||
.bg_color = .{ 0, 0, 0, 0 },
|
||||
.glyph_pos = .{ glyph.atlas_x, glyph.atlas_y },
|
||||
.glyph_size = .{ glyph.width, glyph.height },
|
||||
.glyph_offset = .{ glyph.offset_x, glyph.offset_y },
|
||||
.glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
|
||||
.glyph_size = .{ render.glyph.width, render.glyph.height },
|
||||
.glyph_offset = .{ render.glyph.offset_x, render.glyph.offset_y },
|
||||
});
|
||||
|
||||
return &self.cells.items[self.cells.items.len - 1];
|
||||
@ -2024,6 +2023,8 @@ fn addPreeditCell(
|
||||
x: usize,
|
||||
y: usize,
|
||||
) !void {
|
||||
if (true) @panic("TODO"); // TODO(fontmem)
|
||||
|
||||
// Preedit is rendered inverted
|
||||
const bg = self.foreground_color;
|
||||
const fg = self.background_color;
|
||||
|
@ -20,11 +20,9 @@ pub const FgMode = enum {
|
||||
/// meant to be called from the typical updateCell function within a
|
||||
/// renderer.
|
||||
pub fn fgMode(
|
||||
group: *font.Group,
|
||||
presentation: font.Presentation,
|
||||
cell_pin: terminal.Pin,
|
||||
shaper_run: font.shape.TextRun,
|
||||
) !FgMode {
|
||||
const presentation = try group.presentationFromIndex(shaper_run.font_index);
|
||||
return switch (presentation) {
|
||||
// Emoji is always full size and color.
|
||||
.emoji => .color,
|
||||
|
Reference in New Issue
Block a user