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();
    }
}
