524 lines
18 KiB
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");
|