gc: *graphics.NeonVkContext, allocator: std.mem.Allocator, pipeData: gpd.GpuPipeData = undefined, materialName: core.Name = core.Name.MakeComptime("mat_papyrus"), materialNameText: core.Name = core.Name.MakeComptime("mat_papyrus_text"), material: *graphics.Material = undefined, // main material used for anything that isn't text defaultTextureSet: vk.DescriptorSet, textMaterial: *graphics.Material = undefined, // main material used for text mappedBuffers: []gpd.GpuMappingData(ImageGpu) = undefined, textImageBuffers: []gpd.GpuMappingData(FontInfo) = undefined, indexBuffer: graphics.IndexBuffer = undefined, drawList: papyrus.DrawList, stringArena: std.heap.ArenaAllocator, fontTexture: *graphics.Texture = undefined, papyrusCtx: *papyrus.Context, quad: *graphics.Mesh, ssboCount: u32 = 0, textSsboCount: u32 = 0, time: f64 = 0, textPipeData: gpd.GpuPipeData = undefined, displayDemo: bool = true, drawCommands: std.ArrayList(VkCommand), textRenderer: *TextRenderer, averageFrameTime: f64 = 0, onDebugInfoBinding: usize = 0, sharedData: [graphics.NumFrames]SharedData = undefined, const std = @import("std"); const core = @import("core"); const memory = core.MemoryTracker; const assets = @import("assets"); const graphics = @import("graphics"); const gpd = graphics.gpu_pipe_data; const RenderThread = graphics.RenderThread; const platform = @import("platform"); const papyrus = @import("papyrus"); const papyrus_vk_vert = @import("papyrus_vk_vert"); const papyrus_vk_frag = @import("papyrus_vk_frag"); const FontSDF_vert = @import("FontSDF_vert"); const FontSDF_frag = @import("FontSDF_frag"); const gl = @import("glslTypes"); const vk = @import("vulkan"); const tracy = core.tracy; const Text = papyrus.Text; const VkCommand = @import("VkPapyrusRenderCommand.zig").VkCommand; const text_render = @import("text_render.zig"); const TextRenderer = text_render.TextRenderer; const DisplayText = text_render.DisplayText; const FontAtlasVk = text_render.FontAtlasVk; const Key = papyrus.Event.Key; pub const RawInputListenerVTable = platform.windowing.RawInputListenerInterface.from(@This()); pub var NeonObjectTable: core.EngineObjectVTable = core.EngineObjectVTable.from(@This()); pub const RendererInterfaceVTable = graphics.RendererInterface.from(@This()); pub const PushConstant = FontSDF_vert.constants; pub const ImageGpu = papyrus_vk_vert.ImageRenderData; pub const FontInfo = FontSDF_vert.FontInfo; pub fn init(allocator: std.mem.Allocator) !*@This() { const papyrusCtx = try papyrus.initialize(allocator); const self = try allocator.create(@This()); self.* = .{ .allocator = allocator, .gc = graphics.getContext(), .papyrusCtx = papyrusCtx, .quad = try allocator.create(graphics.Mesh), .drawCommands = std.ArrayList(VkCommand).init(allocator), .textRenderer = try TextRenderer.init(allocator, graphics.getContext(), papyrusCtx), .drawList = papyrus.DrawList.init(allocator), .stringArena = std.heap.ArenaAllocator.init(allocator), .defaultTextureSet = undefined, }; self.initShared(); self.onDebugInfoBinding = try core.addEngineDelegateBinding("onFrameDebugInfoEmitted", onFrameDebugInfo, self); // core.engine_logs("PapyrusSystem init"); memory.MTPrintStatsDelta(); try platform.getInstance().installListener(self); return self; } pub fn onFrameDebugInfo(ctx: *anyopaque, averageFrameTime: f64) core.EngineDataEventError!void { const self: *@This() = @ptrCast(@alignCast(ctx)); self.averageFrameTime = averageFrameTime; } pub fn OnIoEvent(self: *@This(), event: platform.IOEvent) platform.InputListenerError!void { try OnIoEvent_GLFW(self, event); } pub fn OnIoEvent_GLFW(self: *@This(), event: platform.IOEvent) platform.InputListenerError!void { switch (event) { .mousePosition => |mousePosition| { _ = mousePosition; }, .mouseButton => |mouseButton| { var keycode: Key = .Unknown; var eventType: papyrus.Event.PressedType = .onPressed; switch (mouseButton.button) { 0 => { // left click keycode = Key.Mouse1; }, 1 => { // right click keycode = Key.Mouse2; }, 2 => { // middle click keycode = Key.Mouse3; }, 3 => { // button 3 keycode = Key.Mouse4; }, 4 => { // button 4 keycode = Key.Mouse5; }, else => {}, } switch (mouseButton.action) { 0 => { eventType = .onReleased; }, 1 => { eventType = .onPressed; }, else => {}, } // todo: use the right error code here self.papyrusCtx.onKey(keycode, eventType) catch unreachable; }, .windowResize => |e| { const pi = platform.getInstance(); self.papyrusCtx.get(.{}).setSize(.{ .x = e.newSize.x / pi.contentScale.x, .y = e.newSize.y / pi.contentScale.y, }); }, .codepoint => |codepoint| { try self.papyrusCtx.textEntry.sendCodePoint(@as(u32, codepoint)); }, .key => |keyEvent| { var te = self.papyrusCtx.textEntry; const actions = platform.glfw_defs.actions; const keys = platform.glfw_defs.keys; if (@as(actions, @enumFromInt(keyEvent.action)) == actions.Press or @as(actions, @enumFromInt(keyEvent.action)) == actions.Repeat) { // reference glfw3.h switch (@as(keys, @enumFromInt(keyEvent.key))) { keys.Escape => { try te.sendEscape(); }, keys.Enter => { try te.sendEnter(); }, keys.Tab => { try te.sendTab(); }, keys.Backspace => { try te.sendBackspace(); }, keys.Delete => { try te.sendDelete(); }, keys.Right => { try te.sendRight(); }, keys.Left => { try te.sendLeft(); }, keys.Up => { try te.sendUp(); }, keys.Down => { try te.sendDown(); }, keys.Pageup => { try te.sendPageup(); }, keys.Pagedown => { try te.sendPagedown(); }, keys.Home => { try te.sendHome(); }, keys.End => { try te.sendEnd(); }, else => {}, } } }, else => {}, } } const t_white_name = core.StaticName("t_white"); pub fn setup(self: *@This(), gc: *graphics.NeonVkContext) !void { core.ui_log("Papyrus Subsystem setup {x}", .{@intFromPtr(self)}); self.gc = gc; try self.preparePipeline(); try self.setupMeshes(); try self.gc.registerRendererPlugin(self); self.defaultTextureSet = self.gc.textureSets.get(t_white_name.handle()).?; self.mappedBuffers = try self.pipeData.mapBuffers(self.gc, ImageGpu, 0); self.textImageBuffers = try self.textPipeData.mapBuffers(self.gc, FontInfo, 0); // core.ui_log("Mapping buffers.", .{}); const extent = self.gc.actual_extent; const pi = platform.getInstance(); self.papyrusCtx.get(.{}).setSize(.{ .x = @as(f32, @floatFromInt(extent.width)) / pi.contentScale.x, .y = @as(f32, @floatFromInt(extent.height)) / pi.contentScale.y, }); } pub fn preDraw(self: *@This(), frameId: usize) void { _ = self; _ = frameId; } // todo: this thing should be optional pub fn onBindObject(self: *@This(), objectHandle: core.ObjectHandle, objectIndex: usize, cmd: vk.CommandBuffer, frameIndex: usize) void { _ = self; _ = objectHandle; _ = objectIndex; _ = cmd; _ = frameIndex; } // Uploads a new primitive mesh and an index buffer to the gpu. fn setupMeshes(self: *@This()) !void { self.quad.* = graphics.Mesh.init(self.gc, self.allocator); try self.quad.vertices.resize(4); var indexBuffer: [6]u32 = undefined; self.quad.vertices.items[0].position = .{ .x = 1, .y = 1, .z = 0 }; // bot right self.quad.vertices.items[1].position = .{ .x = 1, .y = -1, .z = 0 }; // top right self.quad.vertices.items[2].position = .{ .x = -1, .y = -1, .z = 0 }; // top left self.quad.vertices.items[3].position = .{ .x = -1, .y = 1, .z = 0 }; // bot left self.quad.vertices.items[0].uv = .{ .x = 1.0, .y = 1.0 }; self.quad.vertices.items[1].uv = .{ .x = 1.0, .y = 0.0 }; self.quad.vertices.items[2].uv = .{ .x = 0.0, .y = 0.0 }; self.quad.vertices.items[3].uv = .{ .x = 0.0, .y = 1.0 }; indexBuffer[0] = 0; indexBuffer[1] = 1; indexBuffer[2] = 2; indexBuffer[3] = 2; indexBuffer[4] = 3; indexBuffer[5] = 0; self.indexBuffer = try graphics.IndexBuffer.uploadIndexBuffer(self.gc, &indexBuffer, self.allocator); try self.quad.upload(self.gc); } var lastEventCount: usize = 0; var displayEventsPerSecond: f64 = 0; pub fn tick(self: *@This(), deltaTime: f64) void { self.time += deltaTime; const cursor = platform.getInstance().inputState.mousePos; // TODO, use OnIoEvent, but ehh this is fine. self.papyrusCtx.setCursorLocation(.{ .x = @floatCast(cursor.x), .y = @floatCast(cursor.y), }); self.papyrusCtx.tick(deltaTime) catch unreachable; if (memory.MTGet()) |mt| { const eventsPerFrame = @as(f64, @floatFromInt(mt.eventsCount - lastEventCount)); displayEventsPerSecond = (displayEventsPerSecond + eventsPerFrame / 60.0) - displayEventsPerSecond / 60.0; self.papyrusCtx.pushDebugText("memory used: {d:.3}MB {d} allocations ({d} events per frame)", .{ @as(f32, @floatFromInt(mt.totalAllocSize)) / 1e6, mt.allocationsCount, eventsPerFrame, }) catch {}; lastEventCount = mt.eventsCount; } if (self.gc.vulkanValidation) { self.papyrusCtx.pushDebugText("vulkan validation: ON", .{}) catch unreachable; } self.papyrusCtx.pushDebugText("ecs entities: {d}", .{core.getRegistry().baseSet.count()}) catch unreachable; self.papyrusCtx.pushDebugText("frameTime (ms): {d:.4} fps: {d:.3} engine uptime: {d:.3}", .{ self.averageFrameTime * 1000.0, 1.0 / self.averageFrameTime, core.getEngineUptime() }) catch unreachable; self.papyrusCtx.pushDebugText(" systems (ms): {d:.4}", .{ core.getEngine().systemsThreadTime * 1000.0, }) catch unreachable; self.papyrusCtx.pushDebugText(" renderthread (ms): {d:.4}", .{core.getEngine().renderThreadTime * 1000.0}) catch unreachable; // for(core.getEngine().) } pub fn buildTextPipeline(self: *@This()) !void { var gpdBuilder = gpd.GpuPipeDataBuilder.init(self.allocator, self.gc); gpdBuilder.objectCount = 64; try gpdBuilder.addBufferBinding( FontInfo, .storage_buffer, .{ .vertex_bit = true, .fragment_bit = true }, .storageBuffer, ); self.textPipeData = try gpdBuilder.build("Papyrus-Text"); defer gpdBuilder.deinit(); // const vert_spv = try graphics.loadSpv(self.allocator, "FontSDF_vert.spv"); // defer self.allocator.free(vert_spv); // const frag_spv = try graphics.loadSpv(self.allocator, "FontSDF_frag.spv"); // defer self.allocator.free(frag_spv); const vert_spv = FontSDF_vert.spv(); const frag_spv = FontSDF_frag.spv(); var builder = try graphics.NeonVkPipelineBuilder.init( self.gc.dev, self.gc.vkd, self.gc.allocator, self.gc.vkAllocator, vert_spv, frag_spv, ); defer builder.deinit(); try builder.add_mesh_description(); try builder.add_layout(self.textPipeData.descriptorSetLayout); try builder.add_layout(self.gc.singleTextureSetLayout); try builder.add_depth_stencil(); try builder.add_push_constant_custom(PushConstant); try builder.init_triangle_pipeline(self.gc.actual_extent); self.textMaterial = try self.allocator.create(graphics.Material); self.textMaterial.* = graphics.Material{ .materialName = self.materialNameText, .pipeline = (try builder.build(self.gc.renderPass)).?, .layout = builder.pipelineLayout, }; try self.gc.add_material(self.textMaterial); } pub fn buildImagePipeline(self: *@This()) !void { var spriteDataBuilder = gpd.GpuPipeDataBuilder.init(self.allocator, self.gc); try spriteDataBuilder.addBufferBinding( ImageGpu, .storage_buffer, .{ .vertex_bit = true, .fragment_bit = true }, .storageBuffer, ); self.pipeData = try spriteDataBuilder.build("Papyrus"); defer spriteDataBuilder.deinit(); const vert_spv = papyrus_vk_vert.spv(); const frag_spv = papyrus_vk_frag.spv(); var builder = try graphics.NeonVkPipelineBuilder.init( self.gc.dev, self.gc.vkd, self.gc.allocator, self.gc.vkAllocator, vert_spv, frag_spv, ); defer builder.deinit(); try builder.add_mesh_description(); try builder.add_layout(self.pipeData.descriptorSetLayout); try builder.add_layout(self.gc.singleTextureSetLayout); try builder.add_depth_stencil(); try builder.add_push_constant_custom(PushConstant); try builder.init_triangle_pipeline(self.gc.actual_extent); const material = try self.gc.allocator.create(graphics.Material); material.* = graphics.Material{ .materialName = self.materialName, .pipeline = (try builder.build(self.gc.renderPass)).?, .layout = builder.pipelineLayout, }; try self.gc.add_material(material); self.material = material; } pub fn preparePipeline(self: *@This()) !void { try self.buildTextPipeline(); try self.buildImagePipeline(); } pub fn uploadSSBOData(self: *@This(), frameId: usize, drawList: *const papyrus.DrawList) !void { var z = tracy.ZoneN(@src(), "Uploading SSBOs"); defer z.End(); var imagesGpu = self.mappedBuffers[frameId].objects; var imagesText = self.textImageBuffers[frameId].objects; self.textSsboCount = 0; self.ssboCount = 0; var textFrameContext = self.textRenderer.startRendering(); for (drawList.items) |drawCmd| { switch (drawCmd.primitive) { .Rect => |rect| { imagesGpu[self.ssboCount] = ImageGpu{ .imagePosition = .{ .x = rect.tl.x, .y = rect.tl.y }, .imageSize = .{ .x = rect.size.x, .y = rect.size.y }, .anchorPoint = .{ .x = -1.0, .y = -1.0 }, .scale = .{ .x = 1.0, .y = 1.0 }, .alpha = 1.0, .pad0 = std.mem.zeroes([4]u8), .baseColor = .{ .x = rect.backgroundColor.r, .y = rect.backgroundColor.g, .z = rect.backgroundColor.b, .w = rect.backgroundColor.a, }, .rounding = .{ .x = rect.rounding.tl, .y = rect.rounding.tr, .z = rect.rounding.bl, .w = rect.rounding.br, }, .borderColor = .{ .x = rect.borderColor.r, .y = rect.borderColor.g, .z = rect.borderColor.b, .w = rect.borderColor.a, }, .borderWidth = rect.borderWidth, .flags = 0, }; var imageSet: ?vk.DescriptorSet = null; if (rect.imageRef) |_imageRef| { var ref = _imageRef; if (self.gc.textureSets.get(ref.handle())) |maybeImageSet| { imageSet = maybeImageSet; imagesGpu[self.ssboCount].flags = 1; } } try self.drawCommands.append(.{ .image = .{ .index = self.ssboCount, .imageSet = imageSet }, }); // core.ui_log("drawCmd: {any} {any} {any}", .{ // drawCmd.node, // imagesGpu[self.ssboCount].imagePosition, // imagesGpu[self.ssboCount].imageSize, // }); self.ssboCount += 1; }, .Text => |text| { const nextDisplay = self.textRenderer.getNextSlot(text.text.utf8.len, &textFrameContext); var textDisplay: *DisplayText = undefined; if (nextDisplay.small) { textDisplay = self.textRenderer.smallDisplays.items[nextDisplay.index]; } else { textDisplay = self.textRenderer.displays.items[nextDisplay.index]; } textDisplay.displaySize = text.textSize; textDisplay.renderMode = text.renderMode; textDisplay.boxSize = .{ .x = text.size.x, .y = text.size.y }; textDisplay.color = text.color; textDisplay.position = .{ .x = text.tl.x, .y = text.tl.y }; // TODO this is really bad and confusing. // The renderer should not be storing a hash on the papyrus resource. if (text.rendererHash != 0) { textDisplay.atlas = self.textRenderer.fonts.get(text.rendererHash).?; } textDisplay.setString(&text.text.utf8); try textDisplay.updateMesh(text.flags.setSourceGeometry); self.papyrusCtx.get(drawCmd.node).textRenderedSize = textDisplay.renderedSize; if (text.flags.setSourceGeometry) { self.papyrusCtx.textEntry.trg = textDisplay.renderedGeo; } // core.ui_log("nextDisplay = {any}, font = {d} sdf={any} ssbo={d}", .{ // nextDisplay, // text.rendererHash, // textDisplay.atlas.atlas.isSDF, // self.textSsboCount, // }); imagesText[self.textSsboCount] = .{ .isSdf = if (textDisplay.atlas.atlas.isSDF) 1 else 0, .position = .{ .x = textDisplay.position.x, .y = textDisplay.position.y }, .size = .{ .x = textDisplay.boxSize.x, .y = textDisplay.boxSize.y }, .pad0 = 0, .pad2 = undefined, }; try self.drawCommands.append(.{ .text = .{ .index = nextDisplay.index, .small = nextDisplay.small, .ssbo = self.textSsboCount, } }); self.textSsboCount += 1; }, } } } pub fn rtPostDraw(self: *@This(), rt: *RenderThread, cmd: vk.CommandBuffer, frameIndex: u32) void { const shared = self.getShared(frameIndex); self.drawCommands.clearRetainingCapacity(); const rtShared = rt.getShared(frameIndex); shared.lock.lock(); self.uploadSSBOData(frameIndex, &shared.drawList) catch unreachable; shared.lock.unlock(); var vertexBufferOffset: u64 = 0; var pushConstant = PushConstant{ .extent = .{ .x = rtShared.extent.x, .y = rtShared.extent.y, }, }; for (self.drawCommands.items) |command| { switch (command) { .text => |t| { self.gc.vkd.cmdPushConstants(cmd, self.textMaterial.layout, .{ .vertex_bit = true, .fragment_bit = true }, 0, @sizeOf(PushConstant), &pushConstant); if (t.small) { var drawText = self.textRenderer.smallDisplays.items[t.index]; drawText.draw(frameIndex, cmd, self.textMaterial, t.ssbo, self.textPipeData); } else { var drawText = self.textRenderer.displays.items[t.index]; drawText.draw(frameIndex, cmd, self.textMaterial, t.ssbo, self.textPipeData); } }, .image => |img| { self.gc.vkd.cmdPushConstants(cmd, self.material.layout, .{ .vertex_bit = true, .fragment_bit = true }, 0, @sizeOf(PushConstant), &pushConstant); const index = img.index; self.gc.vkd.cmdBindPipeline(cmd, .graphics, self.material.pipeline); self.gc.vkd.cmdBindVertexBuffers(cmd, 0, 1, @ptrCast(&self.quad.buffer.buffer), @ptrCast(&vertexBufferOffset)); self.gc.vkd.cmdBindIndexBuffer(cmd, self.indexBuffer.buffer.buffer, 0, .uint32); self.gc.vkd.cmdBindDescriptorSets(cmd, .graphics, self.material.layout, 0, 1, self.pipeData.getDescriptorSet(frameIndex), 0, undefined); if (img.imageSet) |imageSet| { self.gc.vkd.cmdBindDescriptorSets(cmd, .graphics, self.material.layout, 1, 1, @ptrCast(&imageSet), 0, undefined); } else { self.gc.vkd.cmdBindDescriptorSets(cmd, .graphics, self.material.layout, 1, 1, @ptrCast(&self.defaultTextureSet), 0, undefined); } self.gc.vkd.cmdDrawIndexed(cmd, @as(u32, @intCast(self.indexBuffer.indices.len)), 1, 0, 0, index); }, } } } pub fn getShared(self: *@This(), fi: u32) *SharedData { return &self.sharedData[fi]; } pub fn sendShared(self: *@This(), frameIndex: u32) void { const z1 = tracy.ZoneN(@src(), "PapryusIntegration - UploadingShared"); defer z1.End(); const shared = self.getShared(frameIndex); shared.lock.lock(); defer shared.lock.unlock(); self.papyrusCtx.makeDrawList(&shared.drawList, &shared.stringArena) catch unreachable; } pub fn postDraw(self: *@This(), cmd: vk.CommandBuffer, frameIndex: usize, frameTime: f64) void { _ = frameTime; var z = tracy.ZoneN(@src(), "Papyrus post draw"); defer z.End(); var vertexBufferOffset: u64 = 0; if (!self.displayDemo) { return; } var z1 = tracy.ZoneN(@src(), "Papyrus Making Draw List"); self.papyrusCtx.makeDrawList(&self.drawList, &self.stringArena) catch unreachable; z1.End(); self.drawCommands.clearRetainingCapacity(); self.uploadSSBOData(frameIndex, &self.drawList) catch unreachable; var pushConstant = PushConstant{ .extent = .{ .x = @as(f32, @floatFromInt(self.gc.extent.width)), .y = @as(f32, @floatFromInt(self.gc.extent.height)), }, }; for (self.drawCommands.items) |command| { switch (command) { .text => |t| { self.gc.vkd.cmdPushConstants(cmd, self.textMaterial.layout, .{ .vertex_bit = true, .fragment_bit = true }, 0, @sizeOf(PushConstant), &pushConstant); if (t.small) { var drawText = self.textRenderer.smallDisplays.items[t.index]; drawText.draw(frameIndex, cmd, self.textMaterial, t.ssbo, self.textPipeData); } else { var drawText = self.textRenderer.displays.items[t.index]; drawText.draw(frameIndex, cmd, self.textMaterial, t.ssbo, self.textPipeData); } }, .image => |img| { self.gc.vkd.cmdPushConstants(cmd, self.material.layout, .{ .vertex_bit = true, .fragment_bit = true }, 0, @sizeOf(PushConstant), &pushConstant); const index = img.index; self.gc.vkd.cmdBindPipeline(cmd, .graphics, self.material.pipeline); self.gc.vkd.cmdBindVertexBuffers(cmd, 0, 1, @ptrCast(&self.quad.buffer.buffer), @ptrCast(&vertexBufferOffset)); self.gc.vkd.cmdBindIndexBuffer(cmd, self.indexBuffer.buffer.buffer, 0, .uint32); self.gc.vkd.cmdBindDescriptorSets(cmd, .graphics, self.material.layout, 0, 1, self.pipeData.getDescriptorSet(frameIndex), 0, undefined); if (img.imageSet) |imageSet| { self.gc.vkd.cmdBindDescriptorSets(cmd, .graphics, self.material.layout, 1, 1, @ptrCast(&imageSet), 0, undefined); } else { self.gc.vkd.cmdBindDescriptorSets(cmd, .graphics, self.material.layout, 1, 1, @ptrCast(&self.defaultTextureSet), 0, undefined); } self.gc.vkd.cmdDrawIndexed(cmd, @as(u32, @intCast(self.indexBuffer.indices.len)), 1, 0, 0, index); }, } } } pub fn shutdown(self: *@This()) void { self.gc.vkd.deviceWaitIdle(self.gc.dev) catch unreachable; core.ui_logs("Shutting down UI"); for (self.mappedBuffers) |*mapped| { mapped.unmap(self.gc); } for (self.textImageBuffers) |*mapped| { mapped.unmap(self.gc); } self.drawCommands.deinit(); self.stringArena.deinit(); self.quad.deinit(self.gc); self.allocator.destroy(self.quad); self.gc.allocator.free(self.mappedBuffers); self.gc.allocator.free(self.textImageBuffers); self.textRenderer.deinit(self.allocator); self.pipeData.deinit(self.allocator, self.gc); self.textPipeData.deinit(self.allocator, self.gc); self.indexBuffer.deinit(self.gc); self.drawList.deinit(); self.papyrusCtx.deinit(); self.deinitShared(); core.ui_logs("finished shutting down ui"); } pub fn deinit(self: *@This()) void { self.shutdown(); self.allocator.destroy(self); } pub fn processEvents(self: *@This(), frameNumber: u64) core.EngineDataEventError!void { _ = frameNumber; _ = self; } const SharedData = struct { lock: std.Thread.Mutex, drawList: papyrus.DrawList, stringArena: std.heap.ArenaAllocator, }; pub fn initShared(self: *@This()) void { for (&self.sharedData) |*s| { s.* = .{ .lock = .{}, .drawList = papyrus.DrawList.init(self.allocator), .stringArena = std.heap.ArenaAllocator.init(self.allocator), }; } } pub fn deinitShared(self: *@This()) void { for (&self.sharedData) |*s| { s.drawList.deinit(); s.stringArena.deinit(); } }