// vim:fileencoding=utf-8:foldmethod=marker
//
// Vulkan backend — single-file implementation of the RHI API.
// Uses volk (dynamic loader) + vulkan_profiles + VMA.
// Profile: VP_PRISM_DESKTOP_2026

#include "pr_rhi_vk.h"
#include "profiles/vulkan_profiles.h"
#include "../pr_rhi.h"
#include "vulkan/vulkan_core.h"
#include <volk/volk.h>
#include <vk_mem_alloc.h>
#include <SDL3/SDL_vulkan.h>
#include <ktx.h>
#include <ktxvulkan.h>
#include <string.h>

#define TEMP_FENCE_MAX_COUNT 64

// ============================================================================
// Typedefs for wapp arrays of Vulkan handle types
// ============================================================================

typedef VkPhysicalDevice                    *VkPhysicalDeviceArray;
typedef VkQueueFamilyProperties2            *VkQueueFamilyProperties2Array;
typedef VkFormat                            *VkFormatArray;
typedef VkBufferImageCopy                   *VkBufferImageCopyArray;
typedef VkBuffer                            *VkBufferArray;
typedef VkImage                             *VkImageArray;
typedef VkImageView                         *VkImageViewArray;
typedef VkDescriptorSetLayoutBinding        *VkDescriptorSetLayoutBindingArray;
typedef VkDescriptorBindingFlags            *VkDescriptorBindingFlagsArray;
typedef VkDescriptorSetLayout               *VkDescriptorSetLayoutArray;
typedef VkDescriptorPoolSize                *VkDescriptorPoolSizeArray;
typedef VkDescriptorImageInfo               *VkDescriptorImageInfoArray;
typedef VkDescriptorBufferInfo              *VkDescriptorBufferInfoArray;
typedef VkDescriptorSetLayout               *VkDescriptorSetLayoutArray;
typedef VkDescriptorSet                     *VkDescriptorSetArray;
typedef VkPushConstantRange                 *VkPushConstantRangeArray;
typedef VkCommandBuffer                     *VkCommandBufferArray;
typedef VkVertexInputBindingDescription     *VkVertexInputBindingDescriptionArray;
typedef VkVertexInputAttributeDescription   *VkVertexInputAttributeDescriptionArray;
typedef VkDynamicState                      *VkDynamicStateArray;
typedef VkPipelineColorBlendAttachmentState *VkPipelineColorBlendAttachmentStateArray;
typedef VkRenderingAttachmentInfo           *VkRenderingAttachmentInfoArray;

// ============================================================================
// Helpers
// ============================================================================

wp_intern void _abort(const char *msg);
wp_intern void _checkVk(VkResult res, const char *site);
wp_intern void _checkVkBool(VkResult res, const char *site);
wp_intern VkFormat _toVkFormat(PrRhiFormat fmt);
wp_intern VkImageUsageFlags _toVkTextureUsage(PrRhiTextureUsage usage);
wp_intern VkBufferUsageFlags _toVkBufferUsage(PrRhiBufferUsage usage);
wp_intern VkMemoryPropertyFlags _toVkMemoryProperty(PrRhiMemoryUsage mem);
wp_intern VmaAllocationCreateFlags _toVmaFlags(PrRhiMemoryUsage mem);
wp_intern VkImageLayout _toVkImageLayout(PrRhiImageLayout layout);
wp_intern VkShaderStageFlags _toVkShaderStage(PrRhiShaderStage stage);
wp_intern VkDescriptorType _toVkDescriptorType(PrRhiDescriptorType type);
wp_intern VkDescriptorBindingFlags _toVkBindingFlags(PrRhiDescriptorBindingFlag flags);
wp_intern VkCompareOp _toVkCompareOp(PrRhiCompareOp op);
wp_intern VkPrimitiveTopology _toVkTopology(PrRhiPrimitiveTopology topo);
wp_intern VkIndexType _toVkIndexType(PrRhiIndexType it);
wp_intern VkPresentModeKHR _toVkPresentMode(PrRhiPresentMode mode);
wp_intern VkFilter _toVkFilter(PrRhiFilter f);
wp_intern VkSamplerMipmapMode _toVkMipmapMode(PrRhiMipmapMode m);
wp_intern VkSamplerAddressMode _toVkAddressMode(PrRhiAddressMode m);
wp_intern VkPipelineStageFlags2 _toVkPipelineStage2(PrRhiPipelineStage stage);
wp_intern VkAccessFlags2        _toVkAccess2(PrRhiAccess access);
wp_intern VkPhysicalDeviceType  _toVkPhysicalDeviceType(PrRhiPhysicalDeviceType type);

wp_persist const VpProfileProperties _profile = {
	.profileName = VP_PRISM_DESKTOP_2026_NAME,
	.specVersion = VP_PRISM_DESKTOP_2026_SPEC_VERSION,
};

wp_persist const VkFormat _default_image_format  = VK_FORMAT_B8G8R8A8_SRGB;
wp_persist const VkColorSpaceKHR _default_colorspace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR;

// ============================================================================
// Helper implementations
// ============================================================================

void _abort(const char *msg) {
	// TODO: use wapp abort when available
	__builtin_trap();
}

void _checkVk(VkResult res, const char *site) {
	if (res != VK_SUCCESS) _abort(site);
}

void _checkVkBool(VkResult res, const char *site) {
	if (res < VK_SUCCESS) _abort(site);
}

PrRhiFormat _fromVkFormat(VkFormat fmt) {
	switch (fmt) {
		case VK_FORMAT_R8G8B8A8_SRGB:       return PR_RHI_FORMAT_R8G8B8A8_SRGB;
		case VK_FORMAT_R8G8B8A8_UNORM:      return PR_RHI_FORMAT_R8G8B8A8_UNORM;
		case VK_FORMAT_R16G16B16A16_SFLOAT: return PR_RHI_FORMAT_R16G16B16A16_SFLOAT;
		case VK_FORMAT_R32G32B32A32_SFLOAT: return PR_RHI_FORMAT_R32G32B32A32_SFLOAT;
		case VK_FORMAT_R32G32B32_SFLOAT:    return PR_RHI_FORMAT_R32G32B32_SFLOAT;
		case VK_FORMAT_R32G32_SFLOAT:       return PR_RHI_FORMAT_R32G32_SFLOAT;
		case VK_FORMAT_R32_SFLOAT:          return PR_RHI_FORMAT_R32_SFLOAT;
		case VK_FORMAT_D24_UNORM_S8_UINT:   return PR_RHI_FORMAT_D24_UNORM_S8_UINT;
		case VK_FORMAT_D32_SFLOAT_S8_UINT:  return PR_RHI_FORMAT_D32_SFLOAT_S8_UINT;
		case VK_FORMAT_B8G8R8A8_SRGB:       return PR_RHI_FORMAT_B8G8R8A8_SRGB;
		default:                            return PR_RHI_FORMAT_UNDEFINED;
	}
}

VkFormat _toVkFormat(PrRhiFormat fmt) {
	switch (fmt) {
		case PR_RHI_FORMAT_R8G8B8A8_SRGB:          return VK_FORMAT_R8G8B8A8_SRGB;
		case PR_RHI_FORMAT_R8G8B8A8_UNORM:         return VK_FORMAT_R8G8B8A8_UNORM;
		case PR_RHI_FORMAT_R16G16B16A16_SFLOAT:    return VK_FORMAT_R16G16B16A16_SFLOAT;
		case PR_RHI_FORMAT_R32G32B32A32_SFLOAT:    return VK_FORMAT_R32G32B32A32_SFLOAT;
		case PR_RHI_FORMAT_R32G32B32_SFLOAT:       return VK_FORMAT_R32G32B32_SFLOAT;
		case PR_RHI_FORMAT_R32G32_SFLOAT:          return VK_FORMAT_R32G32_SFLOAT;
		case PR_RHI_FORMAT_R32_SFLOAT:             return VK_FORMAT_R32_SFLOAT;
		case PR_RHI_FORMAT_D24_UNORM_S8_UINT:      return VK_FORMAT_D24_UNORM_S8_UINT;
		case PR_RHI_FORMAT_D32_SFLOAT_S8_UINT:     return VK_FORMAT_D32_SFLOAT_S8_UINT;
		case PR_RHI_FORMAT_B8G8R8A8_SRGB:          return VK_FORMAT_B8G8R8A8_SRGB;
		default:                                   return VK_FORMAT_UNDEFINED;
	}
}

VkImageUsageFlags _toVkTextureUsage(PrRhiTextureUsage usage) {
	VkImageUsageFlags flags = 0;
	if (usage & PR_RHI_TEXTURE_USAGE_SAMPLED)          flags |= VK_IMAGE_USAGE_SAMPLED_BIT;
	if (usage & PR_RHI_TEXTURE_USAGE_COLOR_ATTACHMENT) flags |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
	if (usage & PR_RHI_TEXTURE_USAGE_DEPTH_ATTACHMENT) flags |= VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
	if (usage & PR_RHI_TEXTURE_USAGE_STORAGE)          flags |= VK_IMAGE_USAGE_STORAGE_BIT;
	if (usage & PR_RHI_TEXTURE_USAGE_TRANSFER_SRC)     flags |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
	if (usage & PR_RHI_TEXTURE_USAGE_TRANSFER_DST)     flags |= VK_IMAGE_USAGE_TRANSFER_DST_BIT;
	return flags;
}

VkBufferUsageFlags _toVkBufferUsage(PrRhiBufferUsage usage) {
	VkBufferUsageFlags flags = 0;
	if (usage & PR_RHI_BUFFER_USAGE_VERTEX)               flags |= VK_BUFFER_USAGE_VERTEX_BUFFER_BIT;
	if (usage & PR_RHI_BUFFER_USAGE_INDEX)                flags |= VK_BUFFER_USAGE_INDEX_BUFFER_BIT;
	if (usage & PR_RHI_BUFFER_USAGE_UNIFORM)              flags |= VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT;
	if (usage & PR_RHI_BUFFER_USAGE_STORAGE)              flags |= VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
	if (usage & PR_RHI_BUFFER_USAGE_TRANSFER_SRC)         flags |= VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
	if (usage & PR_RHI_BUFFER_USAGE_TRANSFER_DST)         flags |= VK_BUFFER_USAGE_TRANSFER_DST_BIT;
	if (usage & PR_RHI_BUFFER_USAGE_SHADER_DEVICE_ADDRESS) flags |= VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT;
	return flags;
}

VkMemoryPropertyFlags _toVkMemoryProperty(PrRhiMemoryUsage mem) {
	switch (mem) {
		case PR_RHI_MEMORY_GPU_ONLY:   return VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
		case PR_RHI_MEMORY_CPU_TO_GPU: return VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
		case PR_RHI_MEMORY_CPU_ONLY:   return VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT | VK_MEMORY_PROPERTY_HOST_CACHED_BIT;
		default:                       return 0;
	}
}

VmaAllocationCreateFlags _toVmaFlags(PrRhiMemoryUsage mem) {
	switch (mem) {
		case PR_RHI_MEMORY_GPU_ONLY:
			return 0;
		case PR_RHI_MEMORY_CPU_TO_GPU:
			return VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT |
			       VMA_ALLOCATION_CREATE_MAPPED_BIT;
		case PR_RHI_MEMORY_CPU_ONLY:
			return VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT |
			       VMA_ALLOCATION_CREATE_MAPPED_BIT;
		default:
			return 0;
	}
}

VkImageLayout _toVkImageLayout(PrRhiImageLayout layout) {
	switch (layout) {
		case PR_RHI_LAYOUT_UNDEFINED:           return VK_IMAGE_LAYOUT_UNDEFINED;
		case PR_RHI_LAYOUT_ATTACHMENT_OPTIMAL:  return VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL;
		case PR_RHI_LAYOUT_READ_ONLY_OPTIMAL:   return VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL;
		case PR_RHI_LAYOUT_TRANSFER_SRC_OPTIMAL: return VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
		case PR_RHI_LAYOUT_TRANSFER_DST_OPTIMAL: return VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
		case PR_RHI_LAYOUT_PRESENT_SRC:         return VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
		case PR_RHI_LAYOUT_GENERAL:             return VK_IMAGE_LAYOUT_GENERAL;
		default:                                return VK_IMAGE_LAYOUT_UNDEFINED;
	}
}

VkShaderStageFlags _toVkShaderStage(PrRhiShaderStage stage) {
	VkShaderStageFlags flags = 0;
	if (stage & PR_RHI_SHADER_STAGE_VERTEX)   flags |= VK_SHADER_STAGE_VERTEX_BIT;
	if (stage & PR_RHI_SHADER_STAGE_FRAGMENT) flags |= VK_SHADER_STAGE_FRAGMENT_BIT;
	if (stage & PR_RHI_SHADER_STAGE_COMPUTE)  flags |= VK_SHADER_STAGE_COMPUTE_BIT;
	return flags;
}

VkDescriptorType _toVkDescriptorType(PrRhiDescriptorType type) {
	switch (type) {
		case PR_RHI_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER: return VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
		case PR_RHI_DESCRIPTOR_TYPE_STORAGE_IMAGE:          return VK_DESCRIPTOR_TYPE_STORAGE_IMAGE;
		case PR_RHI_DESCRIPTOR_TYPE_UNIFORM_BUFFER:         return VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
		case PR_RHI_DESCRIPTOR_TYPE_STORAGE_BUFFER:         return VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
		default:                                            return VK_DESCRIPTOR_TYPE_MAX_ENUM;
	}
}

VkDescriptorBindingFlags _toVkBindingFlags(PrRhiDescriptorBindingFlag flags) {
	VkDescriptorBindingFlags vk = 0;
	if (flags & PR_RHI_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND)         vk |= VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT;
	if (flags & PR_RHI_DESCRIPTOR_BINDING_PARTIALLY_BOUND)           vk |= VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT;
	if (flags & PR_RHI_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT) vk |= VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT;
	return vk;
}

VkCompareOp _toVkCompareOp(PrRhiCompareOp op) {
	switch (op) {
		case PR_RHI_COMPARE_OP_NEVER:           return VK_COMPARE_OP_NEVER;
		case PR_RHI_COMPARE_OP_LESS:            return VK_COMPARE_OP_LESS;
		case PR_RHI_COMPARE_OP_EQUAL:           return VK_COMPARE_OP_EQUAL;
		case PR_RHI_COMPARE_OP_LESS_OR_EQUAL:   return VK_COMPARE_OP_LESS_OR_EQUAL;
		case PR_RHI_COMPARE_OP_GREATER:         return VK_COMPARE_OP_GREATER;
		case PR_RHI_COMPARE_OP_NOT_EQUAL:       return VK_COMPARE_OP_NOT_EQUAL;
		case PR_RHI_COMPARE_OP_GREATER_OR_EQUAL: return VK_COMPARE_OP_GREATER_OR_EQUAL;
		case PR_RHI_COMPARE_OP_ALWAYS:          return VK_COMPARE_OP_ALWAYS;
		default:                                return VK_COMPARE_OP_ALWAYS;
	}
}

VkPrimitiveTopology _toVkTopology(PrRhiPrimitiveTopology topo) {
	switch (topo) {
		case PR_RHI_TOPOLOGY_POINT_LIST:                  return VK_PRIMITIVE_TOPOLOGY_POINT_LIST;
		case PR_RHI_TOPOLOGY_LINE_LIST:                   return VK_PRIMITIVE_TOPOLOGY_LINE_LIST;
		case PR_RHI_TOPOLOGY_LINE_STRIP:                  return VK_PRIMITIVE_TOPOLOGY_LINE_STRIP;
		case PR_RHI_TOPOLOGY_TRIANGLE_LIST:               return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
		case PR_RHI_TOPOLOGY_TRIANGLE_STRIP:              return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP;
		case PR_RHI_TOPOLOGY_TRIANGLE_FAN:                return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN;
		case PR_RHI_TOPOLOGY_LINE_LIST_WITH_ADJACENCY:    return VK_PRIMITIVE_TOPOLOGY_LINE_LIST_WITH_ADJACENCY;
		case PR_RHI_TOPOLOGY_LINE_STRIP_WITH_ADJACENCY:   return VK_PRIMITIVE_TOPOLOGY_LINE_STRIP_WITH_ADJACENCY;
		case PR_RHI_TOPOLOGY_TRIANGLE_LIST_WITH_ADJACENCY: return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST_WITH_ADJACENCY;
		case PR_RHI_TOPOLOGY_TRIANGLE_STRIP_WITH_ADJACENCY: return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP_WITH_ADJACENCY;
		case PR_RHI_TOPOLOGY_PATCH_LIST:                  return VK_PRIMITIVE_TOPOLOGY_PATCH_LIST;
		default:                                          return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
	}
}

VkIndexType _toVkIndexType(PrRhiIndexType it) {
	switch (it) {
		case PR_RHI_INDEX_TYPE_UINT16: return VK_INDEX_TYPE_UINT16;
		case PR_RHI_INDEX_TYPE_UINT32: return VK_INDEX_TYPE_UINT32;
		default:                       return VK_INDEX_TYPE_UINT16;
	}
}

VkPresentModeKHR _toVkPresentMode(PrRhiPresentMode mode) {
	switch (mode) {
		case PR_RHI_PRESENT_MODE_IMMEDIATE: return VK_PRESENT_MODE_IMMEDIATE_KHR;
		case PR_RHI_PRESENT_MODE_FIFO:      return VK_PRESENT_MODE_FIFO_KHR;
		case PR_RHI_PRESENT_MODE_MAILBOX:   return VK_PRESENT_MODE_MAILBOX_KHR;
		default:                            return VK_PRESENT_MODE_FIFO_KHR;
	}
}

VkFilter _toVkFilter(PrRhiFilter f) {
	switch (f) {
		case PR_RHI_FILTER_NEAREST: return VK_FILTER_NEAREST;
		case PR_RHI_FILTER_LINEAR:  return VK_FILTER_LINEAR;
		default:                    return VK_FILTER_NEAREST;
	}
}

VkSamplerMipmapMode _toVkMipmapMode(PrRhiMipmapMode m) {
	switch (m) {
		case PR_RHI_MIPMAP_MODE_NEAREST: return VK_SAMPLER_MIPMAP_MODE_NEAREST;
		case PR_RHI_MIPMAP_MODE_LINEAR:  return VK_SAMPLER_MIPMAP_MODE_LINEAR;
		default:                         return VK_SAMPLER_MIPMAP_MODE_NEAREST;
	}
}

VkSamplerAddressMode _toVkAddressMode(PrRhiAddressMode m) {
	switch (m) {
		case PR_RHI_ADDRESS_MODE_REPEAT:          return VK_SAMPLER_ADDRESS_MODE_REPEAT;
		case PR_RHI_ADDRESS_MODE_CLAMP_TO_EDGE:   return VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
		case PR_RHI_ADDRESS_MODE_CLAMP_TO_BORDER: return VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER;
		default:                                  return VK_SAMPLER_ADDRESS_MODE_REPEAT;
	}
}

VkPipelineStageFlags2 _toVkPipelineStage2(PrRhiPipelineStage stage) {
	VkPipelineStageFlags2 flags = VK_PIPELINE_STAGE_2_NONE;
	if (stage & PR_RHI_PIPELINE_STAGE_TOP_OF_PIPE)             flags |= VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT;
	if (stage & PR_RHI_PIPELINE_STAGE_TRANSFER)                flags |= VK_PIPELINE_STAGE_2_TRANSFER_BIT;
	if (stage & PR_RHI_PIPELINE_STAGE_VERTEX_SHADER)           flags |= VK_PIPELINE_STAGE_2_VERTEX_SHADER_BIT;
	if (stage & PR_RHI_PIPELINE_STAGE_FRAGMENT_SHADER)         flags |= VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT;
	if (stage & PR_RHI_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS)    flags |= VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT;
	if (stage & PR_RHI_PIPELINE_STAGE_LATE_FRAGMENT_TESTS)     flags |= VK_PIPELINE_STAGE_2_LATE_FRAGMENT_TESTS_BIT;
	if (stage & PR_RHI_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT) flags |= VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
	if (stage & PR_RHI_PIPELINE_STAGE_COMPUTE_SHADER)          flags |= VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT;
	if (stage & PR_RHI_PIPELINE_STAGE_BOTTOM_OF_PIPE)          flags |= VK_PIPELINE_STAGE_2_BOTTOM_OF_PIPE_BIT;
	return flags;
}

VkAccessFlags2 _toVkAccess2(PrRhiAccess access) {
	VkAccessFlags2 flags = VK_ACCESS_2_NONE;
	if (access & PR_RHI_ACCESS_TRANSFER_READ)                    flags |= VK_ACCESS_2_TRANSFER_READ_BIT;
	if (access & PR_RHI_ACCESS_TRANSFER_WRITE)                   flags |= VK_ACCESS_2_TRANSFER_WRITE_BIT;
	if (access & PR_RHI_ACCESS_SHADER_READ)                      flags |= VK_ACCESS_2_SHADER_READ_BIT;
	if (access & PR_RHI_ACCESS_SHADER_WRITE)                     flags |= VK_ACCESS_2_SHADER_WRITE_BIT;
	if (access & PR_RHI_ACCESS_COLOR_ATTACHMENT_READ)            flags |= VK_ACCESS_2_COLOR_ATTACHMENT_READ_BIT;
	if (access & PR_RHI_ACCESS_COLOR_ATTACHMENT_WRITE)           flags |= VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT;
	if (access & PR_RHI_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ)    flags |= VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_READ_BIT;
	if (access & PR_RHI_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE)   flags |= VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
	if (access & PR_RHI_ACCESS_MEMORY_READ)                      flags |= VK_ACCESS_2_MEMORY_READ_BIT;
	if (access & PR_RHI_ACCESS_MEMORY_WRITE)                     flags |= VK_ACCESS_2_MEMORY_WRITE_BIT;
	return flags;
}

VkPhysicalDeviceType _toVkPhysicalDeviceType(PrRhiPhysicalDeviceType type) {
	switch (type) {
		case PR_RHI_PHYSICAL_DEVICE_TYPE_OTHER:          return VK_PHYSICAL_DEVICE_TYPE_OTHER;
		case PR_RHI_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU: return VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU;
		case PR_RHI_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU:   return VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU;
		case PR_RHI_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU:    return VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU;
		case PR_RHI_PHYSICAL_DEVICE_TYPE_CPU:            return VK_PHYSICAL_DEVICE_TYPE_CPU;
		default:                                         return VK_PHYSICAL_DEVICE_TYPE_OTHER;
	}
}

// ============================================================================
// Instance
// ============================================================================

PrRhiInstance *prRhiCreateInstanceVk(PrRhiInstanceDesc desc) {
	_checkVk(volkInitialize(), "volkInitialize");

	VkBool32 supported = VK_FALSE;
	_checkVk(vpGetInstanceProfileSupport(NULL, &_profile, &supported), "vpGetInstanceProfileSupport");
	if (!supported) { _abort("VP_PRISM_DESKTOP_2026 not supported at instance level"); }

	u32 sdl_ext_count = 0;
	const char *const *sdl_exts = SDL_Vulkan_GetInstanceExtensions(&sdl_ext_count);
	if (!sdl_exts || sdl_ext_count == 0) {
		_abort("SDL_Vulkan_GetInstanceExtensions failed");
	}

	VkApplicationInfo app_info  = {
		.sType              = VK_STRUCTURE_TYPE_APPLICATION_INFO,
		.pApplicationName   = desc.app_name ? desc.app_name : "Prism",
		.applicationVersion = desc.app_version,
		.apiVersion         = VP_PRISM_DESKTOP_2026_MIN_API_VERSION,
	};

	VkInstanceCreateInfo instance_info    = {
		.sType                   = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
		.pApplicationInfo        = &app_info,
		.enabledExtensionCount   = sdl_ext_count,
		.ppEnabledExtensionNames = sdl_exts,
	};

	VpInstanceCreateInfo vp_instance_info    = {
		.pCreateInfo             = &instance_info,
		.enabledFullProfileCount = 1,
		.pEnabledFullProfiles    = &_profile,
	};

	VkInstance vk_instance = VK_NULL_HANDLE;
	_checkVk(vpCreateInstance(&vp_instance_info, NULL, &vk_instance), "vpCreateInstance");

	volkLoadInstance(vk_instance);

	PrRhiInstance *inst = wpMemAllocatorAlloc(&_G_RHI_CONTEXT.allocator, sizeof(PrRhiInstance));
	if (!inst) { _abort("alloc failed for PrRhiInstance"); }
	inst->handle          = vk_instance;
	inst->debug_messenger = NULL;

	return inst;
}

void prRhiDestroyInstanceVk(PrRhiInstance *inst) {
	if (!inst) { return; }
	vkDestroyInstance((VkInstance)inst->handle, NULL);
	wpMemAllocatorFree(&_G_RHI_CONTEXT.allocator, (void**)&inst, sizeof(PrRhiInstance));
}

// ============================================================================
// Physical device enumeration
// ============================================================================

PrRhiPhysicalDeviceArray prRhiGetPhysicalDevicesVk(PrRhiInstance *inst) {
	VkInstance vk_inst = (VkInstance)inst->handle;

	u32 count = 0;
	_checkVk(vkEnumeratePhysicalDevices(vk_inst, &count, NULL), "vkEnumeratePhysicalDevices");
	if (count == 0) { _abort("no physical devices"); }

	VkPhysicalDeviceArray vk_devices = wpArrayAllocCapacity(VkPhysicalDevice, &_G_RHI_CONTEXT.allocator,
	                                                        count, WP_ARRAY_INIT_FILLED);
	if (!vk_devices) { _abort("alloc failed for physical device array"); }

	_checkVk(vkEnumeratePhysicalDevices(vk_inst, &count, vk_devices), "vkEnumeratePhysicalDevices");
	wpArraySetCount(vk_devices, count);

	PrRhiPhysicalDeviceArray pdevs = wpArrayAllocCapacity(PrRhiPhysicalDevice *, &_G_RHI_CONTEXT.allocator,
	                                                      count, WP_ARRAY_INIT_NONE);
	if (!pdevs) { _abort("alloc failed for PrRhiPhysicalDevice array"); }

	for (u32 i = 0; i < count; ++i) {
		VkBool32 supported = VK_TRUE;
		VkResult result    = vpGetPhysicalDeviceProfileSupport(vk_inst, vk_devices[i], &_profile, &supported);
		if (result != VK_SUCCESS || !supported) { continue; }

		PrRhiPhysicalDevice *pdev = (PrRhiPhysicalDevice *)wpMemAllocatorAlloc(&_G_RHI_CONTEXT.allocator, sizeof(PrRhiPhysicalDevice));
		if (!pdev) { _abort("alloc failed for PrRhiPhysicalDevice"); }
		pdev->handle   = vk_devices[i];
		pdev->instance = inst;

		VkPhysicalDeviceDriverProperties driver = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES };
		VkPhysicalDeviceProperties2 props       = {
			.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2,
			.pNext = &driver,
		};
		vkGetPhysicalDeviceProperties2(vk_devices[i], &props);
		memcpy(pdev->device_name, props.properties.deviceName, sizeof(pdev->device_name));
		memcpy(pdev->driver_info, driver.driverInfo, sizeof(pdev->driver_info));

		wpArrayAppendCapped(PrRhiPhysicalDevice *, pdevs, &pdev);
	}

	return pdevs;
}

void prRhiGetPhysicalDeviceNameVk(PrRhiPhysicalDevice *pdev, WpStr8 *out) {
	u64 name_len  = strlen((const char *)pdev->device_name);
	u64 count     = name_len <= out->capacity ? name_len : out->capacity;
	out->buf      = pdev->device_name;
	out->size     = count;
	out->capacity = count;
}

void prRhiGetPhysicalDeviceDriverInfoVk(PrRhiPhysicalDevice *pdev, WpStr8 *out) {
	u64 name_len  = strlen((const char *)pdev->driver_info);
	u64 count     = name_len <= out->capacity ? name_len : out->capacity;
	out->buf      = pdev->driver_info;
	out->size     = count;
	out->capacity = count;
}

PrRhiPhysicalDeviceProperties prRhiGetPhysicalDevicePropertiesVk(PrRhiPhysicalDevice *pdev) {
	VkPhysicalDeviceProperties2 props = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2 };
	vkGetPhysicalDeviceProperties2((VkPhysicalDevice)pdev->handle, &props);
	return (PrRhiPhysicalDeviceProperties) {
		.api_version = props.properties.apiVersion,
		.device_type = (PrRhiPhysicalDeviceType)props.properties.deviceType,
	};
}

// ============================================================================
// Surface
// ============================================================================

PrRhiSurface *prRhiCreateSurfaceFromWindowVk(PrRhiInstance *inst, SDL_Window *window) {
	VkInstance vk_inst = inst->handle;

	VkSurfaceKHR vk_surface = VK_NULL_HANDLE;
	if (!SDL_Vulkan_CreateSurface(window, vk_inst, NULL, &vk_surface)) {
		_abort("SDL_Vulkan_CreateSurface failed");
	}

	PrRhiSurface *surface = wpMemAllocatorAlloc(&_G_RHI_CONTEXT.allocator, sizeof(PrRhiSurface));
	if (!surface) { _abort("alloc failed for PrRhiSurface"); }
	surface->handle = vk_surface;
	return surface;
}

void prRhiDestroySurfaceVk(PrRhiInstance *inst, PrRhiSurface *surface) {
	if (!surface) { return; }
	vkDestroySurfaceKHR(inst->handle, surface->handle, NULL);
	wpMemAllocatorFree(&_G_RHI_CONTEXT.allocator, (void**)&surface, sizeof(PrRhiSurface));
}

PrRhiSurfaceCapabilities prRhiGetSurfaceCapabilitiesVk(PrRhiPhysicalDevice *pdev, PrRhiSurface *surface) {
	VkSurfaceCapabilitiesKHR caps = {};
	_checkVk(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(pdev->handle, surface->handle, &caps),
	         "vkGetPhysicalDeviceSurfaceCapabilitiesKHR");

	PrRhiSurfaceCapabilities out = {
		.min_image_count = caps.minImageCount,
		.max_image_count = caps.maxImageCount,
		.current_width   = caps.currentExtent.width,
		.current_height  = caps.currentExtent.height,
		.min_width       = caps.minImageExtent.width,
		.min_height      = caps.minImageExtent.height,
		.max_width       = caps.maxImageExtent.width,
		.max_height      = caps.maxImageExtent.height,
	};
	return out;
}

// ============================================================================
// Device
// ============================================================================

PrRhiDevice *prRhiCreateDeviceVk(PrRhiPhysicalDevice *pdev, PrRhiSurface *surface,
                                 PrRhiDeviceDesc desc) {
	VkInstance vk_inst       = pdev->instance->handle;
	VkPhysicalDevice vk_pdev = pdev->handle;
	VkSurfaceKHR vk_surface  = surface->handle;

	// Select queue family
	u32 queue_family_count = 0;
	vkGetPhysicalDeviceQueueFamilyProperties2(vk_pdev, &queue_family_count, NULL);
	if (queue_family_count == 0) { _abort("no queue families"); }

	VkQueueFamilyProperties2Array qf_props =
		wpArrayAllocCapacity(VkQueueFamilyProperties2, &_G_RHI_CONTEXT.allocator, queue_family_count,
		                     WP_ARRAY_INIT_FILLED);
	if (!qf_props) { _abort("alloc failed for queue family props"); }

	for (u32 i = 0; i < queue_family_count; ++i) {
		qf_props[i].sType = VK_STRUCTURE_TYPE_QUEUE_FAMILY_PROPERTIES_2;
	}
	vkGetPhysicalDeviceQueueFamilyProperties2(vk_pdev, &queue_family_count, qf_props);
	wpArraySetCount(qf_props, queue_family_count);

	u32 family_index = UINT32_MAX;
	for (u32 i = 0; i < queue_family_count; ++i) {
		if (qf_props[i].queueFamilyProperties.queueFlags &
		    (VK_QUEUE_GRAPHICS_BIT | VK_QUEUE_COMPUTE_BIT | VK_QUEUE_TRANSFER_BIT)) {
			family_index = i;
			break;
		}
	}
	if (family_index == UINT32_MAX) { _abort("no suitable queue family"); }

	// Check presentation support
	if (!SDL_Vulkan_GetPresentationSupport(pdev->instance->handle, pdev->handle, family_index)) {
		_abort("No presentation support");
	}

	wpArrayDealloc(VkQueueFamilyProperties2, &_G_RHI_CONTEXT.allocator, &qf_props);

	// Create device
	f32 queue_priority = 1.0f;
	VkDeviceQueueCreateInfo queue_create_info = {
		.sType            = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
		.queueCount       = 1,
		.queueFamilyIndex = family_index,
		.pQueuePriorities = &queue_priority,
	};

	VkDeviceCreateInfo device_info = {
		.sType                = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,
		.queueCreateInfoCount = 1,
		.pQueueCreateInfos    = &queue_create_info,
	};

	VpDeviceCreateInfo vp_device_info = {
		.pCreateInfo             = &device_info,
		.enabledFullProfileCount = 1,
		.pEnabledFullProfiles    = &_profile,
	};

	VkDevice vk_device = VK_NULL_HANDLE;
	_checkVk(vpCreateDevice(vk_pdev, &vp_device_info, NULL, &vk_device), "vpCreateDevice");

	volkLoadDevice(vk_device);

	VkQueue vk_queue = VK_NULL_HANDLE;
	vkGetDeviceQueue(vk_device, family_index, 0, &vk_queue);

	// Create VMA allocator
	VmaVulkanFunctions vk_functions = {
		.vkGetInstanceProcAddr = vkGetInstanceProcAddr,
		.vkGetDeviceProcAddr   = vkGetDeviceProcAddr,
		.vkCreateImage         = vkCreateImage,
	};

	VmaAllocatorCreateInfo vma_info = {
		.flags            = VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT |
		                    VMA_ALLOCATOR_CREATE_EXT_MEMORY_PRIORITY_BIT |
		                    VMA_ALLOCATOR_CREATE_KHR_MAINTENANCE5_BIT,
		.instance         = vk_inst,
		.physicalDevice   = vk_pdev,
		.device           = vk_device,
		.pVulkanFunctions = &vk_functions,
	};

	VmaAllocator vma_allocator = VK_NULL_HANDLE;
	_checkVk(vmaCreateAllocator(&vma_info, &vma_allocator), "vmaCreateAllocator");

	PrRhiDevice *device = wpMemAllocatorAlloc(&_G_RHI_CONTEXT.allocator, sizeof(PrRhiDevice));
	if (!device) { _abort("alloc failed for PrRhiDevice"); }
	device->handle             = vk_device;
	device->queue              = vk_queue;
	device->queue_family_index = family_index;
	device->present_mode       = _toVkPresentMode(desc.present_mode);
	device->physical_device    = vk_pdev;
	device->allocator          = vma_allocator;

	return device;
}

void prRhiDestroyDeviceVk(PrRhiDevice *device) {
	if (!device) { return; }
	vkDeviceWaitIdle(device->handle);
	vmaDestroyAllocator(device->allocator);
	vkDestroyDevice(device->handle, NULL);
	wpMemAllocatorFree(&_G_RHI_CONTEXT.allocator, (void**)&device, sizeof(PrRhiDevice));
}

void prRhiDeviceWaitIdleVk(PrRhiDevice *device) {
	_checkVk(vkDeviceWaitIdle((VkDevice)device->handle), "vkDeviceWaitIdle");
}

u32 prRhiGetQueueFamilyIndexVk(PrRhiDevice *device) {
	return device->queue_family_index;
}

// ============================================================================
// Swapchain
// ============================================================================

static PrRhiTexture *_createSwapchainTexture(PrRhiDevice *device, VkImage vk_image,
                                             VkFormat vk_format, u32 width, u32 height,
                                             VkImageView vk_view) {
	PrRhiTexture *tex = wpMemAllocatorAlloc(&_G_RHI_CONTEXT.allocator, sizeof(PrRhiTexture));
	if (!tex) { _abort("alloc failed for swapchain texture"); }
	tex->image      = vk_image;
	tex->view       = vk_view;
	tex->allocation = NULL;
	tex->width      = width;
	tex->height     = height;
	tex->format     = vk_format;
	return tex;
}

static VkFormat _pickDepthFormat(VkPhysicalDevice vk_pdev) {
	const VkFormatArray candidates = wpArray(VkFormat, VK_FORMAT_D24_UNORM_S8_UINT, VK_FORMAT_D32_SFLOAT_S8_UINT);
	for (u32 i = 0; i < wpArrayCount(candidates); ++i) {
		VkFormatProperties2 props = { .sType = VK_STRUCTURE_TYPE_FORMAT_PROPERTIES_2 };
		vkGetPhysicalDeviceFormatProperties2(vk_pdev, candidates[i], &props);
		if (props.formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) {
			return candidates[i];
		}
	}
	return VK_FORMAT_UNDEFINED;
}

PrRhiSwapchain *prRhiCreateSwapchainVk(PrRhiDevice *device, PrRhiSwapchainDesc desc) {
	VkDevice vk_device       = device->handle;
	VkPhysicalDevice vk_pdev = device->physical_device;
	VkSurfaceKHR vk_surface  = desc.surface->handle;

	VkSurfaceCapabilitiesKHR caps = {};
	_checkVk(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(vk_pdev, vk_surface, &caps),
	         "vkGetPhysicalDeviceSurfaceCapabilitiesKHR");

	VkExtent2D extent = caps.currentExtent;
	if (extent.width == 0xffffffff) {
		extent.width  = desc.width;
		extent.height = desc.height;
	}

	VkSwapchainCreateInfoKHR swapchain_info = {
		.sType                    = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR,
		.surface                  = vk_surface,
		.minImageCount            = caps.minImageCount,
		.imageFormat              = _default_image_format,
		.imageColorSpace          = _default_colorspace,
		.imageExtent              = extent,
		.imageArrayLayers         = 1,
		.imageUsage               = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT,
		.imageSharingMode         = VK_SHARING_MODE_EXCLUSIVE,
		.queueFamilyIndexCount    = 1,
		.pQueueFamilyIndices      = &device->queue_family_index,
		.preTransform             = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR,
		.compositeAlpha           = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR,
		.presentMode              = device->present_mode,
	};

	VkSwapchainKHR vk_swapchain = VK_NULL_HANDLE;
	_checkVk(vkCreateSwapchainKHR(vk_device, &swapchain_info, NULL, &vk_swapchain),
	         "vkCreateSwapchainKHR");

	u32 image_count = 0;
	_checkVk(vkGetSwapchainImagesKHR(vk_device, vk_swapchain, &image_count, NULL),
	         "vkGetSwapchainImagesKHR");

	VkImageArray vk_images = wpArrayAllocCapacity(VkImage, &_G_RHI_CONTEXT.allocator, image_count, WP_ARRAY_INIT_FILLED);
	if (!vk_images) { _abort("alloc failed for swapchain VkImage array"); }
	_checkVk(vkGetSwapchainImagesKHR(vk_device, vk_swapchain, &image_count, vk_images),
	         "vkGetSwapchainImagesKHR");
	wpArraySetCount(vk_images, image_count);

	PrRhiTextureArray images = wpArrayAllocCapacity(PrRhiTexture *, &_G_RHI_CONTEXT.allocator, image_count, WP_ARRAY_INIT_NONE);
	if (!images) { _abort("alloc failed for swapchain image array"); }

	for (u32 i = 0; i < image_count; ++i) {
		VkImageViewCreateInfo view_info       = {
			.sType                       = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
			.image                       = vk_images[i],
			.viewType                    = VK_IMAGE_VIEW_TYPE_2D,
			.format                      = _default_image_format,
			.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
			.subresourceRange.levelCount = 1,
			.subresourceRange.layerCount = 1,
		};

		VkImageView vk_view = VK_NULL_HANDLE;
		_checkVk(vkCreateImageView(vk_device, &view_info, NULL, &vk_view), "vkCreateImageView");

		PrRhiTexture *tex = _createSwapchainTexture(device, vk_images[i], _default_image_format,
		                                            extent.width, extent.height, vk_view);
		wpArrayAppendCapped(PrRhiTexture *, images, &tex);
	}

	wpArrayDealloc(VkImage, &_G_RHI_CONTEXT.allocator, &vk_images);

	PrRhiSwapchain *swapchain = wpMemAllocatorAlloc(&_G_RHI_CONTEXT.allocator, sizeof(PrRhiSwapchain));
	if (!swapchain) { _abort("alloc failed for PrRhiSwapchain"); }
	swapchain->device              = device;
	swapchain->handle              = vk_swapchain;
	swapchain->surface             = desc.surface;
	swapchain->image_count         = image_count;
	swapchain->images              = images;
	swapchain->depth               = NULL;
	swapchain->format              = _default_image_format;
	swapchain->depth_format        = VK_FORMAT_UNDEFINED;
	swapchain->width               = extent.width;
	swapchain->height              = extent.height;
	swapchain->current_image_index = 0;

	// Create depth texture if requested
	VkFormat depth_pick_fmt = VK_FORMAT_UNDEFINED;
	if (desc.has_depth) {
		if (desc.depth_format != PR_RHI_FORMAT_UNDEFINED) {
			depth_pick_fmt = _toVkFormat(desc.depth_format);
		} else {
			depth_pick_fmt = _pickDepthFormat(vk_pdev);
		}
		if (depth_pick_fmt == VK_FORMAT_UNDEFINED) { _abort("no suitable depth format"); }

		VkImageCreateInfo depth_img_info     = {
			.sType                 = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
			.imageType             = VK_IMAGE_TYPE_2D,
			.format                = depth_pick_fmt,
			.extent.width          = extent.width,
			.extent.height         = extent.height,
			.extent.depth          = 1,
			.mipLevels             = 1,
			.arrayLayers           = 1,
			.samples               = VK_SAMPLE_COUNT_1_BIT,
			.tiling                = VK_IMAGE_TILING_OPTIMAL,
			.usage                 = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT,
			.sharingMode           = VK_SHARING_MODE_EXCLUSIVE,
			.queueFamilyIndexCount = 1,
			.pQueueFamilyIndices   = &device->queue_family_index,
			.initialLayout         = VK_IMAGE_LAYOUT_UNDEFINED,
		};

		VmaAllocationCreateInfo depth_alloc_info = {
			.flags = VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT,
			.usage = VMA_MEMORY_USAGE_AUTO,
		};

		VkImage depth_image            = VK_NULL_HANDLE;
		VmaAllocation depth_allocation = VK_NULL_HANDLE;
		_checkVk(vmaCreateImage((VmaAllocator)device->allocator, &depth_img_info, &depth_alloc_info,
		                        &depth_image, &depth_allocation, NULL),
		         "vmaCreateImage (depth)");

		VkImageViewCreateInfo depth_view_info       = {
			.sType                       = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
			.image                       = depth_image,
			.viewType                    = VK_IMAGE_VIEW_TYPE_2D,
			.format                      = depth_pick_fmt,
			.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT,
			.subresourceRange.levelCount = 1,
			.subresourceRange.layerCount = 1,
		};

		VkImageView depth_view = VK_NULL_HANDLE;
		_checkVk(vkCreateImageView(vk_device, &depth_view_info, NULL, &depth_view), "vkCreateImageView (depth)");

		PrRhiTexture *depth_tex = wpMemAllocatorAlloc(&_G_RHI_CONTEXT.allocator, sizeof(PrRhiTexture));
		if (!depth_tex) { _abort("alloc failed for depth texture"); }
		depth_tex->image      = depth_image;
		depth_tex->view       = depth_view;
		depth_tex->allocation = depth_allocation;
		depth_tex->width      = extent.width;
		depth_tex->height     = extent.height;
		depth_tex->format     = depth_pick_fmt;

		swapchain->depth        = depth_tex;
		swapchain->depth_format = depth_pick_fmt;
	}

	return swapchain;
}

void prRhiDestroySwapchainVk(PrRhiDevice *device, PrRhiSwapchain *swapchain) {
	if (!swapchain) { return; }

	VkDevice vk_device = device->handle;

	// Destroy per-image views and PrRhiTexture structs
	for (u32 i = 0; i < swapchain->image_count; ++i) {
		PrRhiTexture *tex = swapchain->images[i];
		vkDestroyImageView(vk_device, tex->view, NULL);
		// VkImage is owned by swapchain — skip vmaDestroyImage
		wpMemAllocatorFree(&_G_RHI_CONTEXT.allocator, (void**)&tex, sizeof(PrRhiTexture));
	}
	wpArrayDealloc(PrRhiTexture *, &_G_RHI_CONTEXT.allocator, &swapchain->images);

	// Destroy depth
	if (swapchain->depth) {
		PrRhiTexture *depth = swapchain->depth;
		vkDestroyImageView(vk_device, depth->view, NULL);
		vmaDestroyImage(device->allocator, depth->image, depth->allocation);
		wpMemAllocatorFree(&_G_RHI_CONTEXT.allocator, (void**)&depth, sizeof(PrRhiTexture));
	}

	vkDestroySwapchainKHR(vk_device, swapchain->handle, NULL);
	wpMemAllocatorFree(&_G_RHI_CONTEXT.allocator, (void**)&swapchain, sizeof(PrRhiSwapchain));
}

u32 prRhiGetSwapchainImageCountVk(PrRhiSwapchain *swapchain) {
	return swapchain->image_count;
}

PrRhiSwapchainResult prRhiAcquireNextImageVk(PrRhiDevice *device, PrRhiSwapchain *swapchain,
                                             PrRhiSemaphore *signal_semaphore, u32 *out_image_index) {
	VkSemaphore vk_semaphore = signal_semaphore ? signal_semaphore->handle : VK_NULL_HANDLE;
	VkResult res = vkAcquireNextImageKHR(device->handle, swapchain->handle, UINT64_MAX, vk_semaphore,
	                                     VK_NULL_HANDLE, &swapchain->current_image_index);
	if (res == VK_ERROR_OUT_OF_DATE_KHR || res == VK_SUBOPTIMAL_KHR) {
		return PR_RHI_SWAPCHAIN_OUT_OF_DATE;
	}
	_checkVk(res, "vkAcquireNextImageKHR");
	if (out_image_index) { *out_image_index = swapchain->current_image_index; }
	return PR_RHI_SWAPCHAIN_SUCCESS;
}

PrRhiSwapchainResult prRhiPresentVk(PrRhiDevice *device, PrRhiSwapchain *swapchain,
                                    PrRhiSemaphore *wait_semaphore) {
	VkSemaphore vk_semaphore = wait_semaphore ? wait_semaphore->handle : VK_NULL_HANDLE;

	VkPresentInfoKHR present_info = {
		.sType              = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR,
		.waitSemaphoreCount = wait_semaphore ? 1 : 0,
		.pWaitSemaphores    = &vk_semaphore,
		.swapchainCount     = 1,
		.pSwapchains        = &swapchain->handle,
		.pImageIndices      = &swapchain->current_image_index,
	};

	VkResult res = vkQueuePresentKHR(device->queue, &present_info);
	if (res == VK_ERROR_OUT_OF_DATE_KHR || res == VK_SUBOPTIMAL_KHR) {
		return PR_RHI_SWAPCHAIN_OUT_OF_DATE;
	}
	_checkVk(res, "vkQueuePresentKHR");
	return PR_RHI_SWAPCHAIN_SUCCESS;
}

void prRhiRecreateSwapchainVk(PrRhiDevice *device, PrRhiSwapchain **swapchain,
                              u32 width, u32 height) {
	PrRhiSwapchain *old = *swapchain;
	if (!old) { return; }

	VkDevice vk_device       = device->handle;
	VkPhysicalDevice vk_pdev = device->physical_device;
	VkSurfaceKHR vk_surface  = ((PrRhiSurface *)old->surface)->handle;

	VkSurfaceCapabilitiesKHR caps = {};
	_checkVk(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(vk_pdev, vk_surface, &caps),
	         "vkGetPhysicalDeviceSurfaceCapabilitiesKHR");

	VkExtent2D extent = caps.currentExtent;
	if (extent.width == 0xffffffff) {
		extent.width  = width;
		extent.height = height;
	}

	VkSwapchainCreateInfoKHR swapchain_info = {
		.sType            = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR,
		.oldSwapchain     = old->handle,
		.surface          = vk_surface,
		.minImageCount    = old->image_count,
		.imageFormat      = (VkFormat)old->format,
		.imageColorSpace  = _default_colorspace,
		.imageExtent      = extent,
		.imageArrayLayers = 1,
		.imageUsage       = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT,
		.preTransform     = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR,
		.compositeAlpha   = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR,
		.presentMode      = (VkPresentModeKHR)device->present_mode,
	};

	VkSwapchainKHR vk_new_swapchain = VK_NULL_HANDLE;
	_checkVk(vkCreateSwapchainKHR(vk_device, &swapchain_info, NULL, &vk_new_swapchain),
	         "vkCreateSwapchainKHR (recreate)");

	// Destroy old image views and PrRhiTexture structs (but not VkImages — owned by old swapchain)
	for (u32 i = 0; i < old->image_count; ++i) {
		PrRhiTexture *tex = old->images[i];
		vkDestroyImageView(vk_device, tex->view, NULL);
		wpMemAllocatorFree(&_G_RHI_CONTEXT.allocator, (void**)&tex, sizeof(PrRhiTexture));
	}
	wpArrayDealloc(PrRhiTexture *, &_G_RHI_CONTEXT.allocator, &old->images);

	// Destroy old depth
	if (old->depth) {
		PrRhiTexture *depth = old->depth;
		vkDestroyImageView(vk_device, depth->view, NULL);
		vmaDestroyImage(device->allocator, depth->image, depth->allocation);
		wpMemAllocatorFree(&_G_RHI_CONTEXT.allocator, (void**)&depth, sizeof(PrRhiTexture));
	}

	// Destroy old swapchain
	vkDestroySwapchainKHR(vk_device, old->handle, NULL);

	// Get new swapchain images
	u32 new_image_count = 0;
	_checkVk(vkGetSwapchainImagesKHR(vk_device, vk_new_swapchain, &new_image_count, NULL),
	         "vkGetSwapchainImagesKHR (recreate)");

	VkImageArray vk_images = wpArrayAllocCapacity(VkImage, &_G_RHI_CONTEXT.allocator, new_image_count, WP_ARRAY_INIT_FILLED);
	if (!vk_images) { _abort("alloc failed for swapchain VkImage array (recreate)"); }
	_checkVk(vkGetSwapchainImagesKHR(vk_device, vk_new_swapchain, &new_image_count, vk_images),
	         "vkGetSwapchainImagesKHR (recreate)");
	wpArraySetCount(vk_images, new_image_count);

	PrRhiTextureArray new_images = wpArrayAllocCapacity(PrRhiTexture *, &_G_RHI_CONTEXT.allocator, new_image_count, WP_ARRAY_INIT_NONE);
	if (!new_images) { _abort("alloc failed for swapchain image array (recreate)"); }

	for (u32 i = 0; i < new_image_count; ++i) {
		VkImageViewCreateInfo view_info = {
			.sType                       = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
			.image                       = vk_images[i],
			.viewType                    = VK_IMAGE_VIEW_TYPE_2D,
			.format                      = (VkFormat)old->format,
			.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
			.subresourceRange.levelCount = 1,
			.subresourceRange.layerCount = 1,
		};

		VkImageView vk_view = VK_NULL_HANDLE;
		_checkVk(vkCreateImageView(vk_device, &view_info, NULL, &vk_view), "vkCreateImageView (recreate)");

		PrRhiTexture *tex = _createSwapchainTexture(device, vk_images[i], old->format, extent.width,
		                                            extent.height, vk_view);
		wpArrayAppendCapped(PrRhiTexture *, new_images, &tex);
	}

	wpArrayDealloc(VkImage, &_G_RHI_CONTEXT.allocator, &vk_images);

	// Recreate depth
	PrRhiTexture *new_depth = NULL;
	{
		VkFormat depth_fmt = old->depth_format ? old->depth_format : _pickDepthFormat(vk_pdev);
		if (depth_fmt == VK_FORMAT_UNDEFINED) { _abort("no suitable depth format (recreate)"); }

		VkImageCreateInfo depth_img_info = {
			.sType         = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
			.imageType     = VK_IMAGE_TYPE_2D,
			.format        = depth_fmt,
			.extent.width  = extent.width,
			.extent.height = extent.height,
			.extent.depth  = 1,
			.mipLevels     = 1,
			.arrayLayers   = 1,
			.samples       = VK_SAMPLE_COUNT_1_BIT,
			.tiling        = VK_IMAGE_TILING_OPTIMAL,
			.usage         = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT,
			.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,
		};

		VmaAllocationCreateInfo depth_alloc_info = {
			.flags = VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT,
			.usage = VMA_MEMORY_USAGE_AUTO,
		};

		VkImage depth_image = VK_NULL_HANDLE;
		VmaAllocation depth_allocation = VK_NULL_HANDLE;
		_checkVk(vmaCreateImage(device->allocator, &depth_img_info, &depth_alloc_info, &depth_image,
		                        &depth_allocation, NULL),
		         "vmaCreateImage (depth recreate)");

		VkImageViewCreateInfo depth_view_info = {
			.sType                       = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
			.image                       = depth_image,
			.viewType                    = VK_IMAGE_VIEW_TYPE_2D,
			.format                      = depth_fmt,
			.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT,
			.subresourceRange.levelCount = 1,
			.subresourceRange.layerCount = 1,
		};

		VkImageView depth_view = VK_NULL_HANDLE;
		_checkVk(vkCreateImageView(vk_device, &depth_view_info, NULL, &depth_view), "vkCreateImageView (depth recreate)");

		new_depth = wpMemAllocatorAlloc(&_G_RHI_CONTEXT.allocator, sizeof(PrRhiTexture));
		if (!new_depth) { _abort("alloc failed for depth texture (recreate)"); }
		new_depth->image      = depth_image;
		new_depth->view       = depth_view;
		new_depth->allocation = depth_allocation;
		new_depth->width      = extent.width;
		new_depth->height     = extent.height;
		new_depth->format     = depth_fmt;
		old->depth_format     = depth_fmt;
	}

	// Update old swapchain in-place
	old->handle              = vk_new_swapchain;
	old->image_count         = new_image_count;
	old->images              = new_images;
	old->depth               = new_depth;
	old->width               = extent.width;
	old->height              = extent.height;
	old->current_image_index = 0;

	*swapchain = old;
}

PrRhiTexture *prRhiGetSwapchainTextureVk(PrRhiSwapchain *swapchain, u32 image_index) {
	if (image_index >= swapchain->image_count) { return NULL; }
	return swapchain->images[image_index];
}

PrRhiTexture *prRhiGetSwapchainDepthTextureVk(PrRhiSwapchain *swapchain) {
	return swapchain->depth;
}

PrRhiFormat prRhiGetSwapchainFormatVk(PrRhiSwapchain *swapchain) {
	return _fromVkFormat((VkFormat)swapchain->format);
}

// ============================================================================
// Buffers
// ============================================================================

PrRhiBuffer *prRhiCreateBufferVk(PrRhiDevice *device, PrRhiBufferDesc desc) {
	VkBufferCreateInfo buf_info = {
		.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
		.size  = desc.size,
		.usage = _toVkBufferUsage(desc.usage),
	};

	// For device address, we need VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT
	VkBufferUsageFlags2CreateInfo usage_flags2 = {};
	if (desc.usage & PR_RHI_BUFFER_USAGE_SHADER_DEVICE_ADDRESS) {
		usage_flags2.sType = VK_STRUCTURE_TYPE_BUFFER_USAGE_FLAGS_2_CREATE_INFO;
		usage_flags2.usage = VK_BUFFER_USAGE_2_SHADER_DEVICE_ADDRESS_BIT;
		buf_info.pNext     = &usage_flags2;
	}

	VmaAllocationCreateInfo alloc_info = {
		.flags = _toVmaFlags(desc.memory),
		.usage = VMA_MEMORY_USAGE_AUTO,
	};

	VkBuffer vk_buffer               = VK_NULL_HANDLE;
	VmaAllocation vma_alloc          = VK_NULL_HANDLE;
	VmaAllocationInfo vma_alloc_info = {};
	_checkVk(vmaCreateBuffer(device->allocator, &buf_info, &alloc_info, &vk_buffer, &vma_alloc, &vma_alloc_info),
	         "vmaCreateBuffer");

	PrRhiBuffer *buffer = wpMemAllocatorAlloc(&_G_RHI_CONTEXT.allocator, sizeof(PrRhiBuffer));
	if (!buffer) { _abort("alloc failed for PrRhiBuffer"); }
	buffer->handle         = vk_buffer;
	buffer->allocation     = vma_alloc;
	buffer->size           = vma_alloc_info.size;
	buffer->mapped_data    = vma_alloc_info.pMappedData;
	buffer->device_address = 0;

	// Query device address if needed
	if (desc.usage & PR_RHI_BUFFER_USAGE_SHADER_DEVICE_ADDRESS) {
		VkBufferDeviceAddressInfo addr_info = {
			.sType  = VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO,
			.buffer = vk_buffer,
		};
		buffer->device_address = vkGetBufferDeviceAddress(device->handle, &addr_info);
	}

	return buffer;
}

void prRhiDestroyBufferVk(PrRhiDevice *device, PrRhiBuffer *buffer) {
	if (!buffer) { return; }
	vmaDestroyBuffer(device->allocator, buffer->handle, buffer->allocation);
	wpMemAllocatorFree(&_G_RHI_CONTEXT.allocator, (void**)&buffer, sizeof(PrRhiBuffer));
}

void *prRhiBufferMapVk(PrRhiDevice *device, PrRhiBuffer *buffer) {
	(void)device;
	return buffer->mapped_data;
}

void prRhiBufferUnmapVk(PrRhiDevice *device, PrRhiBuffer *buffer) {
	(void)device;
	(void)buffer;
	// VMA mapped buffers don't need explicit unmap with VMA_ALLOCATION_CREATE_MAPPED_BIT
}

PrRhiDeviceAddress prRhiGetBufferDeviceAddressVk(PrRhiDevice *device, PrRhiBuffer *buffer) {
	(void)device;
	return buffer->device_address;
}

// ============================================================================
// Textures
// ============================================================================

PrRhiTexture *prRhiCreateTextureVk(PrRhiDevice *device, PrRhiTextureDesc desc) {
	VkImageCreateInfo img_info           = {
		.sType         = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
		.imageType     = VK_IMAGE_TYPE_2D,
		.format        = _toVkFormat(desc.format),
		.extent.width  = desc.width,
		.extent.height = desc.height,
		.extent.depth  = 1,
		.mipLevels     = desc.mip_levels,
		.arrayLayers   = 1,
		.samples       = VK_SAMPLE_COUNT_1_BIT,
		.tiling        = VK_IMAGE_TILING_OPTIMAL,
		.usage         = _toVkTextureUsage(desc.usage),
		.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,
	};

	VmaAllocationCreateInfo alloc_info = { .usage = VMA_MEMORY_USAGE_AUTO };

	VkImage vk_image = VK_NULL_HANDLE;
	VmaAllocation vma_alloc = VK_NULL_HANDLE;
	_checkVk(vmaCreateImage((VmaAllocator)device->allocator, &img_info, &alloc_info,
	                          &vk_image, &vma_alloc, NULL),
	         "vmaCreateImage");

	VkImageAspectFlags aspect_mask = VK_IMAGE_ASPECT_COLOR_BIT;
	if (desc.usage & PR_RHI_TEXTURE_USAGE_DEPTH_ATTACHMENT) {
		aspect_mask = VK_IMAGE_ASPECT_DEPTH_BIT;
	}

	VkImageViewCreateInfo view_info       = {
		.sType                       = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
		.image                       = vk_image,
		.viewType                    = VK_IMAGE_VIEW_TYPE_2D,
		.format                      = _toVkFormat(desc.format),
		.subresourceRange.aspectMask = aspect_mask,
		.subresourceRange.levelCount = desc.mip_levels,
		.subresourceRange.layerCount = 1,
	};

	VkImageView vk_view = VK_NULL_HANDLE;
	_checkVk(vkCreateImageView((VkDevice)device->handle, &view_info, NULL, &vk_view),
	         "vkCreateImageView");

	PrRhiTexture *texture = wpMemAllocatorAlloc(&_G_RHI_CONTEXT.allocator, sizeof(PrRhiTexture));
	if (!texture) { _abort("alloc failed for PrRhiTexture"); }
	texture->image      = vk_image;
	texture->view       = vk_view;
	texture->allocation = vma_alloc;
	texture->width      = desc.width;
	texture->height     = desc.height;
	texture->format     = _toVkFormat(desc.format);

	return texture;
}

PrRhiTexture *prRhiCreateTextureFromKtxVk(PrRhiDevice *device, const char *path,
                                          PrRhiCommandPool *pool, PrRhiCommandBuffer *cb) {
	VkDevice vk_device = device->handle;

	ktxTexture *ktx = NULL;
	ktxResult result = ktxTexture_CreateFromNamedFile(path, KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, &ktx);
	if (result != KTX_SUCCESS) { _abort("ktxTexture_CreateFromNamedFile failed"); }

	VkFormat vk_format = ktxTexture_GetVkFormat(ktx);
	u32 width          = ktx->baseWidth;
	u32 height         = ktx->baseHeight;
	u32 mip_levels     = ktx->numLevels;

	// Create staging buffer
	ktx_size_t data_size = ktxTexture_GetDataSize(ktx);
	void *data           = ktxTexture_GetData(ktx);

	PrRhiBufferDesc buf_desc = {
		.size   = data_size,
		.usage  = PR_RHI_BUFFER_USAGE_TRANSFER_SRC,
		.memory = PR_RHI_MEMORY_CPU_TO_GPU,
	};
	PrRhiBuffer *staging = prRhiCreateBufferVk(device, buf_desc);
	memcpy(prRhiBufferMapVk(device, staging), data, data_size);

	// Create the destination image
	VkImageCreateInfo img_info = {
		.sType             = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
		.imageType         = VK_IMAGE_TYPE_2D,
		.format            = vk_format,
		.extent.width      = width,
		.extent.height     = height,
		.extent.depth      = 1,
		.mipLevels         = mip_levels,
		.arrayLayers       = 1,
		.samples           = VK_SAMPLE_COUNT_1_BIT,
		.tiling            = VK_IMAGE_TILING_OPTIMAL,
		.usage             = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
		.sharingMode       = VK_SHARING_MODE_EXCLUSIVE,
		.initialLayout     = VK_IMAGE_LAYOUT_UNDEFINED,
	};

	VmaAllocationCreateInfo alloc_info = { .usage = VMA_MEMORY_USAGE_AUTO };

	VkImage vk_image        = VK_NULL_HANDLE;
	VmaAllocation vma_alloc = VK_NULL_HANDLE;
	_checkVk(vmaCreateImage(device->allocator, &img_info, &alloc_info, &vk_image, &vma_alloc, NULL),
	         "vmaCreateImage");

	// Record copy commands
	VkCommandBuffer vk_cb = cb->handle;
	vkBeginCommandBuffer(vk_cb, &(VkCommandBufferBeginInfo){
		.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
		.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT,
	});

	// Transition to TRANSFER_DST
	vkCmdPipelineBarrier2(vk_cb, &(VkDependencyInfo){
		.sType                   = VK_STRUCTURE_TYPE_DEPENDENCY_INFO,
		.imageMemoryBarrierCount = 1,
		.pImageMemoryBarriers    = &(VkImageMemoryBarrier2){
			.sType            = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2,
			.srcStageMask     = VK_PIPELINE_STAGE_2_NONE,
			.srcAccessMask    = VK_ACCESS_2_NONE,
			.dstStageMask     = VK_PIPELINE_STAGE_2_TRANSFER_BIT,
			.dstAccessMask    = VK_ACCESS_2_TRANSFER_WRITE_BIT,
			.oldLayout        = VK_IMAGE_LAYOUT_UNDEFINED,
			.newLayout        = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
			.image            = vk_image,
			.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, mip_levels, 0, 1 },
		},
	});

	// Copy buffer to image
	VkBufferImageCopyArray copy_regions = wpArrayAllocCapacity(VkBufferImageCopy, &_G_RHI_CONTEXT.allocator,
	                                                           mip_levels, WP_ARRAY_INIT_NONE);
	for (u32 i = 0; i < mip_levels; ++i) {
		ktx_size_t mip_offset = 0;
		KTX_error_code ret = ktxTexture_GetImageOffset(ktx, i, 0, 0, &mip_offset);

		VkBufferImageCopy copy_region = {
			.bufferOffset     = mip_offset,
			.imageSubresource = {
				.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
				.mipLevel   = i,
				.layerCount = 1,
			},
			.imageExtent      = {
				.width  = width >> i,
				.height = height >>i,
				.depth  = 1,
			},
		};

		wpArrayAppendCapped(VkBufferImageCopy, copy_regions, &copy_region);
	}

	vkCmdCopyBufferToImage(vk_cb, staging->handle, vk_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
	                       wpArrayCount(copy_regions), copy_regions);

	// Transition to SHADER_READ_ONLY
	vkCmdPipelineBarrier2(vk_cb, &(VkDependencyInfo){
		.sType                   = VK_STRUCTURE_TYPE_DEPENDENCY_INFO,
		.imageMemoryBarrierCount = 1,
		.pImageMemoryBarriers    = &(VkImageMemoryBarrier2){
			.sType            = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2,
			.srcStageMask     = VK_PIPELINE_STAGE_2_TRANSFER_BIT,
			.srcAccessMask    = VK_ACCESS_2_TRANSFER_WRITE_BIT,
			.dstStageMask     = VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT,
			.dstAccessMask    = VK_ACCESS_2_SHADER_READ_BIT,
			.oldLayout        = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
			.newLayout        = VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL,
			.image            = vk_image,
			.subresourceRange = {
				.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
				.levelCount = mip_levels,
				.layerCount = 1,
			},
		},
	});

	vkEndCommandBuffer(vk_cb);

	// Submit and wait
	PrRhiFenceDesc fence_desc = { .signaled = false };
	PrRhiFence *fence         = prRhiCreateFenceVk(device, fence_desc);
	prRhiQueueSubmitVk(device, cb, NULL, NULL, fence);
	prRhiWaitForFencesVk(device, &fence, 1, VK_TRUE, UINT64_MAX);
	prRhiDestroyFenceVk(device, fence);
	vkResetCommandBuffer(vk_cb, 0);

	// Create image view
	VkImageViewCreateInfo view_info       = {
		.sType                       = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
		.image                       = vk_image,
		.viewType                    = VK_IMAGE_VIEW_TYPE_2D,
		.format                      = vk_format,
		.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
		.subresourceRange.levelCount = mip_levels,
		.subresourceRange.layerCount = 1,
	};

	VkImageView vk_view = VK_NULL_HANDLE;
	_checkVk(vkCreateImageView(vk_device, &view_info, NULL, &vk_view), "vkCreateImageView");

	PrRhiTexture *texture = wpMemAllocatorAlloc(&_G_RHI_CONTEXT.allocator, sizeof(PrRhiTexture));
	if (!texture) { _abort("alloc failed for PrRhiTexture"); }
	texture->image      = vk_image;
	texture->view       = vk_view;
	texture->allocation = vma_alloc;
	texture->width      = width;
	texture->height     = height;
	texture->format     = vk_format;

	// Cleanup staging
	prRhiDestroyBufferVk(device, staging);
	ktxTexture_Destroy(ktx);

	return texture;
}

PrRhiTextureSize prRhiGetTextureSizeVk(PrRhiTexture *texture) {
	PrRhiTextureSize size = {};
	if (!texture) { return size; }
	size.width  = texture->width;
	size.height = texture->height;
	return size;
}

void prRhiDestroyTextureVk(PrRhiDevice *device, PrRhiTexture *texture) {
	if (!texture) { return; }
	VkDevice vk_device = device->handle;

	vkDestroyImageView(vk_device, texture->view, NULL);
	if (texture->allocation) {
		vmaDestroyImage(device->allocator, texture->image, texture->allocation);
	}
	wpMemAllocatorFree(&_G_RHI_CONTEXT.allocator, (void**)&texture, sizeof(PrRhiTexture));
}

// ============================================================================
// Samplers
// ============================================================================

PrRhiSampler *prRhiCreateSamplerVk(PrRhiDevice *device, PrRhiSamplerDesc desc) {
	VkSamplerCreateInfo info = {
		.sType               = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO,
		.magFilter           = _toVkFilter(desc.mag_filter),
		.minFilter           = _toVkFilter(desc.min_filter),
		.mipmapMode          = _toVkMipmapMode(desc.mipmap_mode),
		.addressModeU        = _toVkAddressMode(desc.address_mode_u),
		.addressModeV        = _toVkAddressMode(desc.address_mode_v),
		.addressModeW        = _toVkAddressMode(desc.address_mode_w),
		.anisotropyEnable    = desc.max_anisotropy > 0.0f ? VK_TRUE : VK_FALSE,
		.maxAnisotropy       = desc.max_anisotropy,
		.minLod              = desc.min_lod,
		.maxLod              = desc.max_lod,
		.mipLodBias          = 0.0f,
	};

	VkSampler vk_sampler = VK_NULL_HANDLE;
	_checkVk(vkCreateSampler(device->handle, &info, NULL, &vk_sampler), "vkCreateSampler");

	PrRhiSampler *sampler = wpMemAllocatorAlloc(&_G_RHI_CONTEXT.allocator, sizeof(PrRhiSampler));
	if (!sampler) { _abort("alloc failed for PrRhiSampler"); }
	sampler->handle = vk_sampler;
	return sampler;
}

void prRhiDestroySamplerVk(PrRhiDevice *device, PrRhiSampler *sampler) {
	if (!sampler) { return; }
	vkDestroySampler(device->handle, sampler->handle, NULL);
	wpMemAllocatorFree(&_G_RHI_CONTEXT.allocator, (void**)&sampler, sizeof(PrRhiSampler));
}

// ============================================================================
// Shaders
// ============================================================================

PrRhiShader *prRhiCreateShaderVk(PrRhiDevice *device, PrRhiShaderDesc desc) {
	VkShaderModuleCreateInfo info = {
		.sType    = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
		.codeSize = desc.spirv_size,
		.pCode    = (const u32 *)desc.spirv_code,
	};

	VkShaderModule vk_module = VK_NULL_HANDLE;
	_checkVk(vkCreateShaderModule(device->handle, &info, NULL, &vk_module), "vkCreateShaderModule");

	PrRhiShader *shader = wpMemAllocatorAlloc(&_G_RHI_CONTEXT.allocator, sizeof(PrRhiShader));
	if (!shader) { _abort("alloc failed for PrRhiShader"); }
	shader->handle = vk_module;
	return shader;
}

void prRhiDestroyShaderVk(PrRhiDevice *device, PrRhiShader *shader) {
	if (!shader) { return; }
	vkDestroyShaderModule(device->handle, shader->handle, NULL);
	wpMemAllocatorFree(&_G_RHI_CONTEXT.allocator, (void**)&shader, sizeof(PrRhiShader));
}

// ============================================================================
// Pipeline layouts
// ============================================================================

PrRhiPipelineLayout *prRhiCreatePipelineLayoutVk(PrRhiDevice *device,
                                                 PrRhiPipelineLayoutDesc desc) {
	VkDevice vk_device = device->handle;

	// Gather descriptor set layouts
	u32 layout_count = (desc.set_layouts && wpArrayCount(desc.set_layouts) > 0)
		? (u32)wpArrayCount(desc.set_layouts) : 0;

	VkDescriptorSetLayoutArray vk_layouts = NULL;
	if (layout_count > 0) {
		vk_layouts =
			wpArrayAllocCapacity(VkDescriptorSetLayout, &_G_RHI_CONTEXT.allocator, layout_count,
			                     WP_ARRAY_INIT_FILLED);
		if (!vk_layouts) { _abort("alloc failed for VkDescriptorSetLayout array"); }

		for (u32 i = 0; i < layout_count; ++i) {
			vk_layouts[i] = desc.set_layouts[i]->handle;
		}
	}

	// Gather push constant ranges
	u32 pc_count = (desc.push_constant_ranges && wpArrayCount(desc.push_constant_ranges) > 0)
		? (u32)wpArrayCount(desc.push_constant_ranges) : 0;

	VkPushConstantRangeArray pc_ranges = NULL;
	if (pc_count > 0) {
		pc_ranges =
			wpArrayAllocCapacity(VkPushConstantRange, &_G_RHI_CONTEXT.allocator, pc_count, WP_ARRAY_INIT_NONE);
		if (!pc_ranges) { _abort("alloc failed for VkPushConstantRange array"); }

		for (u32 i = 0; i < pc_count; ++i) {
			pc_ranges[i].stageFlags = _toVkShaderStage(desc.push_constant_ranges[i].stage_flags);
			pc_ranges[i].offset     = desc.push_constant_ranges[i].offset;
			pc_ranges[i].size       = desc.push_constant_ranges[i].size;
		}
	}

	VkPipelineLayoutCreateInfo pl_info = {
		.sType                  = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
		.setLayoutCount         = layout_count,
		.pSetLayouts            = vk_layouts,
		.pushConstantRangeCount = pc_count,
		.pPushConstantRanges    = pc_ranges,
	};

	VkPipelineLayout vk_layout = VK_NULL_HANDLE;
	_checkVk(vkCreatePipelineLayout(vk_device, &pl_info, NULL, &vk_layout), "vkCreatePipelineLayout");

	if (vk_layouts) { wpArrayDealloc(VkDescriptorSetLayout, &_G_RHI_CONTEXT.allocator, &vk_layouts); }
	if (pc_ranges) { wpArrayDealloc(VkPushConstantRange, &_G_RHI_CONTEXT.allocator, &pc_ranges); }

	PrRhiPipelineLayout *layout = wpMemAllocatorAlloc(&_G_RHI_CONTEXT.allocator, sizeof(PrRhiPipelineLayout));
	if (!layout) { _abort("alloc failed for PrRhiPipelineLayout"); }
	layout->handle = vk_layout;
	return layout;
}

void prRhiDestroyPipelineLayoutVk(PrRhiDevice *device, PrRhiPipelineLayout *layout) {
	if (!layout) { return; }
	vkDestroyPipelineLayout(device->handle, layout->handle, NULL);
	wpMemAllocatorFree(&_G_RHI_CONTEXT.allocator, (void**)&layout, sizeof(PrRhiPipelineLayout));
}

// ============================================================================
// Pipelines
// ============================================================================

PrRhiPipeline *prRhiCreateGraphicsPipelineVk(PrRhiDevice *device,
                                             PrRhiGraphicsPipelineDesc desc) {
	VkDevice vk_device = device->handle;

	// Shader stages
	VkPipelineShaderStageCreateInfo stages[2] = {};
	u32 stage_count = 0;

	if (desc.vertex_shader) {
		stages[stage_count].sType  = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
		stages[stage_count].stage  = VK_SHADER_STAGE_VERTEX_BIT;
		stages[stage_count].module = desc.vertex_shader->handle;
		stages[stage_count].pName  = desc.vertex_shader_entry_point;
		++stage_count;
	}
	if (desc.fragment_shader) {
		stages[stage_count].sType  = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
		stages[stage_count].stage  = VK_SHADER_STAGE_FRAGMENT_BIT;
		stages[stage_count].module = desc.fragment_shader->handle;
		stages[stage_count].pName  = desc.fragment_shader_entry_point;
		++stage_count;
	}

	// Vertex input state
	VkVertexInputBindingDescriptionArray vk_bindings = NULL;
	u32 binding_count = 0;
	if (desc.vertex_bindings && wpArrayCount(desc.vertex_bindings) > 0) {
		binding_count = (u32)wpArrayCount(desc.vertex_bindings);
		vk_bindings = wpArrayAllocCapacity(VkVertexInputBindingDescription, &_G_RHI_CONTEXT.allocator, binding_count,
			                                 WP_ARRAY_INIT_FILLED);
		for (u32 i = 0; i < binding_count; ++i) {
			vk_bindings[i].binding   = desc.vertex_bindings[i].binding;
			vk_bindings[i].stride    = desc.vertex_bindings[i].stride;
			vk_bindings[i].inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
		}
	}

	VkVertexInputAttributeDescriptionArray vk_attrs = NULL;
	u32 attr_count = 0;
	if (desc.vertex_attributes && wpArrayCount(desc.vertex_attributes) > 0) {
		attr_count = (u32)wpArrayCount(desc.vertex_attributes);
		vk_attrs   = wpArrayAllocCapacity(VkVertexInputAttributeDescription, &_G_RHI_CONTEXT.allocator,
		                                  attr_count, WP_ARRAY_INIT_FILLED);
		for (u32 i = 0; i < attr_count; ++i) {
			vk_attrs[i].location = desc.vertex_attributes[i].location;
			vk_attrs[i].binding  = desc.vertex_attributes[i].binding;
			vk_attrs[i].format   = _toVkFormat(desc.vertex_attributes[i].format);
			vk_attrs[i].offset   = desc.vertex_attributes[i].offset;
		}
	}

	VkPipelineVertexInputStateCreateInfo vertex_input = {
		.sType                                = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO,
		.vertexBindingDescriptionCount        = binding_count,
		.pVertexBindingDescriptions           = vk_bindings,
		.vertexAttributeDescriptionCount      = attr_count,
		.pVertexAttributeDescriptions         = vk_attrs,
	};

	// Input assembly
	VkPipelineInputAssemblyStateCreateInfo input_assembly = {
		.sType    = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO,
		.topology = _toVkTopology(desc.topology),
	};

	// Viewport & scissor dynamic states
	VkPipelineViewportStateCreateInfo viewport_state = {
		.sType         = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO,
		.viewportCount = 1,
		.scissorCount  = 1,
	};

	VkDynamicStateArray dynamic_states_stack = wpArrayWithCapacity(VkDynamicState, 2, WP_ARRAY_INIT_NONE);
	if (desc.dynamic_viewport) {
		VkDynamicState viewport = VK_DYNAMIC_STATE_VIEWPORT;
		wpArrayAppendCapped(VkDynamicState, dynamic_states_stack, &viewport);
	}
	if (desc.dynamic_scissor) {
		VkDynamicState scissor = VK_DYNAMIC_STATE_SCISSOR;
		wpArrayAppendCapped(VkDynamicState, dynamic_states_stack, &scissor);
	}

	VkPipelineDynamicStateCreateInfo dynamic_state = {
		.sType             = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO,
		.dynamicStateCount = wpArrayCount(dynamic_states_stack),
		.pDynamicStates    = dynamic_states_stack,
	};

	// Depth/stencil
	VkPipelineDepthStencilStateCreateInfo depth_stencil = {
		.sType                 = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO,
		.depthTestEnable       = desc.depth_test_enable ? VK_TRUE : VK_FALSE,
		.depthWriteEnable      = desc.depth_write_enable ? VK_TRUE : VK_FALSE,
		.depthCompareOp        = _toVkCompareOp(desc.depth_compare_op),
		.depthBoundsTestEnable = VK_FALSE,
		.stencilTestEnable     = VK_FALSE,
	};

	// Dynamic rendering
	VkFormatArray vk_color_formats = NULL;
	u64 attachment_count = wpArrayCount(desc.color_attachment_formats);
	if (desc.color_attachment_formats && attachment_count > 0) {
		vk_color_formats = wpArrayAllocCapacity(VkFormat, &_G_RHI_CONTEXT.allocator, attachment_count,
		                                        WP_ARRAY_INIT_FILLED);
		for (u32 i = 0; i < wpArrayCount(vk_color_formats); ++i) {
			vk_color_formats[i] = _toVkFormat(desc.color_attachment_formats[i]);
		}
	}

	VkPipelineRenderingCreateInfo rendering_info = {
		.sType                   = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO,
		.depthAttachmentFormat   = _toVkFormat(desc.depth_attachment_format),
		.colorAttachmentCount    = wpArrayCount(vk_color_formats),
		.pColorAttachmentFormats = vk_color_formats,
	};

	// Blending
	u64 blend_count = wpArrayCount(desc.blend_attachments);
	VkPipelineColorBlendAttachmentStateArray blend_attachments =
		wpArrayAllocCapacity(VkPipelineColorBlendAttachmentState, &_G_RHI_CONTEXT.allocator, blend_count,
		                     WP_ARRAY_INIT_FILLED);
	if (desc.blend_attachments && blend_count > 0) {
		for (u32 i = 0; i < blend_count; ++i) {
			blend_attachments[i].blendEnable         = VK_FALSE;
			blend_attachments[i].colorWriteMask      = desc.blend_attachments[i].color_write_mask;
			blend_attachments[i].srcColorBlendFactor = VK_BLEND_FACTOR_ONE;
			blend_attachments[i].dstColorBlendFactor = VK_BLEND_FACTOR_ZERO;
			blend_attachments[i].colorBlendOp        = VK_BLEND_OP_ADD;
			blend_attachments[i].srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
			blend_attachments[i].dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO;
			blend_attachments[i].alphaBlendOp        = VK_BLEND_OP_ADD;
		}
	}

	VkPipelineColorBlendStateCreateInfo blend_state = {
		.sType           = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO,
		.attachmentCount = wpArrayCount(blend_attachments),
		.pAttachments    = blend_attachments,
	};

	// Rasterization
	VkPipelineRasterizationStateCreateInfo raster = {
		.sType       = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO,
		.polygonMode = (VkPolygonMode)desc.polygon_mode,
		.cullMode    = (VkCullModeFlags)desc.cull_mode,
		.frontFace   = (VkFrontFace)desc.front_face,
		.lineWidth   = desc.line_width,
	};

	// Multisampling
	VkPipelineMultisampleStateCreateInfo multisample = {
		.sType                = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO,
		.rasterizationSamples = (VkSampleCountFlagBits)desc.multisample_count,
	};

	// Create pipeline
	VkGraphicsPipelineCreateInfo pi = {
		.sType               = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO,
		.pNext               = &rendering_info,
		.stageCount          = stage_count,
		.pStages             = stages,
		.pVertexInputState   = &vertex_input,
		.pInputAssemblyState = &input_assembly,
		.pViewportState      = &viewport_state,
		.pRasterizationState = &raster,
		.pMultisampleState   = &multisample,
		.pDepthStencilState  = &depth_stencil,
		.pColorBlendState    = &blend_state,
		.pDynamicState       = &dynamic_state,
		.layout              = desc.layout ? (VkPipelineLayout)desc.layout->handle : VK_NULL_HANDLE,
	};

	VkPipeline vk_pipeline = VK_NULL_HANDLE;
	_checkVk(vkCreateGraphicsPipelines(vk_device, VK_NULL_HANDLE, 1, &pi, NULL, &vk_pipeline),
	         "vkCreateGraphicsPipelines");

	PrRhiPipeline *pipeline = wpMemAllocatorAlloc(&_G_RHI_CONTEXT.allocator, sizeof(PrRhiPipeline));
	if (!pipeline) { _abort("alloc failed for PrRhiPipeline"); }
	pipeline->handle = vk_pipeline;
	return pipeline;
}

PrRhiPipeline *prRhiCreateComputePipelineVk(PrRhiDevice *device, PrRhiComputePipelineDesc desc) {
	VkComputePipelineCreateInfo pi = {
		.sType  = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO,
		.stage  = (VkPipelineShaderStageCreateInfo){
			.sType  = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
			.stage  = VK_SHADER_STAGE_COMPUTE_BIT,
			.module = desc.shader ? desc.shader->handle : VK_NULL_HANDLE,
			.pName  = desc.shader_entry_point,
		},
		.layout = desc.layout ? desc.layout->handle : VK_NULL_HANDLE,
	};

	VkPipeline vk_pipeline = VK_NULL_HANDLE;
	_checkVk(vkCreateComputePipelines(device->handle, VK_NULL_HANDLE, 1, &pi, NULL, &vk_pipeline),
	         "vkCreateComputePipelines");

	PrRhiPipeline *pipeline = wpMemAllocatorAlloc(&_G_RHI_CONTEXT.allocator, sizeof(PrRhiPipeline));
	if (!pipeline) { _abort("alloc failed for PrRhiPipeline"); }
	pipeline->handle = vk_pipeline;
	return pipeline;
}

void prRhiDestroyPipelineVk(PrRhiDevice *device, PrRhiPipeline *pipeline) {
	if (!pipeline) { return; }
	vkDestroyPipeline((VkDevice)device->handle, (VkPipeline)pipeline->handle, NULL);
	wpMemAllocatorFree(&_G_RHI_CONTEXT.allocator, (void**)&pipeline, sizeof(PrRhiPipeline));
}

// ============================================================================
// Descriptor set layouts
// ============================================================================

PrRhiDescriptorSetLayout *prRhiCreateDescriptorSetLayoutVk(PrRhiDevice *device,
                                                           PrRhiDescriptorSetLayoutDesc desc) {
	VkDevice vk_device = device->handle;

	u32 binding_count = (desc.bindings && wpArrayCount(desc.bindings) > 0)
		? (u32)wpArrayCount(desc.bindings) : 0;

	VkDescriptorSetLayoutBindingArray vk_bindings =
		wpArrayAllocCapacity(VkDescriptorSetLayoutBinding, &_G_RHI_CONTEXT.allocator, binding_count,
		                     WP_ARRAY_INIT_FILLED);
	VkDescriptorBindingFlagsArray vk_flags =
		wpArrayAllocCapacity(VkDescriptorBindingFlags, &_G_RHI_CONTEXT.allocator, binding_count,
		                     WP_ARRAY_INIT_FILLED);

	for (u32 i = 0; i < binding_count; ++i) {
		vk_bindings[i].binding            = i;
		vk_bindings[i].descriptorType     = _toVkDescriptorType(desc.bindings[i].type);
		vk_bindings[i].descriptorCount    = desc.bindings[i].descriptor_count;
		vk_bindings[i].stageFlags         = _toVkShaderStage(desc.bindings[i].stage_flags);
		vk_bindings[i].pImmutableSamplers = NULL;
		vk_flags[i]                       = _toVkBindingFlags(desc.bindings[i].binding_flags);
	}

	VkDescriptorSetLayoutBindingFlagsCreateInfo flags_info = {
		.sType         = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_BINDING_FLAGS_CREATE_INFO,
		.bindingCount  = binding_count,
		.pBindingFlags = vk_flags,
	};

	VkDescriptorSetLayoutCreateInfo info = {
		.sType        = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
		.pNext        = &flags_info,
		.bindingCount = binding_count,
		.pBindings    = vk_bindings,
	};

	VkDescriptorSetLayout vk_layout = VK_NULL_HANDLE;
	_checkVk(vkCreateDescriptorSetLayout(vk_device, &info, NULL, &vk_layout), "vkCreateDescriptorSetLayout");

	PrRhiDescriptorSetLayout *layout = wpMemAllocatorAlloc(&_G_RHI_CONTEXT.allocator, sizeof(PrRhiDescriptorSetLayout));
	if (!layout) { _abort("alloc failed for PrRhiDescriptorSetLayout"); }
	layout->handle = vk_layout;
	return layout;
}

void prRhiDestroyDescriptorSetLayoutVk(PrRhiDevice *device, PrRhiDescriptorSetLayout *layout) {
	if (!layout) { return; }
	vkDestroyDescriptorSetLayout(device->handle, layout->handle, NULL);
	wpMemAllocatorFree(&_G_RHI_CONTEXT.allocator, (void**)&layout, sizeof(PrRhiDescriptorSetLayout));
}

// ============================================================================
// Descriptor pools
// ============================================================================

PrRhiDescriptorPool *prRhiCreateDescriptorPoolVk(PrRhiDevice *device,
                                                 PrRhiDescriptorPoolDesc desc) {
	VkDevice vk_device = device->handle;

	u32 pool_size_count = (desc.pool_sizes && wpArrayCount(desc.pool_sizes) > 0)
		? (u32)wpArrayCount(desc.pool_sizes) : 0;

	VkDescriptorPoolSizeArray vk_sizes =
		wpArrayAllocCapacity(VkDescriptorPoolSize, &_G_RHI_CONTEXT.allocator, pool_size_count,
		                     WP_ARRAY_INIT_FILLED);
	for (u32 i = 0; i < pool_size_count; ++i) {
		vk_sizes[i].type            = _toVkDescriptorType(desc.pool_sizes[i].type);
		vk_sizes[i].descriptorCount = desc.pool_sizes[i].descriptor_count;
	}

	VkDescriptorPoolCreateInfo info = {
		.sType         = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO,
		.maxSets       = desc.max_sets,
		.poolSizeCount = pool_size_count,
		.pPoolSizes    = vk_sizes,
	};

	VkDescriptorPool vk_pool = VK_NULL_HANDLE;
	_checkVk(vkCreateDescriptorPool(vk_device, &info, NULL, &vk_pool), "vkCreateDescriptorPool");

	PrRhiDescriptorPool *pool = wpMemAllocatorAlloc(&_G_RHI_CONTEXT.allocator, sizeof(PrRhiDescriptorPool));
	if (!pool) { _abort("alloc failed for PrRhiDescriptorPool"); }
	pool->handle = vk_pool;
	return pool;
}

void prRhiDestroyDescriptorPoolVk(PrRhiDevice *device, PrRhiDescriptorPool *pool) {
	if (!pool) { return; }
	vkDestroyDescriptorPool(device->handle, pool->handle, NULL);
	wpMemAllocatorFree(&_G_RHI_CONTEXT.allocator, (void**)&pool, sizeof(PrRhiDescriptorPool));
}

void prRhiResetDescriptorPoolVk(PrRhiDevice *device, PrRhiDescriptorPool *pool) {
	vkResetDescriptorPool(device->handle, pool->handle, 0);
}

// ============================================================================
// Descriptor sets
// ============================================================================

PrRhiDescriptorSet *prRhiAllocateDescriptorSetVk(PrRhiDevice *device,
                                                 PrRhiDescriptorPool *pool,
                                                 PrRhiDescriptorSetLayout *layout,
                                                 WpU32Array variable_descriptor_counts) {
	VkDevice vk_device = device->handle;

	VkDescriptorSetVariableDescriptorCountAllocateInfo var_info = {
		.sType              = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_VARIABLE_DESCRIPTOR_COUNT_ALLOCATE_INFO,
		.descriptorSetCount = variable_descriptor_counts ? wpArrayCount(variable_descriptor_counts) : 0,
		.pDescriptorCounts  = variable_descriptor_counts,
	};

	VkDescriptorSetLayout vk_layout = layout->handle;

	VkDescriptorSetAllocateInfo info = {
		.sType              = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO,
		.pNext              = &var_info,
		.descriptorPool     = pool->handle,
		.descriptorSetCount = 1,
		.pSetLayouts        = &vk_layout,
	};

	VkDescriptorSet vk_set = VK_NULL_HANDLE;
	_checkVk(vkAllocateDescriptorSets(vk_device, &info, &vk_set), "vkAllocateDescriptorSets");

	PrRhiDescriptorSet *set = wpMemAllocatorAlloc(&_G_RHI_CONTEXT.allocator, sizeof(PrRhiDescriptorSet));
	if (!set) { _abort("alloc failed for PrRhiDescriptorSet"); }
	set->handle = vk_set;
	return set;
}

void prRhiFreeDescriptorSetVk(PrRhiDevice *device, PrRhiDescriptorPool *pool,
                              PrRhiDescriptorSet *set) {
	if (!set) { return; }
	VkDevice vk_device     = device->handle;
	VkDescriptorSet vk_set = set->handle;
	_checkVk(vkFreeDescriptorSets(vk_device, pool->handle, 1, &vk_set), "vkFreeDescriptorSets");
	wpMemAllocatorFree(&_G_RHI_CONTEXT.allocator, (void**)&set, sizeof(PrRhiDescriptorSet));
}

void prRhiUpdateDescriptorSetVk(PrRhiDevice *device, PrRhiWriteDescriptorSetArray writes) {
	VkDevice vk_device = device->handle;

	u32 count = writes ? (u32)wpArrayCount(writes) : 0;
	if (count == 0) { return; }

	// We process each write individually because we need to expand image_info/buffer_info arrays
	for (u32 i = 0; i < count; ++i) {
		PrRhiWriteDescriptorSet *w = &writes[i];
		VkDescriptorType vk_type   = _toVkDescriptorType(w->type);

		if (w->image_info && wpArrayCount(w->image_info) > 0) {
			u32 img_count = (u32)wpArrayCount(w->image_info);

			VkDescriptorImageInfoArray vk_img_info =
				wpArrayAllocCapacity(VkDescriptorImageInfo, &_G_RHI_CONTEXT.allocator, img_count,
				                     WP_ARRAY_INIT_FILLED);
			for (u32 j = 0; j < img_count; ++j) {
				PrRhiDescriptorImageInfo *src = &w->image_info[j];
				vk_img_info[j].sampler        = src->sampler ? src->sampler->handle : VK_NULL_HANDLE;
				vk_img_info[j].imageView      = src->texture ? src->texture->view : VK_NULL_HANDLE;
				vk_img_info[j].imageLayout    = _toVkImageLayout(src->layout);
			}

			VkWriteDescriptorSet vk_write = {
				.sType           = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
				.dstSet          = w->dst_set->handle,
				.dstBinding      = w->dst_binding,
				.dstArrayElement = w->dst_array_element,
				.descriptorCount = img_count,
				.descriptorType  = vk_type,
				.pImageInfo      = vk_img_info,
			};

			vkUpdateDescriptorSets(vk_device, 1, &vk_write, 0, NULL);
		}

		if (w->buffer_info && wpArrayCount(w->buffer_info) > 0) {
			u32 buf_count = (u32)wpArrayCount(w->buffer_info);


			VkDescriptorBufferInfoArray vk_buf_info =
				wpArrayAllocCapacity(VkDescriptorBufferInfo, &_G_RHI_CONTEXT.allocator, buf_count,
				                     WP_ARRAY_INIT_FILLED);
			for (u32 j = 0; j < buf_count; ++j) {
				PrRhiDescriptorBufferInfo *src = &w->buffer_info[j];
				vk_buf_info[j].buffer          = src->buffer ? src->buffer->handle : VK_NULL_HANDLE;
				vk_buf_info[j].offset          = src->offset;
				vk_buf_info[j].range           = src->range;
			}

			VkWriteDescriptorSet vk_write = {
				.sType           = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
				.dstSet          = w->dst_set->handle,
				.dstBinding      = w->dst_binding,
				.dstArrayElement = w->dst_array_element,
				.descriptorCount = buf_count,
				.descriptorType  = vk_type,
				.pBufferInfo     = vk_buf_info,
			};

			vkUpdateDescriptorSets(vk_device, 1, &vk_write, 0, NULL);
		}
	}
}

// ============================================================================
// Fences
// ============================================================================

PrRhiFence *prRhiCreateFenceVk(PrRhiDevice *device, PrRhiFenceDesc desc) {
	VkFenceCreateInfo info = {
		.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO,
		.flags = desc.signaled ? VK_FENCE_CREATE_SIGNALED_BIT : 0,
	};

	VkFence vk_fence = VK_NULL_HANDLE;
	_checkVk(vkCreateFence(device->handle, &info, NULL, &vk_fence), "vkCreateFence");

	PrRhiFence *fence = wpMemAllocatorAlloc(&_G_RHI_CONTEXT.allocator, sizeof(PrRhiFence));
	if (!fence) { _abort("alloc failed for PrRhiFence"); }
	fence->handle = vk_fence;
	return fence;
}

void prRhiDestroyFenceVk(PrRhiDevice *device, PrRhiFence *fence) {
	if (!fence) { return; }
	vkDestroyFence(device->handle, fence->handle, NULL);
	wpMemAllocatorFree(&_G_RHI_CONTEXT.allocator, (void**)&fence, sizeof(PrRhiFence));
}

void prRhiWaitForFencesVk(PrRhiDevice *device, PrRhiFenceArray fences, u32 count,
                          b8 wait_all, u64 timeout_ns) {
	VkFence vk_fences[TEMP_FENCE_MAX_COUNT] = {0};
	u32 real_count = count < TEMP_FENCE_MAX_COUNT ? count : TEMP_FENCE_MAX_COUNT;
	for (u32 i = 0; i < real_count; ++i) {
		vk_fences[i] = fences[i]->handle;
	}
	_checkVk(vkWaitForFences(device->handle, real_count, vk_fences,
	                         wait_all ? VK_TRUE : VK_FALSE, timeout_ns),
	         "vkWaitForFences");
}

void prRhiResetFencesVk(PrRhiDevice *device, PrRhiFenceArray fences, u32 count) {
	VkFence vk_fences[TEMP_FENCE_MAX_COUNT] = {0};
	u32 real_count = count < TEMP_FENCE_MAX_COUNT ? count : TEMP_FENCE_MAX_COUNT;
	for (u32 i = 0; i < real_count; ++i) {
		vk_fences[i] = fences[i]->handle;
	}
	_checkVk(vkResetFences(device->handle, real_count, vk_fences), "vkResetFences");
}

// ============================================================================
// Semaphores
// ============================================================================

PrRhiSemaphore *prRhiCreateSemaphoreVk(PrRhiDevice *device) {
	VkSemaphoreCreateInfo info = { .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO };

	VkSemaphore vk_semaphore = VK_NULL_HANDLE;
	_checkVk(vkCreateSemaphore(device->handle, &info, NULL, &vk_semaphore), "vkCreateSemaphore");

	PrRhiSemaphore *semaphore = wpMemAllocatorAlloc(&_G_RHI_CONTEXT.allocator, sizeof(PrRhiSemaphore));
	if (!semaphore) { _abort("alloc failed for PrRhiSemaphore"); }
	semaphore->handle = vk_semaphore;
	return semaphore;
}

void prRhiDestroySemaphoreVk(PrRhiDevice *device, PrRhiSemaphore *semaphore) {
	if (!semaphore) { return; }
	vkDestroySemaphore(device->handle, semaphore->handle, NULL);
	wpMemAllocatorFree(&_G_RHI_CONTEXT.allocator, (void**)&semaphore, sizeof(PrRhiSemaphore));
}

// ============================================================================
// Command pools
// ============================================================================

PrRhiCommandPool *prRhiCreateCommandPoolVk(PrRhiDevice *device) {
	VkCommandPoolCreateInfo info = {
		.sType            = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,
		.flags            = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT,
		.queueFamilyIndex = device->queue_family_index,
	};

	VkCommandPool vk_pool = VK_NULL_HANDLE;
	_checkVk(vkCreateCommandPool(device->handle, &info, NULL, &vk_pool), "vkCreateCommandPool");

	PrRhiCommandPool *pool = wpMemAllocatorAlloc(&_G_RHI_CONTEXT.allocator, sizeof(PrRhiCommandPool));
	if (!pool) { _abort("alloc failed for PrRhiCommandPool"); }
	pool->handle = vk_pool;
	return pool;
}

void prRhiDestroyCommandPoolVk(PrRhiDevice *device, PrRhiCommandPool *pool) {
	if (!pool) { return; }
	vkDestroyCommandPool(device->handle, pool->handle, NULL);
	wpMemAllocatorFree(&_G_RHI_CONTEXT.allocator, (void**)&pool, sizeof(PrRhiCommandPool));
}

PrRhiCommandBufferArray prRhiAllocateCommandBuffersVk(PrRhiDevice *device, PrRhiCommandPool *pool,
                                                      u32 count) {
	VkCommandBufferAllocateInfo info = {
		.sType              = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,
		.commandPool        = pool->handle,
		.level              = VK_COMMAND_BUFFER_LEVEL_PRIMARY,
		.commandBufferCount = count,
	};

	VkCommandBufferArray vk_cbs = wpArrayAllocCapacity(VkCommandBuffer, &_G_RHI_CONTEXT.allocator, count, WP_ARRAY_INIT_FILLED);
	if (!vk_cbs) { _abort("alloc failed for VkCommandBuffer array"); }

	_checkVk(vkAllocateCommandBuffers((VkDevice)device->handle, &info, vk_cbs),
	         "vkAllocateCommandBuffers");

	PrRhiCommandBufferArray cbs = wpArrayAllocCapacity(PrRhiCommandBuffer *, &_G_RHI_CONTEXT.allocator, count, WP_ARRAY_INIT_NONE);
	if (!cbs) { _abort("alloc failed for PrRhiCommandBuffer array"); }

	for (u32 i = 0; i < count; ++i) {
		PrRhiCommandBuffer *cb = (PrRhiCommandBuffer *)wpMemAllocatorAlloc(&_G_RHI_CONTEXT.allocator, sizeof(PrRhiCommandBuffer));
		if (!cb) { _abort("alloc failed for PrRhiCommandBuffer"); }
		cb->handle = vk_cbs[i];
		wpArrayAppendCapped(PrRhiCommandBuffer *, cbs, &cb);
	}

	wpArrayDealloc(VkCommandBuffer, &_G_RHI_CONTEXT.allocator, &vk_cbs);

	return cbs;
}

void prRhiFreeCommandBuffersVk(PrRhiDevice *device, PrRhiCommandPool *pool, PrRhiCommandBufferArray buffers) {
	u64 count = wpArrayCount(buffers);

	if (!buffers || count == 0) { return; }

	VkDevice vk_device    = device->handle;
	VkCommandPool vk_pool = pool->handle;

	VkCommandBufferArray vk_cbs = wpArrayAllocCapacity(VkCommandBuffer, &_G_RHI_CONTEXT.allocator, count,
	                                                   WP_ARRAY_INIT_FILLED);
	for (u32 i = 0; i < count; ++i) {
		vk_cbs[i] = buffers[i]->handle;
	}

	vkFreeCommandBuffers(vk_device, vk_pool, count, vk_cbs);
}

// ============================================================================
// Command buffer recording
// ============================================================================

void prRhiBeginCommandBufferVk(PrRhiCommandBuffer *cb) {
	VkCommandBufferBeginInfo info = {
		.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
		.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT,
	};

	_checkVk(vkBeginCommandBuffer((VkCommandBuffer)cb->handle, &info),
	         "vkBeginCommandBuffer");
}

void prRhiEndCommandBufferVk(PrRhiCommandBuffer *cb) {
	_checkVk(vkEndCommandBuffer((VkCommandBuffer)cb->handle),
	         "vkEndCommandBuffer");
}

void prRhiResetCommandBufferVk(PrRhiCommandBuffer *cb) {
	_checkVk(vkResetCommandBuffer((VkCommandBuffer)cb->handle, 0),
	         "vkResetCommandBuffer");
}

// ============================================================================
// Pipeline barriers
// ============================================================================

void prRhiCmdPipelineBarrierVk(PrRhiCommandBuffer *cb,
                                PrRhiImageMemoryBarrierArray image_barriers,
                                PrRhiBufferMemoryBarrierArray buffer_barriers) {
	VkCommandBuffer vk_cb = (VkCommandBuffer)cb->handle;

	u32 img_count = (image_barriers && wpArrayCount(image_barriers) > 0)
		? (u32)wpArrayCount(image_barriers) : 0;
	u32 buf_count = (buffer_barriers && wpArrayCount(buffer_barriers) > 0)
		? (u32)wpArrayCount(buffer_barriers) : 0;

	if (img_count == 0 && buf_count == 0) return;

	VkImageMemoryBarrier2 vk_img_barriers[16] = {0};
	for (u32 i = 0; i < img_count && i < 16; ++i) {
		PrRhiImageMemoryBarrier *src = &image_barriers[i];
		VkImageLayout old_layout     = _toVkImageLayout(src->old_layout);
		VkImageLayout new_layout     = _toVkImageLayout(src->new_layout);

		VkImageAspectFlags aspect = VK_IMAGE_ASPECT_COLOR_BIT;
		if (src->texture) {
			VkFormat fmt = (VkFormat)src->texture->format;
			if (fmt == VK_FORMAT_D24_UNORM_S8_UINT || fmt == VK_FORMAT_D32_SFLOAT_S8_UINT)
				aspect = VK_IMAGE_ASPECT_DEPTH_BIT;
		}

		vk_img_barriers[i].sType                           = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2;
		vk_img_barriers[i].srcStageMask                    = _toVkPipelineStage2(src->src_stage_mask);
		vk_img_barriers[i].srcAccessMask                   = _toVkAccess2(src->src_access_mask);
		vk_img_barriers[i].dstStageMask                    = _toVkPipelineStage2(src->dst_stage_mask);
		vk_img_barriers[i].dstAccessMask                   = _toVkAccess2(src->dst_access_mask);
		vk_img_barriers[i].oldLayout                       = old_layout;
		vk_img_barriers[i].newLayout                       = new_layout;
		vk_img_barriers[i].image                           = src->texture ? (VkImage)src->texture->image : VK_NULL_HANDLE;
		vk_img_barriers[i].subresourceRange.aspectMask     = aspect;
		vk_img_barriers[i].subresourceRange.levelCount     = VK_REMAINING_MIP_LEVELS;
		vk_img_barriers[i].subresourceRange.layerCount     = VK_REMAINING_ARRAY_LAYERS;
	}

	VkBufferMemoryBarrier2 vk_buf_barriers[16] = {0};
	for (u32 i = 0; i < buf_count && i < 16; ++i) {
		PrRhiBufferMemoryBarrier *src = &buffer_barriers[i];

		vk_buf_barriers[i].sType         = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2;
		vk_buf_barriers[i].srcStageMask  = _toVkPipelineStage2(src->src_stage_mask);
		vk_buf_barriers[i].srcAccessMask = _toVkAccess2(src->src_access_mask);
		vk_buf_barriers[i].dstStageMask  = _toVkPipelineStage2(src->dst_stage_mask);
		vk_buf_barriers[i].dstAccessMask = _toVkAccess2(src->dst_access_mask);
		vk_buf_barriers[i].buffer        = src->buffer ? (VkBuffer)src->buffer->handle : VK_NULL_HANDLE;
		vk_buf_barriers[i].offset        = src->offset;
		vk_buf_barriers[i].size          = src->size;
	}

	VkDependencyInfo dep = {
		.sType                    = VK_STRUCTURE_TYPE_DEPENDENCY_INFO,
		.imageMemoryBarrierCount  = img_count,
		.pImageMemoryBarriers     = vk_img_barriers,
		.bufferMemoryBarrierCount = buf_count,
		.pBufferMemoryBarriers    = vk_buf_barriers,
	};

	vkCmdPipelineBarrier2(vk_cb, &dep);
}

// ============================================================================
// Dynamic rendering
// ============================================================================

void prRhiCmdBeginRenderingVk(PrRhiCommandBuffer *cb, PrRhiColorAttachmentArray color_attachments,
                              const PrRhiDepthAttachment *depth_attachment) {
	VkCommandBuffer vk_cb = cb->handle;

	u32 color_count = (color_attachments && wpArrayCount(color_attachments) > 0)
		? (u32)wpArrayCount(color_attachments) : 0;

	VkRenderingAttachmentInfoArray vk_color =
		wpArrayAllocCapacity(VkRenderingAttachmentInfo, &_G_RHI_CONTEXT.allocator, color_count, WP_ARRAY_INIT_FILLED);
	u32 width = 0, height = 0;

	for (u32 i = 0; i < color_count; ++i) {
		PrRhiColorAttachment *src = &color_attachments[i];
		vk_color[i].sType       = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO;
		vk_color[i].imageView   = src->texture ? src->texture->view : VK_NULL_HANDLE;
		vk_color[i].imageLayout = _toVkImageLayout(src->layout);
		vk_color[i].loadOp      = src->clear ? VK_ATTACHMENT_LOAD_OP_CLEAR : VK_ATTACHMENT_LOAD_OP_LOAD;
		vk_color[i].storeOp     = VK_ATTACHMENT_STORE_OP_STORE;
		if (src->clear) {
			vk_color[i].clearValue.color.float32[0] = src->clear_color[0];
			vk_color[i].clearValue.color.float32[1] = src->clear_color[1];
			vk_color[i].clearValue.color.float32[2] = src->clear_color[2];
			vk_color[i].clearValue.color.float32[3] = src->clear_color[3];
		}
		if (src->texture) {
			width  = src->texture->width;
			height = src->texture->height;
		}
	}

	VkRenderingAttachmentInfo vk_depth = {};
	if (depth_attachment && depth_attachment->texture) {
		vk_depth.sType                         = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO;
		vk_depth.imageView                     = depth_attachment->texture->view;
		vk_depth.imageLayout                   = _toVkImageLayout(depth_attachment->layout);
		vk_depth.loadOp                        = depth_attachment->clear ? VK_ATTACHMENT_LOAD_OP_CLEAR : VK_ATTACHMENT_LOAD_OP_LOAD;
		vk_depth.storeOp                       = VK_ATTACHMENT_STORE_OP_STORE;
		vk_depth.clearValue.depthStencil.depth = depth_attachment->clear_depth;
		if (!width || !height) {
			width  = depth_attachment->texture->width;
			height = depth_attachment->texture->height;
		}
	}

	VkRect2D render_area = {
		.extent.width  = width,
		.extent.height = height,
	};

	VkRenderingInfo render_info = {
		.sType                = VK_STRUCTURE_TYPE_RENDERING_INFO,
		.renderArea           = render_area,
		.layerCount           = 1,
		.colorAttachmentCount = color_count,
		.pColorAttachments    = vk_color,
		.pDepthAttachment     = &vk_depth,
	};

	vkCmdBeginRendering(vk_cb, &render_info);
}

void prRhiCmdEndRenderingVk(PrRhiCommandBuffer *cb) {
	vkCmdEndRendering(cb->handle);
}

// ============================================================================
// Dynamic state
// ============================================================================

void prRhiCmdSetViewportVk(PrRhiCommandBuffer *cb, f32 x, f32 y, f32 width, f32 height) {
	VkViewport vp = {
		 .x        = x,
		 .y        = y,
		 .width    = width,
		 .height   = height,
		 .minDepth = 0.0f,
		 .maxDepth = 1.0f 
	};
	vkCmdSetViewport(cb->handle, 0, 1, &vp);
}

void prRhiCmdSetScissorVk(PrRhiCommandBuffer *cb, i32 x, i32 y, u32 width, u32 height) {
	VkRect2D scissor = {
		.offset.x      = x,
		.offset.y      = y,
		.extent.width  = width,
		.extent.height = height,
	};
	vkCmdSetScissor(cb->handle, 0, 1, &scissor);
}

// ============================================================================
// Binding
// ============================================================================

void prRhiCmdBindPipelineVk(PrRhiCommandBuffer *cb, PrRhiPipelineBindPoint bind_point,
                            PrRhiPipeline *pipeline) {
	VkPipelineBindPoint vk_bp = (bind_point == PR_RHI_PIPELINE_BIND_POINT_COMPUTE)
		? VK_PIPELINE_BIND_POINT_COMPUTE : VK_PIPELINE_BIND_POINT_GRAPHICS;
	vkCmdBindPipeline(cb->handle, vk_bp, pipeline->handle);
}

void prRhiCmdBindDescriptorSetsVk(PrRhiCommandBuffer *cb, PrRhiPipelineBindPoint bind_point,
                                  PrRhiPipelineLayout *layout, u32 first_set,
                                  PrRhiDescriptorSetArray sets) {
	VkPipelineBindPoint vk_bp = (bind_point == PR_RHI_PIPELINE_BIND_POINT_COMPUTE)
		? VK_PIPELINE_BIND_POINT_COMPUTE : VK_PIPELINE_BIND_POINT_GRAPHICS;
	VkPipelineLayout vk_layout = layout ? layout->handle : VK_NULL_HANDLE;

	u32 set_count = sets ? (u32)wpArrayCount(sets) : 0;

	// NOTE (Abdelrahman): Commands run frequently and since we're using arena allocator, which means
	// that we would be allocating an array over and over. We could use a local arena allocator, but
	// that means we would pay the cost of creating and destorying it every frame. This is why we use
	// this while loop with a stack array instead, submitting a series of commands instead of one.
	while (set_count > 0) {
		VkDescriptorSetArray vk_sets = wpArrayWithCapacity(VkDescriptorSet, 16, WP_ARRAY_INIT_FILLED);
		u32 total_capacity           = (u32)wpArrayCapacity(vk_sets);
		u32 real_count = set_count < total_capacity ? set_count : total_capacity;
		for (u32 i = 0; i < real_count; ++i) {
			vk_sets[i] = sets[i]->handle;
		}

		vkCmdBindDescriptorSets(cb->handle, vk_bp, vk_layout, first_set, real_count, vk_sets, 0, NULL);

		set_count -= real_count;
		first_set += real_count;
	}
}

void prRhiCmdPushConstantsVk(PrRhiCommandBuffer *cb, PrRhiPipelineLayout *layout,
                             PrRhiShaderStage stage_flags, u32 offset, u32 size,
                             const void *data) {
	vkCmdPushConstants(cb->handle, layout ? layout->handle : VK_NULL_HANDLE,
	                   _toVkShaderStage(stage_flags), offset, size, data);
}

void prRhiCmdBindVertexBuffersVk(PrRhiCommandBuffer *cb, u32 first_binding, PrRhiBufferArray buffers,
                                 WpU64Array offsets) {
	u32 buf_count = buffers ? (u32)wpArrayCount(buffers) : 0;
	u32 off_count = offsets ? (u32)wpArrayCount(offsets) : 0;
	if (buf_count != off_count) { _abort("Mismatched buffer and offset count"); }

	u32 count = buf_count;

	while (count > 0) {
		VkBufferArray vk_bufs = wpArrayWithCapacity(VkBuffer, 16, WP_ARRAY_INIT_FILLED);
		u32 total_capacity    = (u32)wpArrayCapacity(vk_bufs);
		u32 real_count = count < total_capacity ? count : total_capacity;
		for (u32 i = 0; i < real_count; ++i) {
			vk_bufs[i] = buffers[i]->handle;
		}

		vkCmdBindVertexBuffers(cb->handle, first_binding, real_count, vk_bufs, (const VkDeviceSize *)offsets);

		count         -= real_count;
		first_binding += real_count;
	}
}

void prRhiCmdBindIndexBufferVk(PrRhiCommandBuffer *cb, PrRhiBuffer *buffer, u64 offset,
                               PrRhiIndexType index_type) {
	vkCmdBindIndexBuffer(cb->handle, buffer->handle, offset, _toVkIndexType(index_type));
}

// ============================================================================
// Draw calls
// ============================================================================

void prRhiCmdDrawVk(PrRhiCommandBuffer *cb, u32 vertex_count, u32 instance_count,
                    u32 first_vertex, u32 first_instance) {
	vkCmdDraw(cb->handle, vertex_count, instance_count, first_vertex, first_instance);
}

void prRhiCmdDrawIndexedVk(PrRhiCommandBuffer *cb, u32 index_count, u32 instance_count,
                           u32 first_index, i32 vertex_offset, u32 first_instance) {
	vkCmdDrawIndexed(cb->handle, index_count, instance_count, first_index, vertex_offset, first_instance);
}

// ============================================================================
// Copy
// ============================================================================

void prRhiCmdCopyBufferToImageVk(PrRhiCommandBuffer *cb, PrRhiBuffer *src, PrRhiTexture *dst,
                                 PrRhiBufferImageCopyArray copies) {
	u32 copy_count = (copies && wpArrayCount(copies) > 0) ? (u32)wpArrayCount(copies) : 0;
	if (copy_count == 0) { return; }

	u32 start_index = 0;

	while (copy_count > 0) {
		VkBufferImageCopyArray vk_regions = wpArrayWithCapacity(VkBufferImageCopy, 16, WP_ARRAY_INIT_FILLED);
		u32 total_capacity                = (u32)wpArrayCapacity(vk_regions);
		u32 real_count = copy_count < total_capacity ? copy_count : total_capacity;
		for (u32 j = start_index, i = j - start_index; i < real_count; ++i, ++j) {
			vk_regions[i].bufferOffset                    = copies[j].buffer_offset;
			vk_regions[i].bufferRowLength                 = 0;
			vk_regions[i].bufferImageHeight               = 0;
			vk_regions[i].imageSubresource.aspectMask     = VK_IMAGE_ASPECT_COLOR_BIT;
			vk_regions[i].imageSubresource.mipLevel       = copies[j].mip_level;
			vk_regions[i].imageSubresource.baseArrayLayer = 0;
			vk_regions[i].imageSubresource.layerCount     = 1;
			vk_regions[i].imageExtent.width               = copies[j].width;
			vk_regions[i].imageExtent.height              = copies[j].height;
			vk_regions[i].imageExtent.depth               = 1;
		}

		vkCmdCopyBufferToImage(cb->handle, src->handle, dst->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
		                       real_count, vk_regions);

		copy_count  -= real_count;
		start_index += real_count;
	}
}

// ============================================================================
// Queue submission
// ============================================================================

void prRhiQueueSubmitVk(PrRhiDevice *device, PrRhiCommandBuffer *cb, PrRhiSemaphore *wait_semaphore,
                        PrRhiSemaphore *signal_semaphore, PrRhiFence *fence) {
	VkCommandBuffer vk_cb = cb->handle;

	VkSemaphore wait_sems[1]            = {};
	VkPipelineStageFlags wait_stages[1] = { VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT };
	u32 wait_count                      = 0;
	if (wait_semaphore) {
		wait_sems[0] = wait_semaphore->handle;
		wait_count   = 1;
	}

	VkSemaphore signal_sems[1] = {};
	u32 signal_count = 0;
	if (signal_semaphore) {
		signal_sems[0] = signal_semaphore->handle;
		signal_count   = 1;
	}

	VkSubmitInfo submit = {
		.sType                = VK_STRUCTURE_TYPE_SUBMIT_INFO,
		.waitSemaphoreCount   = wait_count,
		.pWaitSemaphores      = wait_sems,
		.pWaitDstStageMask    = wait_stages,
		.commandBufferCount   = 1,
		.pCommandBuffers      = &vk_cb,
		.signalSemaphoreCount = signal_count,
		.pSignalSemaphores    = signal_sems,
	};

	_checkVk(vkQueueSubmit(device->queue, 1, &submit, fence ? fence->handle : VK_NULL_HANDLE),
	         "vkQueueSubmit");
}
