gfx: integrate new font stack into Text module

Helps hexops/mach#877

Signed-off-by: Stephen Gutekanst <stephen@hexops.com>
This commit is contained in:
Stephen Gutekanst 2023-10-05 18:57:49 -07:00
parent 34259ed1b8
commit 1f8962408c
4 changed files with 66 additions and 104 deletions

View file

@ -1,10 +1,10 @@
const std = @import("std"); const std = @import("std");
const core = @import("mach-core");
const gpu = core.gpu;
const ecs = @import("mach-ecs");
const Engine = @import("../engine.zig").Engine;
const FontRenderer = @import("font.zig").FontRenderer;
const mach = @import("../main.zig"); const mach = @import("../main.zig");
const core = mach.core;
const gpu = mach.gpu;
const ecs = mach.ecs;
const Engine = mach.Engine;
const gfx = mach.gfx;
const math = mach.math; const math = mach.math;
const vec2 = math.vec2; const vec2 = math.vec2;
@ -48,7 +48,7 @@ pub const components = struct {
pub const text = []const u8; pub const text = []const u8;
/// The font to be rendered. /// The font to be rendered.
pub const font = FontRenderer; pub const font_name = []const u8;
/// Font size in pixels. To convert from points to pixels, multiply by `points_to_pixels`. /// Font size in pixels. To convert from points to pixels, multiply by `points_to_pixels`.
pub const font_size = f32; pub const font_size = f32;
@ -83,19 +83,18 @@ const Glyph = extern struct {
text_index: u32, text_index: u32,
}; };
const GlyphDetail = struct { const GlyphKey = struct {
codepoint: u21 = 0, index: u32,
font: FontRenderer,
// Auto Hashing doesn't work for floats, so we bitcast to integer. // Auto Hashing doesn't work for floats, so we bitcast to integer.
size: u32, size: u32,
}; };
const RegionMap = std.AutoArrayHashMapUnmanaged(u21, GlyphDetail); const RegionMap = std.AutoArrayHashMapUnmanaged(GlyphKey, gfx.Atlas.Region);
const Pipeline = struct { const Pipeline = struct {
render: *gpu.RenderPipeline, render: *gpu.RenderPipeline,
texture_sampler: *gpu.Sampler, texture_sampler: *gpu.Sampler,
texture: *gpu.Texture, texture: *gpu.Texture,
texture_atlas: mach.Atlas, texture_atlas: gfx.Atlas,
texture2: ?*gpu.Texture, texture2: ?*gpu.Texture,
texture3: ?*gpu.Texture, texture3: ?*gpu.Texture,
texture4: ?*gpu.Texture, texture4: ?*gpu.Texture,
@ -198,7 +197,7 @@ pub fn machGfxTextInitPipeline(
.render_attachment = true, .render_attachment = true,
}, },
}); });
const texture_atlas = try mach.Atlas.init( const texture_atlas = try gfx.Atlas.init(
engine.allocator, engine.allocator,
img_size.width, img_size.width,
.rgba, .rgba,
@ -355,7 +354,7 @@ pub fn machGfxTextUpdated(
.pipeline, .pipeline,
.transform, .transform,
.text, .text,
.font, .font_name,
.font_size, .font_size,
.color, .color,
} }, } },
@ -387,27 +386,44 @@ pub fn machGfxTextUpdated(
// TODO: this is very expensive and shouldn't be done here, should be done only on detected // TODO: this is very expensive and shouldn't be done here, should be done only on detected
// text change. // text change.
const px_density = 2.0; const px_density = 2.0;
var fonts = archetype.slice(.mach_gfx_text, .font); var font_names = archetype.slice(.mach_gfx_text, .font_name);
var font_sizes = archetype.slice(.mach_gfx_text, .font_size); var font_sizes = archetype.slice(.mach_gfx_text, .font_size);
var texts = archetype.slice(.mach_gfx_text, .text); var texts = archetype.slice(.mach_gfx_text, .text);
for (fonts, font_sizes, texts) |font, font_size, text| { for (font_names, font_sizes, texts) |font_name, font_size, text| {
var offset_x: f32 = 0.0; _ = font_name;
var offset_y: f32 = 0.0; var origin_x: f32 = 0.0;
var utf8 = (try std.unicode.Utf8View.init(text)).iterator(); var origin_y: f32 = 0.0;
var glyph_detail = GlyphDetail{
.font = font,
.size = @bitCast(font_size),
};
while (utf8.nextCodepoint()) |codepoint| {
const m = try font.measure(codepoint, font_size * px_density);
glyph_detail.codepoint = codepoint;
if (codepoint != '\n') {
var region = try pipeline.regions.getOrPut(engine.allocator, glyph_detail);
if (!region.found_existing) {
const glyph = try font.render(codepoint, font_size * px_density);
if (glyph.bitmap) |bitmap| { // Load a font
var glyph_atlas_region = try pipeline.texture_atlas.reserve(engine.allocator, glyph.width, glyph.height); // TODO: resolve font by name, not hard-code
const font_bytes = @import("font-assets").fira_sans_regular_ttf;
var font = try gfx.Font.initBytes(font_bytes);
defer font.deinit(engine.allocator);
// Create a text shaper
var run = try gfx.TextRun.init();
run.font_size_px = font_size;
run.px_density = 2; // TODO
defer run.deinit();
run.addText(text);
try font.shape(&run);
while (run.next()) |glyph| {
const codepoint = text[glyph.cluster];
// TODO: use flags(?) to detect newline, or at least something more reliable?
if (codepoint != '\n') {
var region = try pipeline.regions.getOrPut(engine.allocator, .{
.index = glyph.glyph_index,
.size = @bitCast(font_size),
});
if (!region.found_existing) {
const rendered_glyph = try font.render(engine.allocator, glyph.glyph_index, .{
.font_size_px = run.font_size_px,
});
if (rendered_glyph.bitmap) |bitmap| {
var glyph_atlas_region = try pipeline.texture_atlas.reserve(engine.allocator, rendered_glyph.width, rendered_glyph.height);
pipeline.texture_atlas.set(glyph_atlas_region, @as([*]const u8, @ptrCast(bitmap.ptr))[0 .. bitmap.len * 4]); pipeline.texture_atlas.set(glyph_atlas_region, @as([*]const u8, @ptrCast(bitmap.ptr))[0 .. bitmap.len * 4]);
texture_update = true; texture_update = true;
@ -421,7 +437,7 @@ pub fn machGfxTextUpdated(
region.value_ptr.* = glyph_atlas_region; region.value_ptr.* = glyph_atlas_region;
} else { } else {
// whitespace // whitespace
region.value_ptr.* = mach.Atlas.Region{ region.value_ptr.* = gfx.Atlas.Region{
.width = 0, .width = 0,
.height = 0, .height = 0,
.x = 0, .x = 0,
@ -430,41 +446,33 @@ pub fn machGfxTextUpdated(
} }
} }
// Note: render(font_size) and render(font_size*px_density) is not equal in
// m.size.x() and m.size.x()*px_density, because font rendering may handle rounding
// differently. We always work in native pixels, and then convert to virtual pixels
// right before display in order to keep everything accurate.
//
// Also note that e.g. font_size*px_density may result in a different horizontal
// bearing than font_size with horizontal bearing * 2.0. These subtleties are
// important and decided by the font itself.
const r = region.value_ptr.*; const r = region.value_ptr.*;
std.debug.assert(r.width == @as(u32, @intFromFloat(m.size.x()))); const size = vec2(@floatFromInt(r.width), @floatFromInt(r.height));
std.debug.assert(r.height == @as(u32, @intFromFloat(m.size.y())));
try glyphs.append(engine.allocator, .{ try glyphs.append(engine.allocator, .{
.pos = vec2( .pos = vec2(
offset_x + m.bearing_horizontal.x(), origin_x + glyph.offset.x(),
offset_y - (m.size.y() - m.bearing_horizontal.y()), origin_y - (size.y() - glyph.offset.y()),
).divScalar(px_density), ).divScalar(px_density),
.size = m.size.divScalar(px_density), .size = size.divScalar(px_density),
.text_index = 0, .text_index = 0,
.uv_pos = vec2(@floatFromInt(r.x), @floatFromInt(r.y)), .uv_pos = vec2(@floatFromInt(r.x), @floatFromInt(r.y)),
}); });
pipeline.num_glyphs += 1; pipeline.num_glyphs += 1;
} }
if (codepoint == '\n') { if (codepoint == '\n') {
offset_x = 0; origin_x = 0;
offset_y -= m.advance.y(); origin_y -= font_size;
} else { } else {
offset_x += m.advance.x(); origin_x += glyph.advance.x();
} }
} }
} }
} }
encoder.writeBuffer(pipeline.glyphs, 0, glyphs.items); // TODO: could writeBuffer check for zero?
glyphs.deinit(engine.allocator); if (glyphs.items.len > 0) encoder.writeBuffer(pipeline.glyphs, 0, glyphs.items);
defer glyphs.deinit(engine.allocator);
if (texture_update) { if (texture_update) {
// rgba32_pixels // rgba32_pixels
// TODO: use proper texture dimensions here // TODO: use proper texture dimensions here

View file

@ -1,41 +0,0 @@
const math = @import("../main.zig").math;
const std = @import("std");
/// An interface that can render Unicode codepoints into glyphs.
pub const FontRenderer = struct {
ptr: *anyopaque,
vtable: *const VTable,
pub const VTable = struct {
render: *const fn (ctx: *anyopaque, codepoint: u21, size: f32) error{RenderError}!Glyph,
measure: *const fn (ctx: *anyopaque, codepoint: u21, size: f32) error{MeasureError}!GlyphMetrics,
};
pub fn render(r: FontRenderer, codepoint: u21, size: f32) error{RenderError}!Glyph {
return r.vtable.render(r.ptr, codepoint, size);
}
pub fn measure(r: FontRenderer, codepoint: u21, size: f32) error{MeasureError}!GlyphMetrics {
return r.vtable.measure(r.ptr, codepoint, size);
}
};
pub const RGBA32 = extern struct {
r: u8,
g: u8,
b: u8,
a: u8,
};
pub const Glyph = struct {
bitmap: ?[]const RGBA32,
width: u32,
height: u32,
};
pub const GlyphMetrics = struct {
size: math.Vec2,
advance: math.Vec2,
bearing_horizontal: math.Vec2,
bearing_vertical: math.Vec2,
};

View file

@ -1,14 +1,11 @@
pub const util = @import("util.zig"); // TODO: banish 2-level deep namespaces pub const util = @import("util.zig"); // TODO: banish 2-level deep namespaces
pub const Sprite = @import("Sprite.zig");
pub const Atlas = @import("atlas/Atlas.zig"); pub const Atlas = @import("atlas/Atlas.zig");
// ECS modules
pub const Sprite = @import("Sprite.zig");
pub const Text = @import("Text.zig"); pub const Text = @import("Text.zig");
// TODO: integrate font rendering // Fonts
// pub const RGBA32 = @import("font.zig").RGBA32;
// pub const FontRenderer = @import("font.zig").FontRenderer;
// pub const Glyph = @import("font.zig").Glyph;
// pub const GlyphMetrics = @import("font.zig").GlyphMetrics;
pub const Font = @import("font/main.zig").Font; pub const Font = @import("font/main.zig").Font;
pub const TextRun = @import("font/main.zig").TextRun; pub const TextRun = @import("font/main.zig").TextRun;
pub const Glyph = @import("font/main.zig").Glyph; pub const Glyph = @import("font/main.zig").Glyph;

View file

@ -362,15 +362,13 @@ pub fn Mat(
/// Matrix * Vector multiplication /// Matrix * Vector multiplication
pub inline fn mulVec(a: *const Matrix, b: *const ColVec) ColVec { pub inline fn mulVec(a: *const Matrix, b: *const ColVec) ColVec {
var result = [_]ColVec.T{0}**ColVec.n; var result = [_]ColVec.T{0} ** ColVec.n;
inline for (0..Matrix.rows) |row| { inline for (0..Matrix.rows) |row| {
inline for (0..ColVec.n) |i| { inline for (0..ColVec.n) |i| {
result[i] += a.v[row].v[i] * b.v[row]; result[i] += a.v[row].v[i] * b.v[row];
} }
} }
return vec.Vec(ColVec.n, ColVec.T){ return vec.Vec(ColVec.n, ColVec.T){ .v = result };
.v = result
};
} }
// TODO: the below code was correct in our old implementation, it just needs to be updated // TODO: the below code was correct in our old implementation, it just needs to be updated
@ -671,7 +669,7 @@ test "Mat3x3_mulVec_vec3" {
); );
const m = math.Mat3x3.mulVec(&mat, &v); const m = math.Mat3x3.mulVec(&mat, &v);
const expected = math.vec3(2,2,3); const expected = math.vec3(2, 2, 3);
try testing.expect(math.Vec3, expected).eql(m); try testing.expect(math.Vec3, expected).eql(m);
} }