Backlog/engine/ui/src/sgpu/textRenderer.zig

524 lines
18 KiB
Zig

// reusing the exact same setup that i used to have in neonwood.
//
const vec2 = text_frag.vec2;
const u8vec4 = text_frag.u8vec4;
pub const TextMeshVertex = extern struct {
position: vec2,
uv: vec2,
color: u8vec4,
// pad0: u32 = 0, hope it isnt needed..
};
pub fn addVertexAttributes(pci: *gpu.GPUGraphicsPipelineCreateInfo) !std.ArrayList(gpu.GPUVertexAttribute) {
return rend.renderer.addVertexAttributesFromStruct(TextMeshVertex, pci);
}
pub fn TextBufferList(comptime T: type) type {
return struct {
slice: []T,
count: u32 = 0,
pub fn init(s: []T) @This() {
return .{
.slice = s,
.count = 0,
};
}
pub fn push(self: *@This(), v: T) !void {
if (self.count < self.slice.len) {
self.slice[self.count] = v;
self.count += 1;
} else {
return error.OutOfMemory;
}
}
};
}
pub fn pushQuadToWriters(
topLeft: core.Vector2f,
size: core.Vector2f,
uvTopLeft: core.Vector2f,
uvSize: core.Vector2f,
color: core.colors.Color,
vertex: *TextBufferList(TextMeshVertex),
index: *TextBufferList(u32),
) !void {
const o = vertex.count;
const quadColor: u8vec4 = .{
@intFromFloat(std.math.clamp(color.r, 0.0, 1.0) * 255),
@intFromFloat(std.math.clamp(color.g, 0.0, 1.0) * 255),
@intFromFloat(std.math.clamp(color.b, 0.0, 1.0) * 255),
@intFromFloat(std.math.clamp(color.a, 0.0, 1.0) * 255),
};
try vertex.push(.{
.position = @bitCast(topLeft),
.uv = @bitCast(uvTopLeft),
.color = quadColor,
});
try vertex.push(.{
.position = @bitCast(topLeft.add(.{ .x = size.x })),
.uv = @bitCast(uvTopLeft.add(.{ .x = uvSize.x })),
.color = quadColor,
});
try vertex.push(.{
.position = @bitCast(topLeft.add(size)),
.uv = @bitCast(uvTopLeft.add(uvSize)),
.color = quadColor,
});
try vertex.push(.{
.position = @bitCast(topLeft.add(.{ .y = size.y })),
.uv = @bitCast(uvTopLeft.add(.{ .y = uvSize.y })),
.color = quadColor,
});
try index.push(o + 0);
try index.push(o + 1);
try index.push(o + 2);
try index.push(o + 2);
try index.push(o + 3);
try index.push(o + 0);
}
pub const TextRenderer = struct {
// operation of the text renderer.
// given
textInstances: std.ArrayListUnmanaged(*TextMeshBuffer) = .{},
deadList: std.ArrayListUnmanaged(*TextMeshBuffer) = .{},
assignedBuffers: std.AutoHashMapUnmanaged(papyrus.NodeHandle, *TextMeshBuffer) = .{},
linearBuffers: std.AutoHashMapUnmanaged(papyrus.NodeHandle, *TextMeshBuffer) = .{}, // set of mesh buffers for linearlly allocated text
allocator: std.mem.Allocator,
lastUpdateTime: f64 = 0,
geo: *papyrus.TextRenderGeometry,
pub fn create(allocator: std.mem.Allocator) !*@This() {
const self = try allocator.create(@This());
self.* = .{
.allocator = allocator,
.geo = try papyrus.TextRenderGeometry.create(allocator),
};
return self;
}
pub fn uploadMesh(self: *@This(), copyPass: *gpu.GPUCopyPass, node: papyrus.NodeHandle, text: papyrus.DrawCommand.PrimitiveText) !*TextMeshBuffer {
var checkGC: bool = false;
if (core.getEngineTime() - self.lastUpdateTime > 5) {
checkGC = true;
}
// assign meshBuffer
const meshBuffer = try self.getOrAllocMeshBuffer(node, text);
// upload mesh data to mesh buffer
try self.updateMeshBuffer(meshBuffer, copyPass, text);
// set the fragment sampler
return meshBuffer;
}
pub fn submitMeshBuffer(self: *@This(), meshBuffer: *TextMeshBuffer) void {
_ = self;
_ = meshBuffer;
}
pub fn updateMeshBuffer(self: *@This(), meshBuffer: *TextMeshBuffer, copyPass: *gpu.GPUCopyPass, text: papyrus.DrawCommand.PrimitiveText) !void {
var z = tracy.ZoneN(@src(), "papyrus meshBuffer updates");
defer z.End();
const string = text.text.getRead();
if (string.len == 0)
return;
try self.geo.resetAllLines();
var indexList = meshBuffer.mapIndexBufferWriter();
var vertexList = meshBuffer.mapVertexBufferWriter();
//const position = core.Vector2f{.x = text.tl.x, .y = text.tl.y};
const displaySize = text.textSize;
// 1. get the font Atlas from the textPrimitive's font file reference
const atlas = papyrus.PapyrusRuntime.get().getFont(text.fontHandle) orelse {
meshBuffer.unmapAll();
return;
};
meshBuffer.isSDF = atlas.isSDF;
meshBuffer.fontHandle = text.fontHandle;
const ratio = displaySize / atlas.fontSize;
const stride = @as(f32, @floatFromInt(atlas.glyphMetrics['l'].x)) * ratio;
var xOffset: f32 = 0;
var yOffset: f32 = 0;
const fontHeight = @as(f32, @floatFromInt(atlas.glyphMetrics['l'].y)) * ratio;
self.geo.setCharHeight(fontHeight);
self.geo.setPosition(text.tl);
try self.geo.addGeoLine(yOffset + text.tl.y, 0);
var largestXOffset: f32 = 0;
for (string, 0..) |ch, i| {
// if (i * 4 > self.mesh.maxVertexCount - 16) {
//break;
// }
if (!atlas.hasGlyph[ch]) {
try self.geo.addCharGeo(text.tl.x + xOffset, stride, @intCast(i));
xOffset += stride;
continue;
}
if (ch == 0 or ch == '\r') {
continue;
}
if (ch == ' ' or (ch == '\n' and text.renderMode == .NoControl)) {
try self.geo.addCharGeo(text.tl.x + xOffset, stride, @intCast(i));
xOffset += stride;
continue;
}
// newline if we see newline and we're in simple or rich mode.
if (ch == '\n' and (text.renderMode == .Simple or text.renderMode == .Rich)) {
try self.geo.addCharGeo(text.tl.x + xOffset, stride, @intCast(i));
xOffset = 0;
yOffset += fontHeight * 1.2;
try self.geo.addGeoLine(yOffset + text.tl.y, @intCast(i));
continue;
}
if (ch == ' ') {
try self.geo.addCharGeo(text.tl.x + xOffset, stride, @intCast(i));
xOffset += stride;
continue;
}
const box = core.Vector2f.from(atlas.glyphBox1[ch]).fmul(ratio);
const metrics = core.Vector2f.from(atlas.glyphMetrics[ch]).fmul(ratio);
const baseMetrics = core.Vector2f.from(atlas.glyphMetrics[ch]);
const uv_tl = atlas.glyphCoordinates[ch][0];
xOffset += box.x;
//if (xOffset + box.x + metrics.x > self.boxSize.x) {
if (xOffset + box.x + metrics.x > text.size.x) {
xOffset = 0;
yOffset += fontHeight * 1.2;
try self.geo.addGeoLine(yOffset + text.tl.y, @intCast(i));
}
const color = text.color;
const topLeft = core.Vector2f{
// .x = self.position.x + xOffset + box.x,
// .y = yOffset + self.position.y + box.y + fontHeight,
.x = xOffset,
.y = yOffset + box.y + fontHeight,
};
// core.engine_log("topleft = {d}x{d}", .{ topLeft.x, topLeft.y });
const metric_size = core.Vector2f{ .x = metrics.x, .y = metrics.y };
try pushQuadToWriters(
topLeft, // topLeft: core.Vector2f,
metric_size, // size: core.Vector2f,
.{ .x = uv_tl.x, .y = uv_tl.y }, // uvTopLeft: core.Vector2f,
.{
.x = baseMetrics.x / @as(f32, @floatFromInt(atlas.atlasSize.x)),
.y = baseMetrics.y / @as(f32, @floatFromInt(atlas.atlasSize.y)),
}, // uv size
.{ .r = color.r, .g = color.g, .b = color.b, .a = color.a }, // color
// uvSize: core.Vector2f,
// color: core.colors.Color,
// vertex: *TextBufferList(TextMeshVertex),
// index: *TextBufferList(u32),
&vertexList,
&indexList,
);
// self.mesh.addQuad2D(
// topLeft,
// metric_size,
// .{ .x = uv_tl.x, .y = uv_tl.y }, // uv topleft
// .{
// .x = baseMetrics.x / @as(f32, @floatFromInt(atlas.atlasSize.x)),
// .y = baseMetrics.y / @as(f32, @floatFromInt(atlas.atlasSize.y)),
// }, // uv size
// .{ .r = color.r, .g = color.g, .b = color.b }, // color
// );
// todo insert geo
//try self.renderedGeo.addCharGeo(self.position.x + xOffset, box.x + metrics.x, @intCast(i));
//xOffset += box.x + metrics.x;
try self.geo.addCharGeo(text.tl.x + xOffset, metrics.x, @intCast(i));
xOffset += metrics.x;
if (xOffset > largestXOffset) {
largestXOffset = xOffset;
}
}
try self.geo.addCharGeo(text.tl.x + xOffset, 200.0, @intCast(string.len));
const renderedSize = .{
.x = largestXOffset,
.y = yOffset + fontHeight * 1.2,
};
self.geo.setBoundsX(text.tl.x, text.tl.x + renderedSize.x);
// pushQuadToWriters(
// // topLeft: core.Vector2f,
// // size: core.Vector2f,
// // uvTopLeft: core.Vector2f,
// // uvSize: core.Vector2f,
// // color: core.colors.Color,
// vertexList, // vertex: *TextBufferList(TextMeshVertex),
// indexList,
// ); //index: *TextBufferList(u32),);
meshBuffer.unmapAll();
meshBuffer.submitBuffers(copyPass);
meshBuffer.indexCount = indexList.count;
}
pub fn getOrAllocMeshBuffer(self: *@This(), node: papyrus.NodeHandle, text: papyrus.DrawCommand.PrimitiveText) !*TextMeshBuffer {
const textLen = text.text.getRead().len;
const assignedBuffers: *std.AutoHashMapUnmanaged(papyrus.NodeHandle, *TextMeshBuffer) = if (text.flags.debugText) &self.linearBuffers else &self.assignedBuffers;
// 1. get the assigned buffer;
if (assignedBuffers.get(node)) |buffer| {
if (textLen <= buffer.capacity) {
return buffer;
}
core.engine_log("recycling text buffer {any}", .{node});
// if the buffer is too small for the text, dont use that one and add it to the dead list.
try self.deadList.append(self.allocator, buffer);
_ = assignedBuffers.remove(node);
}
// 2. if no text renderer is available, search through the deadList until you find a TextMeshBuffer
// which has a capacity large enough for the text we're trying to render.
{
var foundBuffer: ?*TextMeshBuffer = null;
var foundIndex: usize = 0;
for (self.deadList.items, 0..) |buffer, i| {
if (textLen <= buffer.capacity) {
try assignedBuffers.put(self.allocator, node, buffer);
foundIndex = i;
foundBuffer = buffer;
}
}
if (foundBuffer) |buffer| {
_ = self.deadList.swapRemove(foundIndex);
return buffer;
}
}
// 3. if we don't have any allocations already available in the deadList create a new one large enough for this text
const textLengthBins: []const u32 = &.{ 32, 64, 256, 512, 1024, 4096, 8192, 8192 * 4 };
var newBufferLength: u32 = @intFromFloat(@as(f32, @floatFromInt(textLen)) * 1.5);
for (textLengthBins) |bin| {
if (textLen <= bin) {
newBufferLength = bin;
break;
}
}
core.engine_log("creating text buffer {any} with length {d} for textLen = {d}", .{ node, newBufferLength, textLen });
const newBuffer = try TextMeshBuffer.create(self.allocator, newBufferLength);
try assignedBuffers.put(self.allocator, node, newBuffer);
try self.textInstances.append(self.allocator, newBuffer);
return newBuffer;
}
// once every 5 seconds mark the frame as a GC frame
// during a GC frame any TextMeshBuffers which aren't being used get kicked to the deadList
// any TextMeshBuffers which stay in the deadList for 10 or more GC frames get released and removed from the deadlist
pub fn bindFontBuffersAndSamplers(
self: *@This(),
pass: *gpu.GPURenderPass,
meshBuffer: *TextMeshBuffer,
) void {
pass.bindGPUVertexBuffers(0, &.{ .buffer = meshBuffer.vertexBuffer, .offset = 0 }, 1);
pass.bindGPUIndexBuffer(&.{ .buffer = meshBuffer.indexBuffer, .offset = 0 }, .indexelementsize32bit);
_ = self;
}
pub fn destroy(self: *@This()) void {
for (self.textInstances.items) |item| {
item.destroy(self.allocator);
}
for (self.deadList.items) |item| {
item.destroy(self.allocator);
}
self.geo.destroy();
self.linearBuffers.deinit(self.allocator);
self.assignedBuffers.deinit(self.allocator);
self.textInstances.deinit(self.allocator);
self.allocator.destroy(self);
}
};
pub const TextMeshBuffer = struct {
indexBuffer: *gpu.GPUBuffer = undefined,
vertexBuffer: *gpu.GPUBuffer = undefined,
indexTransferBuffer: *gpu.GPUTransferBuffer = undefined,
indexTransferBufferSize: usize,
vertexTransferBuffer: *gpu.GPUTransferBuffer = undefined,
vertexTransferBufferSize: usize,
indexCount: u32 = 0,
isSDF: bool = true,
fontHandle: u32 = 0,
capacity: u32 = 0,
pub fn create(allocator: std.mem.Allocator, maxChars: u32) !*@This() {
var self: *@This() = try allocator.create(@This());
self.capacity = maxChars;
const vertexBufferCount: u32 = maxChars * 4;
const indexBufferCount: u32 = maxChars * 6;
self.vertexTransferBufferSize = vertexBufferCount * @sizeOf(TextMeshVertex);
self.indexTransferBufferSize = indexBufferCount * @sizeOf(u32);
const ctx = rend.context();
self.vertexTransferBuffer = rend.renderer.createGPUTransferBuffer(&.{
.usage = .transferbufferusageUpload,
.size = vertexBufferCount * @sizeOf(TextMeshVertex),
.props = 0,
});
self.indexTransferBuffer = rend.renderer.createGPUTransferBuffer(&.{
.usage = .transferbufferusageUpload,
.size = indexBufferCount * @sizeOf(u32),
.props = 0,
});
self.vertexBuffer = ctx.device.createGPUBuffer(&.{
.usage = .{ .bufferusageVertex = true },
.size = vertexBufferCount * @sizeOf(TextMeshVertex),
.props = 0,
});
self.indexBuffer = ctx.device.createGPUBuffer(&.{
.usage = .{ .bufferusageIndex = true },
.size = indexBufferCount * @sizeOf(u32),
.props = 0,
});
return self;
}
pub fn destroy(self: *@This(), allocator: std.mem.Allocator) void {
const ctx = rend.context();
ctx.device.releaseGPUBuffer(self.indexBuffer);
ctx.device.releaseGPUBuffer(self.vertexBuffer);
rend.renderer.releaseGPUTransferBuffer(self.indexTransferBuffer, self.indexTransferBufferSize);
rend.renderer.releaseGPUTransferBuffer(self.vertexTransferBuffer, self.vertexTransferBufferSize);
allocator.destroy(self);
}
// buffer management stuff below
pub fn mapIndexBuffer(self: *@This()) []u32 {
const ctx = rend.context();
var slice: []u32 = undefined;
slice.ptr = @ptrCast(@alignCast(ctx.device.mapGPUTransferBuffer(self.indexTransferBuffer, true)));
slice.len = self.capacity * 6;
return slice;
}
pub fn unmapIndexBuffer(self: *@This()) void {
const ctx = rend.context();
ctx.device.unmapGPUTransferBuffer(self.indexTransferBuffer);
}
pub fn mapVertexBuffer(self: *@This()) []TextMeshVertex {
const ctx = rend.context();
var slice: []TextMeshVertex = undefined;
slice.ptr = @ptrCast(@alignCast(ctx.device.mapGPUTransferBuffer(self.vertexTransferBuffer, true)));
slice.len = self.capacity * 4;
return slice;
}
pub fn unmapVertexBuffer(self: *@This()) void {
const ctx = rend.context();
ctx.device.unmapGPUTransferBuffer(self.vertexTransferBuffer);
}
pub fn mapVertexBufferWriter(self: *@This()) TextBufferList(TextMeshVertex) {
const slice = self.mapVertexBuffer();
return TextBufferList(TextMeshVertex).init(slice);
}
pub fn mapIndexBufferWriter(self: *@This()) TextBufferList(u32) {
const slice = self.mapIndexBuffer();
return TextBufferList(u32).init(slice);
}
pub fn unmapAll(self: *@This()) void {
self.unmapIndexBuffer();
self.unmapVertexBuffer();
}
pub fn submitBuffers(self: *@This(), copyPass: *gpu.GPUCopyPass) void {
copyPass.uploadToGPUBuffer(&.{ .transfer_buffer = self.indexTransferBuffer, .offset = 0 }, &.{
.buffer = self.indexBuffer,
.offset = 0,
.size = self.capacity * 6 * @sizeOf(u32),
}, false);
copyPass.uploadToGPUBuffer(&.{ .transfer_buffer = self.vertexTransferBuffer, .offset = 0 }, &.{
.buffer = self.vertexBuffer,
.offset = 0,
.size = self.capacity * 4 * @sizeOf(TextMeshVertex),
}, false);
}
};
pub const text_frag = @import("text.frag");
pub const text_vert = @import("text.vert");
pub const rend = @import("rend");
pub const core = @import("core");
pub const tracy = core.tracy;
pub const std = @import("std");
const sdl = @import("sdl3");
const gpu = sdl.gpu;
const papyrus = @import("papyrus");