// 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");