font: implement many rendering, caching functions for SharedGrid

This commit is contained in:
Mitchell Hashimoto
2024-04-05 20:50:35 -07:00
parent c88137d254
commit c45747bf1f
5 changed files with 141 additions and 21 deletions

View File

@ -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);

View File

@ -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,

View File

@ -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

View File

@ -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;

View File

@ -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,