Backlog/engine/papyrus/src/Font.zig

455 lines
17 KiB
Zig

const std = @import("std");
const c = @import("c.zig");
const core = @import("core");
const Vector2i = core.Vector2i;
const Vector2f = core.Vector2f;
const Name = core.Name;
const colors = core.colors;
const ColorRGBA8 = colors.ColorRGBA8;
const Color = colors.Color;
const LocText = @import("localization.zig").LocText;
const utils = @import("utils.zig");
const loadFileAlloc = utils.loadFileAlloc;
const BmpWriter = @import("BmpRenderer.zig").BmpWriter;
name: Name,
atlas: *FontAtlas,
pub fn setRendererHash(self: *@This(), hash: u32) void {
self.atlas.rendererHash = hash;
}
pub const FontCreateOpts = struct {
isMonospace: bool = false,
isSDF: bool = true,
};
// this is going to be interesting... perhaps fonts aren't something that i need to actually make
// a cooked format for.
//
// instead I should make something like a glyph cache
pub const FontAtlas = struct {
font: c.stbtt_fontinfo = undefined,
allocator: std.mem.Allocator,
fileContent: []const u8,
fromArchive: bool = false,
filePath: []const u8,
rendererHash: u32 = 0, // optional field to associate this atlas with an identifier to the renderer implementation
isEmbedded: bool = false,
isSDF: bool = false,
fontSize: f32,
atlasSize: Vector2i = .{},
glyphMax: Vector2i = .{},
glyphStride: i32 = 0,
scale: f32 = 0,
lineSize: f32 = 0,
isMonospace: bool = false,
glyphMetrics: [256]Vector2i = undefined,
glyphBox0: [256]Vector2i = undefined,
glyphBox1: [256]Vector2i = undefined,
hasGlyph: [256]bool = undefined,
meshes: [256][4]Vector2f = undefined,
glyphCoordinates: [256][2]Vector2f = undefined,
atlasBuffer: ?[]u8,
const defaultFontEmbed = @embedFile("fonts/Roboto-Regular.ttf");
const defaultMonoEmbed = @embedFile("fonts/FiraMono-Medium.ttf");
pub fn makeBitmapRGBA(self: @This(), allocator: std.mem.Allocator) ![]u8 {
var buf = try allocator.alloc(u8, self.atlasBuffer.?.len * 4);
for (0..self.atlasBuffer.?.len) |i| {
buf[(i * 4) + 0] = self.atlasBuffer.?[i];
buf[(i * 4) + 1] = self.atlasBuffer.?[i];
buf[(i * 4) + 2] = self.atlasBuffer.?[i];
buf[(i * 4) + 3] = 1.0;
}
return buf;
}
pub fn initFontCacheEmbedded(allocator: std.mem.Allocator, fontName: []const u8, bytes: []const u8, fontSize: f32, opts: FontCreateOpts) !@This() {
const cacheFile = try std.fmt.allocPrint(allocator, ".fontcache/{s}.fontcache", .{fontName});
defer allocator.free(cacheFile);
std.fs.cwd().access(cacheFile, .{}) catch {
const rv = try initEmbeddedFont(allocator, bytes, fontSize, opts);
core.engine_log("[Fontcache] creating font cache for {s} font", .{fontName});
var cachedFontArchive = std.ArrayList(u8).init(allocator);
defer cachedFontArchive.deinit();
try rv.saveToArchive(&cachedFontArchive);
try std.fs.cwd().makePath(".fontcache");
const file = try std.fs.cwd().createFile(cacheFile, .{});
defer file.close();
try file.writeAll(cachedFontArchive.items);
return rv;
};
core.engine_log("[Fontcache] loading font cache for {s} font", .{fontName});
const archiveBytes = try loadFileAlloc(cacheFile, 8, allocator);
defer allocator.free(archiveBytes);
var rv = try initFromArchive(allocator, archiveBytes);
rv.fromArchive = true;
return rv;
}
pub fn initDefaultBitmapFont(allocator: std.mem.Allocator, fontSize: f32) !@This() {
return try initFontCacheEmbedded(allocator, "bitmap", defaultFontEmbed, fontSize, .{ .isSDF = false });
}
pub fn initMonoFont(allocator: std.mem.Allocator, fontSize: f32) !@This() {
return try initFontCacheEmbedded(allocator, "monospace", defaultMonoEmbed, fontSize, .{ .isMonospace = true });
}
pub fn initDefaultFont(allocator: std.mem.Allocator, fontSize: f32) !@This() {
return try initFontCacheEmbedded(allocator, "default", defaultFontEmbed, fontSize, .{});
}
pub fn initEmbeddedFont(allocator: std.mem.Allocator, fontContent: []const u8, fontSize: f32, opts: FontCreateOpts) !@This() {
var self = @This(){
.allocator = allocator,
.filePath = "embedded_file",
.fileContent = fontContent,
.atlasBuffer = null,
.fontSize = fontSize,
.isSDF = opts.isSDF,
.isEmbedded = true,
.isMonospace = opts.isMonospace,
};
_ = c.stbtt_InitFont(
&self.font,
self.fileContent.ptr,
c.stbtt_GetFontOffsetForIndex(self.fileContent.ptr, 0),
);
try self.createAtlas();
return self;
}
pub fn initFromFileSDF(allocator: std.mem.Allocator, file: []const u8, fontSize: f32, opts: FontCreateOpts) !@This() {
var self = @This(){
.allocator = allocator,
.filePath = file,
.fileContent = try loadFileAlloc(file, 8, allocator),
.atlasBuffer = null,
.fontSize = fontSize,
.isSDF = true,
.isMonospace = opts.isMonospace,
};
_ = c.stbtt_InitFont(
&self.font,
self.fileContent.ptr,
c.stbtt_GetFontOffsetForIndex(self.fileContent.ptr, 0),
);
try self.createAtlas();
return self;
}
// destroys all bitmaps created during the setup process.
// if the bitmaps are already uploaded to the GPU, you wouldnt need to
// keep them mapped
pub fn cleanUp(self: *@This()) void {
if (self.atlasBuffer) |buffer| {
self.atlasBuffer = null;
self.allocator.free(buffer);
}
}
// creates a font atlas from
pub fn initFromFile(allocator: std.mem.Allocator, file: []const u8, fontSize: f32, opts: FontCreateOpts) !@This() {
var self = @This(){
.allocator = allocator,
.filePath = file,
.fileContent = try loadFileAlloc(file, 8, allocator),
.atlasBuffer = null,
.fontSize = fontSize,
.isMonospace = opts.isMonospace,
};
_ = c.stbtt_InitFont(&self.font, self.fileContent.ptr, c.stbtt_GetFontOffsetForIndex(self.fileContent.ptr, 0));
try self.createAtlas();
return self;
}
pub fn initFromArchive(allocator: std.mem.Allocator, archive: []const u8) !@This() {
var self = @This(){
.allocator = allocator,
.filePath = "from_archive",
.fileContent = undefined,
.fromArchive = true,
.atlasBuffer = null,
.fontSize = 0,
.isMonospace = false,
};
try self.loadFromBytes(archive);
return self;
}
pub fn saveToArchive(self: @This(), out: *std.ArrayList(u8)) !void {
var writer = out.writer();
try writer.writeInt(u8, @intFromBool(self.isSDF), .little); // isSDF
try writer.writeInt(u32, @bitCast(self.fontSize), .little); // fontSize: f32 = 0,
try writer.writeStruct(self.atlasSize); // atlasSize: Vector2i = .{},
try writer.writeStruct(self.glyphMax); // glyphMax: Vector2i = .{},
try writer.writeInt(i32, self.glyphStride, .little); // glyphStride: i32 = 0,
try writer.writeInt(u32, @bitCast(self.scale), .little); // scale: f32 = 0,
try writer.writeInt(u32, @bitCast(self.lineSize), .little); // lineSize: f32 = 0,
try writer.writeInt(u8, @intFromBool(self.isMonospace), .little); // isMonospace: bool = false,
for (self.glyphMetrics) |x| { // glyphMetrics: [256]Vector2i = undefined,
try writer.writeStruct(x);
}
for (self.glyphBox0) |x| { // glyphBox0: [256]Vector2i = undefined,
try writer.writeStruct(x);
}
for (self.glyphBox1) |x| { // glyphBox1: [256]Vector2i = undefined,
try writer.writeStruct(x);
}
for (self.hasGlyph) |x| { // hasGlyph: [256]bool = undefined,
try writer.writeInt(u8, @intFromBool(x), .little);
}
for (self.meshes) |x| { // meshes: [256][4]Vector2f = undefined,
for (x) |y| {
try writer.writeStruct(y);
}
}
for (self.glyphCoordinates) |x| { // glyphCoordinates: [256][2]Vector2f = undefined,
for (x) |y| {
try writer.writeStruct(y);
}
}
if (self.atlasBuffer) |buffer| { // atlasBuffer: ?[]u8,
try writer.writeInt(u32, @intCast(buffer.len), .little);
try writer.writeAll(buffer);
}
}
pub fn loadFromBytes(self: *@This(), archive: []const u8) !void {
var fbs = std.io.fixedBufferStream(archive);
var reader = fbs.reader();
self.isSDF = (try reader.readByte()) == 1; // isSDF: bool = false,
self.fontSize = @bitCast(try reader.readInt(u32, .little)); // fontSize: f32,
self.atlasSize = try reader.readStruct(Vector2i); // atlasSize: Vector2i = .{},
self.glyphMax = try reader.readStruct(Vector2i); // glyphMax: Vector2i = .{},
self.glyphStride = try reader.readInt(i32, .little); // glyphStride: i32 = 0,
self.scale = @bitCast(try reader.readInt(u32, .little)); // scale: f32 = 0,
self.lineSize = @bitCast(try reader.readInt(u32, .little)); // lineSize: f32 = 0,
self.isMonospace = (try reader.readByte()) == 1; // isMonospace: bool = false,
for (self.glyphMetrics, 0..) |_, i| { // glyphMetrics: [256]Vector2i = undefined,
self.glyphMetrics[i] = try reader.readStruct(Vector2i);
}
for (self.glyphBox0, 0..) |_, i| { // glyphBox0: [256]Vector2i = undefined,
self.glyphBox0[i] = try reader.readStruct(Vector2i);
}
for (self.glyphBox1, 0..) |_, i| { // glyphBox1: [256]Vector2i = undefined,
self.glyphBox1[i] = try reader.readStruct(Vector2i);
}
for (self.hasGlyph, 0..) |_, i| { // hasGlyph: [256]bool = undefined,
self.hasGlyph[i] = (try reader.readByte()) == 1;
}
// meshes: [256][4]Vector2f = undefined,
for (self.meshes, 0..) |_, i| { // meshes: [256]Vector2i = undefined,
self.meshes[i][0] = try reader.readStruct(Vector2f);
self.meshes[i][1] = try reader.readStruct(Vector2f);
self.meshes[i][2] = try reader.readStruct(Vector2f);
self.meshes[i][3] = try reader.readStruct(Vector2f);
}
for (self.glyphCoordinates, 0..) |_, i| { // glyphCoordinates: [256][2]Vector2f = undefined,
self.glyphCoordinates[i][0] = try reader.readStruct(Vector2f);
self.glyphCoordinates[i][1] = try reader.readStruct(Vector2f);
}
const size = try reader.readInt(u32, .little);
self.atlasBuffer = try reader.readAllAlloc(self.allocator, size); // atlasBuffer: ?[]u8,
}
fn createAtlas(self: *@This()) !void {
const glyphCount = 256;
var glyphs: [glyphCount][*c]u8 = undefined;
var max: Vector2i = .{};
var ch: u32 = 0;
self.scale = c.stbtt_ScaleForPixelHeight(&self.font, self.fontSize);
while (ch < glyphCount) : (ch += 1) {
self.glyphMetrics[ch] = .{ .x = 0, .y = 0 };
self.glyphBox1[ch] = .{ .x = 0, .y = 0 };
if (self.isSDF) {
glyphs[ch] = c.stbtt_GetCodepointSDF(
&self.font,
c.stbtt_ScaleForPixelHeight(&self.font, self.fontSize),
@as(c_int, @intCast(ch)),
5,
180,
36,
&self.glyphMetrics[ch].x,
&self.glyphMetrics[ch].y,
&self.glyphBox1[ch].x,
&self.glyphBox1[ch].y,
);
} else {
glyphs[ch] = c.stbtt_GetCodepointBitmap(
&self.font,
0,
c.stbtt_ScaleForPixelHeight(&self.font, self.fontSize),
@as(c_int, @intCast(ch)),
&self.glyphMetrics[ch].x,
&self.glyphMetrics[ch].y,
&self.glyphBox1[ch].x,
&self.glyphBox1[ch].y,
);
}
if (self.glyphMetrics[ch].x > max.x) {
//if (self.glyphMetrics[ch].x < 128) {
max.x = self.glyphMetrics[ch].x;
//}
}
if (self.glyphMetrics[ch].y > max.y) {
//if (self.glyphMetrics[ch].y < 128) {
max.y = self.glyphMetrics[ch].y;
//}
}
}
self.glyphMax = max;
// allocate the atlasBuffer, just a linear strip
self.atlasSize = .{ .x = (max.x + 1) * glyphCount, .y = (max.y + 1) };
self.glyphStride = max.x + 1;
self.atlasBuffer = try self.allocator.alloc(u8, @as(usize, @intCast(self.atlasSize.x * self.atlasSize.y)));
@memset(self.atlasBuffer.?, 0x0);
// write bitmaps into the atlas buffer
ch = 0;
while (ch < glyphCount) : (ch += 1) {
if (@intFromPtr(glyphs[ch]) == 0) {
self.hasGlyph[ch] = false;
continue;
}
self.hasGlyph[ch] = true;
const tl = Vector2i{ .x = @as(i32, @intCast(ch)) * (max.x + 1), .y = 0 };
const maxCol = self.glyphMetrics[ch].x;
const maxRow = self.glyphMetrics[ch].y;
var col: i32 = 0;
var row: i32 = 0;
// get floating point coordinates for rendering to opengl/vulkan
// get top left coordinates
self.glyphCoordinates[ch][0] = .{
.x = @as(f32, @floatFromInt(tl.x)) / @as(f32, @floatFromInt(self.atlasSize.x)),
.y = @as(f32, @floatFromInt(tl.y)) / @as(f32, @floatFromInt(self.atlasSize.y)),
};
// get bottom right coordinates
self.glyphCoordinates[ch][1] = .{
.x = @as(f32, @floatFromInt(tl.x + self.glyphMetrics[ch].x)) / @as(f32, @floatFromInt(self.atlasSize.x)),
.y = @as(f32, @floatFromInt(tl.y + self.glyphMetrics[ch].y)) / @as(f32, @floatFromInt(self.atlasSize.y)),
};
while (row < maxRow) : (row += 1) {
col = 0;
while (col < maxCol) : (col += 1) {
const pixelOffset = @as(usize, @intCast(((row + tl.y) * self.atlasSize.x) + (col + tl.x)));
self.atlasBuffer.?[pixelOffset] = glyphs[ch][@as(usize, @intCast((row * maxCol) + col))];
}
}
const xSize = @as(f32, @floatFromInt(self.glyphMetrics[ch].x)) * self.scale;
const ySize = @as(f32, @floatFromInt(self.glyphMetrics[ch].y)) * self.scale;
const xOff = @as(f32, @floatFromInt(self.glyphBox1[ch].x)) * self.scale;
const yOff = @as(f32, @floatFromInt(self.glyphBox1[ch].y)) * self.scale;
// create an appropriately proportioned mesh based on the scale.
self.meshes[ch][0] = .{ .x = xOff, .y = ySize + yOff }; // TL
self.meshes[ch][1] = .{ .x = xSize + xOff, .y = ySize + yOff }; // TR
self.meshes[ch][2] = .{ .x = xSize + xOff, .y = 0 + yOff }; // BR
self.meshes[ch][3] = .{ .x = xOff, .y = yOff }; // BL
}
ch = 0;
while (ch < glyphCount) : (ch += 1) {
if (glyphs[ch]) |ptr| {
if (self.isSDF) {
c.stbtt_FreeSDF(ptr, null);
} else {
c.stbtt_FreeBitmap(ptr, null);
}
}
}
if (self.isMonospace) {
var i: usize = 0;
const fixedSize = self.glyphBox1['a'].x + self.glyphMetrics['a'].x;
while (i < self.glyphMetrics.len) : (i += 1) {
self.glyphMetrics[i].x = fixedSize - self.glyphBox1[i].x;
}
}
}
pub fn dumpBufferToFile(self: *@This(), fileName: []const u8) !void {
var renderer = try BmpWriter.init(std.testing.allocator, self.atlasSize);
var row: i32 = 0;
var col: i32 = 0;
while (row < renderer.extent.y) : (row += 1) {
col = 0;
while (col < renderer.extent.x) : (col += 1) {
const pixelOffset = @as(usize, @intCast((renderer.extent.x * (row)) + col));
const pixelOffset2 = @as(usize, @intCast((renderer.extent.x * (self.atlasSize.y - row - 1)) + col));
renderer.pixelBuffer[pixelOffset * 3 + 0] = self.atlasBuffer.?[pixelOffset2];
renderer.pixelBuffer[pixelOffset * 3 + 1] = self.atlasBuffer.?[pixelOffset2];
renderer.pixelBuffer[pixelOffset * 3 + 2] = self.atlasBuffer.?[pixelOffset2];
}
}
try renderer.writeOut(fileName);
defer renderer.deinit();
}
pub fn deinit(self: *@This()) void {
if (self.atlasBuffer != null) {
self.allocator.free(self.atlasBuffer.?);
}
if (!self.isEmbedded and !self.fromArchive)
self.allocator.free(self.fileContent);
}
};