// API dispatch functions
const vkd = vk_api.vkd;
const vki = vk_api.vki;
const vkb = vk_api.vkb;

exitSignal: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
exitConfirmed: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
// allocators
allocator: std.mem.Allocator,
vkAllocator: *NeonVkAllocator,

// device information and api handles
dev: vk.Device,
pdev: vk.PhysicalDevice,
pdevProperties: vk.PhysicalDeviceProperties,
caps: vk.SurfaceCapabilitiesKHR,
cmdPool: vk.CommandPool,
minUniformBufferAlignment: usize,

// current extent of the window (raw values from platform)
extent: vk.Extent2D,

dynamicMeshManager: *mesh.DynamicMeshManager,

graphicsQueue: NeonVkQueue,
presentQueue: NeonVkQueue,
frameData: [NumFrames]NeonVkFrameData,

displayTarget: DisplayTarget,

// frame data to upload to gpu
framesInFlight: std.atomic.Value(u32),

sharedData: [NumFrames]SharedData = undefined,

actual_extent: vk.Extent2D = undefined, // actual_extent is the base extent used for active drawing, while extent is used
// for a specific frame
commandBuffers: [NumFrames]vk.CommandBuffer = undefined,
frameSync: [NumFrames]FrameSyncs = undefined,

indirectStaging: graphics.NeonVkBuffer = undefined,
indirectGpu: graphics.NeonVkBuffer = undefined,

isMinimized: std.atomic.Value(bool),

// acquireNextFrame will prime an already allocated semaphore at the same time it
// returns an index.
// with double buffering we can't garutee which index will be acquired next.
// and we can't risk acquiring a semaphore that is already in flight.
// therefore this extraSemaphore shall act as an empty spot in the semaphore queue
emptyAcquireSemaphore: vk.Semaphore = undefined,

// main renderpass information
renderPass: vk.RenderPass,
sceneParameterBuffer: NeonVkBuffer,
maxObjectCount: u32,

plugins: *const std.ArrayListUnmanaged(RendererInterfaceRef),

meshPool: *MeshPoolBuffers = undefined,

listeners: std.ArrayListUnmanaged(ProcessEventListener) = .{},

const ProcessEventListener = struct {
    ptr: *anyopaque,
    func: *const fn (*anyopaque) void,
};

pub const ObjectSharedData = struct {
    //textureSet: vk.DescriptorSet,
    // textureId: u32,
    indexedMesh: mesh_pool.IndexedMesh,
};

pub const SharedData = struct {
    lock: std.Thread.Mutex,
    cameraData: vk_renderer_camera_gpu.NeonVkCameraDataGpu,
    sceneData: NeonVkSceneDataGpu,
    pipeline: vk.Pipeline,
    pipelineLayout: vk.PipelineLayout,
    models: std.ArrayList(NeonVkObjectDataGpu) = .{},
    objectData: std.ArrayList(ObjectSharedData) = .{},
    skinning: std.ArrayList(core.Mat) = .{},
    extent: core.Vector2f,
};

const DisplayTarget = struct {
    surface: vk.SurfaceKHR,
    surfaceFormat: vk.SurfaceFormatKHR,
    presentMode: vk.PresentModeKHR,
    depthFormat: vk.Format,

    depthImage: NeonVkImage = undefined,
    depthImageView: vk.ImageView = undefined,
    scissor: vk.Rect2D = undefined,
    viewport: vk.Viewport = undefined,
    swapchain: vk.SwapchainKHR = .null_handle,
    swapImages: []NeonVkSwapImage = undefined,
    framebuffers: []vk.Framebuffer = undefined,

    pub fn deinit(self: *@This(), rt: *RenderThread) void {
        var z1 = tracy.ZoneN(@src(), "RT - destroy displaytarget");
        defer z1.End();

        var z2 = tracy.ZoneN(@src(), "RT - destroy swapimages");
        for (self.swapImages) |*swapImage| {
            swapImage.deinit(vkd.*, rt.dev);
        }
        z2.End();
        rt.allocator.free(self.swapImages);

        var z3 = tracy.ZoneN(@src(), "RT - destroy framebuffers");
        for (self.framebuffers) |fb| {
            vkd.destroyFramebuffer(rt.dev, fb, null);
        }
        rt.allocator.free(self.framebuffers);
        z3.End();

        var z4 = tracy.ZoneN(@src(), "RT - destroy swapchain");
        vkd.destroySwapchainKHR(rt.dev, self.swapchain, null);
        self.swapchain = .null_handle;
        z4.End();

        var z5 = tracy.ZoneN(@src(), "RT - destroy imageviews");
        vkd.destroyImageView(rt.dev, self.depthImageView, null);
        self.depthImage.deinit(rt.vkAllocator);
        z5.End();
    }
};

// all the high level synchronization required for a frame
// task to be done with this engine
const FrameSyncs = struct {
    // semaphore = resource dependency specification
    // barrier = command and data flow completion specification
    // fence = notification to host side

    acquire: vk.Semaphore,
    renderComplete: vk.Semaphore,
    cmdFence: vk.Fence,

    pub fn deinit(self: *@This(), dev: vk.Device) void {
        var z1 = tracy.ZoneN(@src(), "RT - framesync deinit");
        defer z1.End();
        vkd.destroySemaphore(dev, self.acquire, null);
        vkd.destroySemaphore(dev, self.renderComplete, null);
        vkd.destroyFence(dev, self.cmdFence, null);
    }
};

pub fn setup(self: *@This(), gc: *NeonVkContext) !void {
    self.actual_extent = try vk_swapchain_helpers.findActualExtent(self.extent, self.caps);
    self.meshPool = try MeshPoolBuffers.create(self.allocator, gc, .{});

    try self.createSyncs();
    try self.initCommandBuffers();
    try self.initOrRecycleSwapchain();
    try self.initFramebuffers();
    try self.initIndirectBuffers();
    try self.initShared();
}

pub fn uploadIndirectCommands(self: *@This(), cmd: vk.CommandBuffer, count: u32) void {
    vkd_utils.copyStagingSlice(vk.DrawIndexedIndirectCommand, cmd, .{
        .src = &self.indirectStaging,
        .dst = &self.indirectGpu,
        .size = count,
    });
}

pub fn initIndirectBuffers(self: *@This()) !void {
    self.indirectStaging = try self.vkAllocator.createStagingBuffer(8192 * @sizeOf(vk.DrawIndexedIndirectCommand), "main systems indirect staging buffer");
    self.indirectGpu = try self.vkAllocator.createIndirectCommandBuffer(8192 * @sizeOf(vk.DrawIndexedIndirectCommand), "main systems indirect command buffer");
}

fn deinitExtras(self: *@This()) void {
    _ = self;
}

pub fn onExitSignal(self: *@This()) void {
    self.exitSignal.store(true, .seq_cst);
}

fn destroySyncs(self: *@This()) void {
    var z1 = tracy.ZoneN(@src(), "RT - destroy syncs");
    defer z1.End();

    for (&self.frameSync) |*frameSync| {
        frameSync.deinit(self.dev);
    }
    vkd.destroySemaphore(self.dev, self.emptyAcquireSemaphore, null);
}

fn deinitCommandBuffers(self: *@This()) void {

    // no-op
    _ = self;
}

fn deinitSwapchain(_: *@This()) void {}

fn deinitShared(self: *@This()) void {
    var z1 = tracy.ZoneN(@src(), "RT - deinit shared");
    defer z1.End();

    for (&self.sharedData) |*s| {
        s.models.deinit();
        s.objectData.deinit();
    }
}

fn processExitSignal(self: *@This()) void {
    var z1 = tracy.ZoneN(@src(), "RT - destruction");
    defer z1.End();

    core.engine_logs("RT - Process Exit Signal");

    self.vkAllocator.destroyBuffer(&self.indirectGpu);
    self.vkAllocator.destroyBuffer(&self.indirectStaging);

    vkd.queueWaitIdle(self.graphicsQueue.handle) catch {};

    self.deinitShared();
    self.destroySyncs();
    self.deinitCommandBuffers();

    self.displayTarget.deinit(self);
    self.deinitExtras();
    self.listeners.deinit(self.allocator);
    self.meshPool.destroy();

    self.exitConfirmed.store(true, .seq_cst);
}

pub fn acquireNextFrame(self: *@This()) !u32 {
    var z1 = tracy.ZoneNC(@src(), "Waiting for frame", 0x111111);
    defer z1.End();

    const nextFrameIndex = try self.getNextSwapImage();

    _ = try vkd.waitForFences(
        self.dev,
        1,
        @as([*]const vk.Fence, @ptrCast(&self.frameSync[nextFrameIndex].cmdFence)),
        1,
        vk_constants.FrameTimeout,
    );
    try vkd.resetFences(self.dev, 1, @as([*]const vk.Fence, @ptrCast(&self.frameSync[nextFrameIndex].cmdFence)));

    return nextFrameIndex;
}

pub fn spinProcessExitSignal(self: *@This()) void {
    while (self.framesInFlight.load(.acquire) > 0) {}
    vkd.deviceWaitIdle(self.dev) catch {};
    self.processExitSignal();
    return;
}

pub fn updateMeshImages(self: *@This()) !void {
    if (graphics.getContext().newMeshImages.count() > 0) {
        graphics.getContext().newMeshImages.lock();
        defer graphics.getContext().newMeshImages.unlock();
        while (graphics.getContext().newMeshImages.popFromUnlocked()) |new| {
            var imageBufferInfo = new.bufferInfo;
            for (0..self.frameData.len) |i| {
                var writeDescriptorSet = vkinit.writeDescriptorImage(
                    .combined_image_sampler,
                    self.frameData[i].globalDescriptorSet,
                    &imageBufferInfo,
                    2,
                );
                writeDescriptorSet.dst_array_element = new.textureId;
                vkd.updateDescriptorSets(self.dev, 1, @ptrCast(&writeDescriptorSet), 0, undefined);
            }
        }
    }
}

// can be called from any thread.
// queues up a render job for the next frame
pub fn dispatchNextFrame(self: *@This(), deltaTime: f64, frameIndex: u32) !void {
    var z2 = tracy.ZoneN(@src(), "rendering job request");
    defer z2.End();

    const L = struct {
        r: *RenderThread,
        dt: f64,
        frameIndex: u32,

        pub fn func(ctx: *@This(), _: *core.JobContext) void {
            // check for mesh pool updates
            ctx.r.updateMeshImages() catch unreachable;
            ctx.r.meshPool.checkUpdates() catch unreachable;
            ctx.r.draw(ctx.dt, ctx.frameIndex) catch unreachable;
            while (ctx.r.framesInFlight.cmpxchgStrong(1, 0, .seq_cst, .acquire) != null) {}
        }
    };

    try core.dispatchJob(L{
        .r = self,
        .dt = deltaTime,
        .frameIndex = frameIndex,
    });
}

pub fn getFrameData(self: *@This(), index: u32) *NeonVkFrameData {
    return &self.frameData[index];
}

pub fn getShared(self: *@This(), index: u32) *SharedData {
    return &self.sharedData[index];
}

pub fn pad_uniform_buffer_size(self: @This(), originalSize: usize) usize {
    const alignment = @as(usize, @intCast(self.pdevProperties.limits.min_uniform_buffer_offset_alignment));

    var alignedSize: usize = originalSize;
    if (alignment > 0) {
        alignedSize = (alignedSize + alignment - 1) & ~(alignment - 1);
    }

    return alignedSize;
}

pub fn renderMeshes(self: *@This(), cmd: vk.CommandBuffer, fi: u32) void {
    const shared = self.getShared(fi);

    const offset: vk.DeviceSize = 0;
    const paddedSceneSize = @as(u32, @intCast(self.pad_uniform_buffer_size(@sizeOf(NeonVkSceneDataGpu))));
    const startOffset: u32 = paddedSceneSize * fi;

    const frameData = self.getFrameData(fi);
    const layout = shared.pipelineLayout;
    const pipeline = shared.pipeline;

    var buffers = graphics.getMeshPoolBuffers();
    vkd.cmdBindVertexBuffers(cmd, 0, 1, @ptrCast(&buffers.vertex.buffer), @ptrCast(&offset));
    vkd.cmdBindIndexBuffer(cmd, buffers.index.buffer, 0, .uint32);

    // skybox
    const skybox = SkyboxSystem.gSkybox;
    if (skybox.cubeMapShared[fi] != null and skybox.mesh != null) {
        vkd.cmdBindPipeline(cmd, .graphics, skybox.material.pipeline);
        vkd.cmdBindDescriptorSets(cmd, .graphics, skybox.material.layout, 0, 1, @ptrCast(&frameData.globalDescriptorSet), 1, @ptrCast(&startOffset));
        vkd.cmdBindDescriptorSets(cmd, .graphics, skybox.material.layout, 1, 1, @ptrCast(&skybox.cubeMapShared[fi]), 0, undefined);
        const skyboxMesh = skybox.mesh.?;
        vkd.cmdDrawIndexed(cmd, skyboxMesh.index.size, 1, skyboxMesh.index.start, 0, 0);
    }

    // all regular meshes
    vkd.cmdBindPipeline(cmd, .graphics, pipeline);
    vkd.cmdBindDescriptorSets(cmd, .graphics, layout, 0, 1, @ptrCast(&frameData.globalDescriptorSet), 1, @ptrCast(&startOffset));
    vkd.cmdBindDescriptorSets(cmd, .graphics, layout, 1, 1, @ptrCast(&frameData.objectDescriptorSet), 0, undefined);

    const count = shared.objectData.items.len;
    vkd.cmdDrawIndexedIndirect(cmd, self.indirectGpu.buffer, 0, @intCast(count), @sizeOf(vk.DrawIndexedIndirectCommand));
}

pub fn updateIndirectBuffers(self: *@This(), cmd: vk.CommandBuffer, fi: u32) !void {
    const shared = self.getShared(fi);

    const count = shared.objectData.items.len;
    if (count == 0)
        return;

    const mapped = self.vkAllocator.mapBuffer(vk.DrawIndexedIndirectCommand, self.indirectStaging) catch unreachable;
    defer self.vkAllocator.unmapMemory(self.indirectStaging);

    for (shared.objectData.items, 0..) |object, i| {
        const meshBuffer = object.indexedMesh;
        mapped[i] = .{
            .index_count = meshBuffer.index.size,
            .instance_count = 1,
            .first_index = meshBuffer.index.start,
            .vertex_offset = 0,
            .first_instance = @intCast(i),
        };
    }

    self.uploadIndirectCommands(cmd, @intCast(count));
}

// fi = frameIndex
pub fn draw(self: *@This(), deltaTime: f64, fi: u32) !void {
    _ = deltaTime;
    var z1 = tracy.ZoneNC(@src(), "draw", 0x00FF1111);
    defer z1.End();

    if (!self.isMinimized.load(.acquire)) {
        const syncIndex = self.acquireNextFrame() catch |err| {
            switch (err) {
                error.OutOfDateKHR => {
                    if (self.updateExtentIfDirty()) {
                        try self.resizeToNewExtents();
                    }
                    return;
                },
                else => {
                    return err;
                },
            }
        };
        try self.preFrameUpdate(fi);
        const cmd = try self.startFrameCommands(fi);

        // try self.meshPool.updateRequests(cmd);
        var z = tracy.ZoneNC(@src(), "Main RenderPass", 0x00FF1111);
        const time = core.getEngineTime();

        try self.updateIndirectBuffers(cmd, fi);
        self.preDrawPlugins(cmd, fi);
        try self.beginMainRenderpass(cmd, syncIndex);

        self.renderMeshes(cmd, fi);
        self.postDrawPlugins(cmd, fi);

        try self.finishMainRenderpass(cmd, fi);
        try self.dynamicMeshManager.updateMeshes(cmd);

        const delta = core.getEngineTime() - time;

        core.rollingAverage(&core.getEngine().renderThreadTime, delta, 60);

        z.End();
        try vkd.endCommandBuffer(cmd);
        try self.finishFrame(fi, syncIndex);
    } else {
        if (self.updateExtentIfDirty()) {
            try self.resizeToNewExtents();

            std.time.sleep(300 * 1000 * 1000);
        }
    }
    // self.dynamicMeshManager.finishUpload() catch unreachable;
}

fn preDrawPlugins(self: *@This(), cmd: vk.CommandBuffer, fi: u32) void {
    for (self.plugins.items) |interface| {
        if (interface.vtable.rtPreDraw) |rtPreDraw| {
            rtPreDraw(interface.ptr, self, cmd, fi);
        }
    }
}

fn postDrawPlugins(self: *@This(), cmd: vk.CommandBuffer, fi: u32) void {
    for (self.plugins.items) |interface| {
        if (interface.vtable.rtPostDraw) |rtPostDraw| {
            rtPostDraw(interface.ptr, self, cmd, fi);
        }
    }
}

fn finishMainRenderpass(self: *@This(), cmd: vk.CommandBuffer, fi: u32) !void {
    _ = self;
    _ = fi;
    vkd.cmdEndRenderPass(cmd);
}

fn preFrameUpdate(self: *@This(), fi: u32) !void {
    for (self.listeners.items) |entry| {
        entry.func(entry.ptr);
    }

    // upload camera data
    const shared = self.getShared(fi);
    shared.lock.lock();
    defer shared.lock.unlock();

    shared.extent = .{
        .x = @as(f32, @floatFromInt(platform.getInstance().extent().x)),
        .y = @as(f32, @floatFromInt(platform.getInstance().extent().y)),
    };

    {
        const data = try self.vkAllocator.vmaAllocator.mapMemory(self.frameData[fi].cameraBuffer.allocation, u8);
        shared.cameraData.upload(data);
        self.vkAllocator.vmaAllocator.unmapMemory(self.frameData[fi].cameraBuffer.allocation);
    }

    // upload scene global data
    {
        const paddedSceneSize = self.padUniformBufferSize(@sizeOf(NeonVkSceneDataGpu));
        const startOffset = paddedSceneSize * fi;
        const data = try self.vkAllocator.vmaAllocator.mapMemory(self.sceneParameterBuffer.allocation, u8);

        var dataSlice: []u8 = undefined;
        dataSlice.ptr = data + startOffset;
        dataSlice.len = @sizeOf(@TypeOf(shared.sceneData));

        var inputSlice: []const u8 = undefined;
        inputSlice.ptr = @as([*]const u8, @ptrCast(&shared.sceneData));
        inputSlice.len = dataSlice.len;

        @memcpy(dataSlice, inputSlice);

        self.vkAllocator.vmaAllocator.unmapMemory(self.sceneParameterBuffer.allocation);
    }

    try self.uploadObjectData(shared, fi);
}

fn uploadObjectData(self: *@This(), shared: *SharedData, fi: u32) !void {
    var z1 = tracy.ZoneN(@src(), "uploading ssbo data");
    defer z1.End();
    {
        const allocation = self.frameData[fi].objectBuffer.allocation;
        const data = try self.vkAllocator.vmaAllocator.mapMemory(allocation, NeonVkObjectDataGpu);
        var ssbo: []NeonVkObjectDataGpu = undefined;
        ssbo.ptr = @as([*]NeonVkObjectDataGpu, @ptrCast(data));
        ssbo.len = self.maxObjectCount;

        for (shared.models.items, 0..) |model, i| {
            ssbo[i] = model;
        }

        self.vkAllocator.vmaAllocator.unmapMemory(allocation);
    }

    // animations
    {
        const finals = try self.vkAllocator.mapBuffer(core.Mat, self.frameData[fi].animationsBuffer);
        defer self.vkAllocator.unmapMemory(self.frameData[fi].animationsBuffer);

        const as = animationSystem.gAnimationSys;
        as.sharedLocks[fi].lock();
        defer as.sharedLocks[fi].unlock();

        const sharedFinals = as.getShared(fi);

        for (sharedFinals) |upload| {
            for (upload.matrices.items, 0..) |final, i| {
                finals[upload.offset + i] = final;
            }
        }
    }
}

fn padUniformBufferSize(self: @This(), originalSize: usize) usize {
    var alignedSize: usize = originalSize;
    if (self.minUniformBufferAlignment > 0) {
        alignedSize = (alignedSize + self.minUniformBufferAlignment - 1) & ~(self.minUniformBufferAlignment - 1);
    }

    return alignedSize;
}

fn startFrameCommands(self: *@This(), fi: u32) !vk.CommandBuffer {
    const cmd = self.commandBuffers[fi];

    try vkd.resetCommandBuffer(cmd, .{});

    var cbi = vk.CommandBufferBeginInfo{
        .p_inheritance_info = null,
        .flags = .{ .one_time_submit_bit = true },
    };

    try vkd.beginCommandBuffer(cmd, &cbi);

    return cmd;
}

fn beginMainRenderpass(self: *@This(), cmd: vk.CommandBuffer, syncIndex: u32) !void {
    var z = tracy.ZoneNC(@src(), "Begin RenderPass", 0xFFBBBB);
    defer z.End();

    var clearValues = [2]vk.ClearValue{
        .{
            .color = .{ .float_32 = [4]f32{ 0.005, 0.005, 0.005, 1.0 } },
        },
        .{
            .depth_stencil = .{
                .depth = 1.0,
                .stencil = 0.0,
            },
        },
    };

    var rpbi = vk.RenderPassBeginInfo{
        .render_area = .{
            .extent = self.actual_extent,
            .offset = .{ .x = 0, .y = 0 },
        },
        .framebuffer = self.displayTarget.framebuffers[syncIndex],
        .render_pass = self.renderPass,
        .clear_value_count = 2,
        .p_clear_values = @as([*]const vk.ClearValue, @ptrCast(&clearValues)),
    };

    vkd.cmdBeginRenderPass(cmd, &rpbi, .@"inline");

    vkd.cmdSetViewport(cmd, 0, 1, @ptrCast(&self.displayTarget.viewport));
    vkd.cmdSetScissor(cmd, 0, 1, @ptrCast(&self.displayTarget.scissor));
}

fn finishFrame(self: *@This(), frameIndex: u32, syncIndex: u32) !void {
    var waitStage = vk.PipelineStageFlags{ .color_attachment_output_bit = true };

    var submit = vk.SubmitInfo{
        .p_wait_dst_stage_mask = @as([*]const vk.PipelineStageFlags, @ptrCast(&waitStage)),
        .wait_semaphore_count = 1,
        .p_wait_semaphores = @as([*]const vk.Semaphore, @ptrCast(&self.frameSync[syncIndex].acquire)),
        .signal_semaphore_count = 1,
        .p_signal_semaphores = @as([*]const vk.Semaphore, @ptrCast(&self.frameSync[syncIndex].renderComplete)),
        .command_buffer_count = 1,
        .p_command_buffers = @as([*]const vk.CommandBuffer, @ptrCast(&self.commandBuffers[frameIndex])),
    };

    try vkd.queueSubmit(
        self.graphicsQueue.handle,
        1,
        @as([*]const vk.SubmitInfo, @ptrCast(&submit)),
        self.frameSync[syncIndex].cmdFence,
    );

    var presentInfo = vk.PresentInfoKHR{
        .p_swapchains = @as([*]const vk.SwapchainKHR, @ptrCast(&self.displayTarget.swapchain)),
        .swapchain_count = 1,
        .p_wait_semaphores = @as([*]const vk.Semaphore, @ptrCast(&self.frameSync[syncIndex].renderComplete)),
        .wait_semaphore_count = 1,
        .p_image_indices = @as([*]const u32, @ptrCast(&syncIndex)),
        .p_results = null,
    };

    // todo re-implement handling out of date KHR
    var outOfDate: bool = false;
    _ = vkd.queuePresentKHR(self.graphicsQueue.handle, &presentInfo) catch |err| switch (err) {
        error.OutOfDateKHR => {
            outOfDate = true;
        },
        else => |narrow| return narrow,
    };

    if (outOfDate and self.updateExtentIfDirty()) {
        try self.resizeToNewExtents();
    }
}

fn resizeToNewExtents(self: *@This()) !void {
    var z1 = tracy.ZoneN(@src(), "resizing to new extents");
    defer z1.End();
    self.isMinimized.store(false, .seq_cst);
    try vkd.deviceWaitIdle(self.dev);
    self.displayTarget.deinit(self);

    try self.initOrRecycleSwapchain();
    try self.initFramebuffers();
}

pub fn minimized(self: @This()) bool {
    return self.isMinimized.load(.acquire);
}

pub fn checkIfStillMinimized() bool {
    var w: c_int = undefined;
    var h: c_int = undefined;
    platform.glfw3.glfwGetWindowSize(platform.getInstance().window, &w, &h);

    if (w > 0 and h > 0) {
        return true;
    }
    return false;
}

pub fn updateExtentIfDirty(self: *@This()) bool {
    var w: c_int = undefined;
    var h: c_int = undefined;
    platform.glfw3.glfwGetWindowSize(platform.getInstance().window, &w, &h);

    if (((self.extent.width != @as(u32, @intCast(w)) or self.extent.height != @as(u32, @intCast(h))) and w > 0 and h > 0)) {
        self.extent = .{ .width = @as(u32, @intCast(w)), .height = @as(u32, @intCast(h)) };
        platform.getInstance().updateExtent(.{ .x = w, .y = h });

        return true;
    }

    if (w <= 0 or h <= 0) {
        self.extent.width = 0;
        self.extent.height = 0;
        self.isMinimized.store(true, .seq_cst);
    } else {
        self.isMinimized.store(false, .seq_cst);
    }

    return false;
}

fn getNextSwapImage(self: *@This()) !u32 {
    const imageIndex = (try vkd.acquireNextImageKHR(
        self.dev,
        self.displayTarget.swapchain,
        1000000000,
        self.emptyAcquireSemaphore,
        .null_handle,
    )).image_index;

    // core.assert(imageIndex < 2) catch @panic("swap index > 2??");
    // load the newly primed semaphore for the acquire operation into the next slot
    std.mem.swap(
        vk.Semaphore,
        &self.emptyAcquireSemaphore,
        &self.frameSync[imageIndex].acquire,
    );

    return imageIndex;
}

// -- initSyncs --
fn createSyncs(self: *@This()) !void {
    const semaphoreCreateInfo = vk.SemaphoreCreateInfo{
        .flags = .{},
    };

    var fci = vk.FenceCreateInfo{
        .flags = .{ .signaled_bit = true },
    };

    for (0..NumFrames) |i| {
        self.frameSync[i].acquire = try vkd.createSemaphore(self.dev, &semaphoreCreateInfo, null);
        self.frameSync[i].renderComplete = try vkd.createSemaphore(self.dev, &semaphoreCreateInfo, null);
        self.frameSync[i].cmdFence = try vkd.createFence(self.dev, &fci, null);
    }

    self.emptyAcquireSemaphore = try vkd.createSemaphore(self.dev, &semaphoreCreateInfo, null);
}

// -- init command buffers --
fn initCommandBuffers(self: *@This()) !void {
    var cbai = vk.CommandBufferAllocateInfo{
        .command_pool = self.cmdPool,
        .level = vk.CommandBufferLevel.primary,
        .command_buffer_count = NumFrames,
    };
    try vkd.allocateCommandBuffers(self.dev, &cbai, &self.commandBuffers);
}

// -- Swapchain initialization --
fn initOrRecycleSwapchain(self: *@This()) !void {
    self.caps = try vki.getPhysicalDeviceSurfaceCapabilitiesKHR(self.pdev, self.displayTarget.surface);

    self.displayTarget.presentMode = try vk_swapchain_helpers.findPresentMode(self.allocator, self.pdev, self.displayTarget.surface);

    self.actual_extent = try vk_swapchain_helpers.findActualExtent(self.extent, self.caps);

    if (self.actual_extent.width == 0 or self.actual_extent.height == 0) {
        return error.InvalidSurfaceDimensions;
    }

    var image_count = @as(u32, @intCast(NumFrames));
    if (self.caps.max_image_count > 0) {
        image_count = @min(image_count, self.caps.max_image_count);
    }

    const qfi = [_]u32{ self.graphicsQueue.family, self.presentQueue.family };

    const sharing_mode: vk.SharingMode = if (self.graphicsQueue.family != self.presentQueue.family)
        .concurrent
    else
        .exclusive;

    var scci = vk.SwapchainCreateInfoKHR{
        .flags = .{},
        .surface = self.displayTarget.surface,
        .min_image_count = image_count,
        .image_format = self.displayTarget.surfaceFormat.format,
        .image_color_space = self.displayTarget.surfaceFormat.color_space,
        .image_extent = self.actual_extent,
        .image_array_layers = 1,
        .image_usage = .{ .color_attachment_bit = true, .transfer_dst_bit = true },
        .image_sharing_mode = sharing_mode,
        .queue_family_index_count = qfi.len,
        .p_queue_family_indices = &qfi,
        .pre_transform = self.caps.current_transform,
        .composite_alpha = .{ .opaque_bit_khr = true },
        .present_mode = self.displayTarget.presentMode,
        .clipped = vk.TRUE,
        .old_swapchain = self.displayTarget.swapchain,
    };

    const newSwapchain = try vkd.createSwapchainKHR(self.dev, &scci, null);
    errdefer vkd.destroySwapchainKHR(self.dev, newSwapchain, null);

    if (self.displayTarget.swapchain != .null_handle) {
        vkd.destroySwapchainKHR(self.dev, self.displayTarget.swapchain, null);
    }

    self.displayTarget.swapchain = newSwapchain;
    try self.createSwapchainImagesAndViews();

    self.displayTarget.viewport = vk.Viewport{
        .x = 0,
        .y = 0,
        .width = @as(f32, @floatFromInt(self.actual_extent.width)),
        .height = @as(f32, @floatFromInt(self.actual_extent.height)),
        .min_depth = 0.0,
        .max_depth = 1.0,
    };

    self.displayTarget.scissor = .{
        .offset = .{ .x = 0, .y = 0 },
        .extent = self.actual_extent,
    };
}

fn createSwapchainImagesAndViews(self: *@This()) !void {
    var count: u32 = 0;
    _ = try vkd.getSwapchainImagesKHR(self.dev, self.displayTarget.swapchain, &count, null);
    if (count == 0) {
        core.engine_errs("No swap chain image found");
        return error.NoSwapchainImagesFound;
    }
    core.graphics_log("Creating {d} swap images", .{count});
    self.displayTarget.swapImages = try self.allocator.alloc(NeonVkSwapImage, count);

    const images = try self.allocator.alloc(vk.Image, count);
    defer self.allocator.free(images);

    _ = try vkd.getSwapchainImagesKHR(self.dev, self.displayTarget.swapchain, &count, images.ptr);

    for (0..NumFrames) |i| {
        const image = images[i];

        var ivci = vk.ImageViewCreateInfo{
            .flags = .{},
            .image = image,
            .view_type = .@"2d",
            .format = self.displayTarget.surfaceFormat.format,
            .components = .{ .r = .r, .g = .g, .b = .b, .a = .a },
            .subresource_range = .{
                .aspect_mask = .{
                    .color_bit = true,
                },
                .base_mip_level = 0,
                .level_count = 1,
                .base_array_layer = 0,
                .layer_count = 1,
            },
        };

        const imageView = try vkd.createImageView(self.dev, &ivci, null);

        const swapImage = NeonVkSwapImage{
            .image = image,
            .view = imageView,
            .imageIndex = i,
        };

        self.displayTarget.swapImages[i] = swapImage;
    }

    const depthImageExtent = vk.Extent3D{
        .width = self.actual_extent.width,
        .height = self.actual_extent.height,
        .depth = 1,
    };

    const dimg_ici = vk.ImageCreateInfo{
        .flags = .{},
        .sharing_mode = .exclusive,
        .queue_family_index_count = 0,
        .p_queue_family_indices = undefined,
        .initial_layout = .undefined,
        .image_type = .@"2d",
        .format = self.displayTarget.depthFormat,
        .extent = depthImageExtent,
        .mip_levels = 1,
        .array_layers = 1,
        .samples = .{
            .@"1_bit" = true,
        },
        .tiling = .optimal,
        .usage = .{ .depth_stencil_attachment_bit = true },
    };

    const dimg_aci = vma.AllocationCreateInfo{
        .requiredFlags = .{
            .device_local_bit = true,
        },
        .usage = .gpuOnly,
    };

    self.displayTarget.depthImage = try self.vkAllocator.createImage(dimg_ici, dimg_aci, @src().fn_name ++ " depth Image - rt");

    var imageViewCreate = vk.ImageViewCreateInfo{
        .flags = .{},
        .image = self.displayTarget.depthImage.image,
        .view_type = .@"2d",
        .format = self.displayTarget.depthFormat,
        .components = .{ .r = .identity, .g = .identity, .b = .identity, .a = .identity },
        .subresource_range = .{
            .aspect_mask = .{
                .depth_bit = true,
            },
            .base_mip_level = 0,
            .level_count = 1,
            .base_array_layer = 0,
            .layer_count = 1,
        },
    };

    self.displayTarget.depthImageView = try vkd.createImageView(self.dev, &imageViewCreate, null);
}

fn initShared(self: *@This()) !void {
    for (&self.sharedData) |*s| {
        s.lock = .{};
        s.models = std.ArrayList(NeonVkObjectDataGpu).init(self.allocator);
        s.objectData = std.ArrayList(ObjectSharedData).init(self.allocator);
    }
}

// init framebuffers
fn initFramebuffers(self: *@This()) !void {
    self.displayTarget.framebuffers = try self.allocator.alloc(vk.Framebuffer, self.getImageCount());

    var attachments: [2]vk.ImageView = undefined;

    var fbci = vk.FramebufferCreateInfo{
        .flags = .{},
        .render_pass = self.renderPass,
        .attachment_count = 2,
        .p_attachments = &attachments,
        .width = self.actual_extent.width,
        .height = self.actual_extent.height,
        .layers = 1,
    };

    for (self.displayTarget.swapImages, 0..) |image, i| {
        attachments[0] = image.view;
        attachments[1] = self.displayTarget.depthImageView;
        self.displayTarget.framebuffers[i] = try vkd.createFramebuffer(self.dev, &fbci, null);
    }
}

inline fn getImageCount(self: @This()) usize {
    return self.displayTarget.swapImages.len;
}

inline fn maxFramesInFlight() u32 {
    return 1;
}

pub fn installListener(
    self: *@This(),
    ptr: *anyopaque,
    func: *const fn (*anyopaque) void,
) !void {
    try self.listeners.append(self.allocator, .{ .ptr = ptr, .func = func });
}

const RenderThread = @This();
const std = @import("std");
const vk = @import("vulkan");
const core = @import("core");
const platform = @import("platform");
const vma = @import("vma");
const tracy = core.tracy;

const vk_api = @import("../vk_api.zig");
const vk_constants = @import("../vk_constants.zig");
const vk_allocator = @import("../vk_allocator.zig");
const render_objects = @import("../render_objects.zig");

const vk_renderer_types = @import("vk_renderer_types.zig");
const vk_swapchain_helpers = @import("vk_swapchain_helpers.zig");
const vk_renderer_camera_gpu = @import("vk_renderer_camera_gpu.zig");

const NeonVkAllocator = vk_allocator.NeonVkAllocator;
const NeonVkBuffer = vk_allocator.NeonVkBuffer;
const NeonVkImage = vk_allocator.NeonVkImage;

const NumFrames = vk_constants.NUM_FRAMES;

const NeonVkQueue = vk_renderer_types.NeonVkQueue;
const NeonVkSwapImage = vk_renderer_types.NeonVkSwapImage;
const NeonVkFrameData = vk_renderer_types.NeonVkFrameData;
const NeonVkSceneDataGpu = vk_renderer_types.NeonVkSceneDataGpu;
const NeonVkObjectDataGpu = vk_renderer_types.NeonVkObjectDataGpu;
const NeonVkSwapchain = vk_renderer_types.NeonVkSwapchain;

const vk_renderer_interface = @import("vk_renderer_interface.zig");
const RendererInterfaceRef = vk_renderer_interface.RendererInterfaceRef;

const mesh = @import("../mesh.zig");

const mesh_pool = @import("vk_mesh_pool.zig");
const MeshPoolBuffers = mesh_pool.MeshPoolBuffers;

const graphics = @import("../graphics.zig");
const NeonVkContext = graphics.NeonVkContext;

const texture_list = @import("vk_texture_list.zig");
const TextureList = texture_list.TextureList;

const vkd_utils = @import("vkd_utils.zig");
const vkinit = @import("../vk_init.zig");
const animationSystem = @import("../animation/animationSystem.zig");

const SkyboxSystem = @import("../skybox.zig");
// const vk_mesh_pool = @import("vk_mesh_pool.zig");
// const MeshPool = vk_mesh_pool.MeshPool;

// todo and documentation
//
// This struct is the root of the feature to have a seperate rendering thread for handling all vulkan IOs
// the way this will work is, instead of having all processing done on the main systems thread.
// the systems thread will send over a set of data required to render each given frame.
// this data is called sharedData.
//
// all plugins into the renderer will follow this arrangement as well.
// this includes the dynamicMeshManager and anything else.
// they will all have a similar setup to this.
//
