// 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 <volk/volk.h>
#include <vk_mem_alloc.h>
#include <string.h>

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

typedef VkPhysicalDevice           *VkPhysicalDeviceArray;
typedef VkQueueFamilyProperties2   *VkQueueFamilyProperties2Array;
typedef VkImage                    *VkImageArray;
typedef VkImageView                *VkImageViewArray;
typedef VkDescriptorSetLayout      *VkDescriptorSetLayoutArray;
typedef VkPushConstantRange        *VkPushConstantRangeArray;
typedef VkCommandBuffer            *VkCommandBufferArray;

// ============================================================================
// 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_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);
}

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

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

PrRhiInstance *prRhiCreateInstanceVk(PrRhiInstanceDesc desc, WpAllocator *alloc) {
	(void)desc;

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

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

	VkInstanceCreateInfo instance_info    = {};
	instance_info.sType                   = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
	instance_info.pApplicationInfo        = &app_info;

	if (desc.extra_extensions && wpArrayCount(desc.extra_extensions) > 0) {
		instance_info.enabledExtensionCount   = (u32)wpArrayCount(desc.extra_extensions);
		instance_info.ppEnabledExtensionNames = (const char *const *)desc.extra_extensions;
	}

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

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

	volkLoadInstance(vk_instance);

	PrRhiInstance *inst = wpMemAllocatorAlloc(alloc, sizeof(PrRhiInstance));
	if (!inst) _abort("alloc failed for PrRhiInstance");
	inst->handle          = vk_instance;
	inst->debug_messenger = NULL;

	return inst;
}

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

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

PrRhiPhysicalDeviceArray prRhiGetPhysicalDevicesVk(PrRhiInstance *inst, const WpAllocator *scratch) {
	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, scratch, 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 *, scratch, count, WP_ARRAY_INIT_NONE);
	if (!pdevs) _abort("alloc failed for PrRhiPhysicalDevice array");

	for (u32 i = 0; i < count; ++i) {
		PrRhiPhysicalDevice *pdev = (PrRhiPhysicalDevice *)wpMemAllocatorAlloc(scratch, sizeof(PrRhiPhysicalDevice));
		if (!pdev) _abort("alloc failed for PrRhiPhysicalDevice");
		pdev->handle   = vk_devices[i];
		pdev->instance = inst;
		wpArrayAppendCapped(PrRhiPhysicalDevice *, pdevs, &pdev);
	}

	return pdevs;
}

void prRhiGetPhysicalDeviceNameVk(PrRhiPhysicalDevice *pdev, WpStr8 *out) {
	VkPhysicalDeviceProperties2 props = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2 };
	vkGetPhysicalDeviceProperties2((VkPhysicalDevice)pdev->handle, &props);
	const char *name = props.properties.deviceName;
	out->buf      = (c8*)name;
	out->size     = strlen(name);
	out->capacity = 0;
}

void prRhiGetPhysicalDeviceDriverInfoVk(PrRhiPhysicalDevice *pdev, WpStr8 *out) {
	VkPhysicalDeviceDriverProperties driver_props = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES };
	VkPhysicalDeviceProperties2 props = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, .pNext = &driver_props };
	vkGetPhysicalDeviceProperties2((VkPhysicalDevice)pdev->handle, &props);
	const char *info = driver_props.driverInfo;
	out->buf      = (c8*)info;
	out->size     = strlen(info);
	out->capacity = 0;
}

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

PrRhiSurface *prRhiCreateSurfaceVk(PrRhiInstance *inst, void *window_handle, WpAllocator *alloc) {
	(void)inst;
	// window_handle is already a VkSurfaceKHR created by the caller (e.g. via SDL).
	PrRhiSurface *surface = wpMemAllocatorAlloc(alloc, sizeof(PrRhiSurface));
	if (!surface) _abort("alloc failed for PrRhiSurface");
	surface->handle = window_handle;
	return surface;
}

void prRhiDestroySurfaceVk(PrRhiInstance *inst, PrRhiSurface *surface, WpAllocator *alloc) {
	if (!surface) return;
	vkDestroySurfaceKHR((VkInstance)inst->handle, (VkSurfaceKHR)surface->handle, NULL);
	wpMemAllocatorFree(alloc, (void**)&surface, sizeof(PrRhiSurface));
}

PrRhiSurfaceCapabilities prRhiGetSurfaceCapabilitiesVk(PrRhiPhysicalDevice *pdev, PrRhiSurface *surface) {
	VkSurfaceCapabilitiesKHR caps = {};
	_checkVk(vkGetPhysicalDeviceSurfaceCapabilitiesKHR((VkPhysicalDevice)pdev->handle,
	                                                    (VkSurfaceKHR)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, WpAllocator *alloc) {
	VkInstance   vk_inst          = (VkInstance)pdev->instance->handle;
	VkPhysicalDevice vk_pdev      = (VkPhysicalDevice)pdev->handle;
	VkSurfaceKHR vk_surface       = (VkSurfaceKHR)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, alloc, 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
	VkBool32 present_supported = VK_FALSE;
	_checkVk(vkGetPhysicalDeviceSurfaceSupportKHR(vk_pdev, family_index, vk_surface, &present_supported),
	         "vkGetPhysicalDeviceSurfaceSupportKHR");
	if (!present_supported) _abort("no presentation support");

	wpArrayDealloc(VkQueueFamilyProperties2, alloc, &qf_props);

	// Check device profile support
	VkBool32 profile_supported = VK_FALSE;
	_checkVk(vpGetPhysicalDeviceProfileSupport(vk_inst, vk_pdev, &_profile, &profile_supported),
	         "vpGetPhysicalDeviceProfileSupport");
	if (!profile_supported) _abort("device does not support profile");

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

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

	VpDeviceCreateInfo vp_device_info = {};
	vp_device_info.pCreateInfo             = &device_info;
	vp_device_info.enabledFullProfileCount = 1;
	vp_device_info.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 = {};
	vk_functions.vkGetInstanceProcAddr = vkGetInstanceProcAddr;
	vk_functions.vkGetDeviceProcAddr   = vkGetDeviceProcAddr;
	vk_functions.vkCreateImage         = vkCreateImage;

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

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

	PrRhiDevice *device = wpMemAllocatorAlloc(alloc, 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, WpAllocator *alloc) {
	if (!device) return;
	vkDeviceWaitIdle((VkDevice)device->handle);
	vmaDestroyAllocator((VmaAllocator)device->allocator);
	vkDestroyDevice((VkDevice)device->handle, NULL);
	wpMemAllocatorFree(alloc, (void**)&device, sizeof(PrRhiDevice));
}

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

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

static PrRhiTexture *_createSwapchainTexture(PrRhiDevice *device, VkImage vk_image,
                                              VkFormat vk_format, u32 width, u32 height,
                                              VkImageView vk_view, WpAllocator *alloc) {
	PrRhiTexture *tex = wpMemAllocatorAlloc(alloc, 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 VkFormat candidates[] = { VK_FORMAT_D24_UNORM_S8_UINT, VK_FORMAT_D32_SFLOAT_S8_UINT };
	for (u32 i = 0; i < (u32)(sizeof(candidates) / sizeof(candidates[0])); ++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,
                                        WpAllocator *alloc) {
	VkDevice vk_device       = (VkDevice)device->handle;
	VkPhysicalDevice vk_pdev = (VkPhysicalDevice)device->physical_device;
	VkSurfaceKHR vk_surface  = (VkSurfaceKHR)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 = {};
	swapchain_info.sType            = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
	swapchain_info.surface          = vk_surface;
	swapchain_info.minImageCount    = caps.minImageCount;
	swapchain_info.imageFormat      = _default_image_format;
	swapchain_info.imageColorSpace  = _default_colorspace;
	swapchain_info.imageExtent      = extent;
	swapchain_info.imageArrayLayers = 1;
	swapchain_info.imageUsage       = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
	swapchain_info.preTransform     = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
	swapchain_info.compositeAlpha   = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
	swapchain_info.presentMode      = (VkPresentModeKHR)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, alloc, 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 *, alloc, 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       = {};
		view_info.sType                       = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
		view_info.image                       = vk_images[i];
		view_info.viewType                    = VK_IMAGE_VIEW_TYPE_2D;
		view_info.format                      = _default_image_format;
		view_info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
		view_info.subresourceRange.levelCount = 1;
		view_info.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, alloc);
		wpArrayAppendCapped(PrRhiTexture *, images, &tex);
	}

	wpArrayDealloc(VkImage, alloc, &vk_images);

	PrRhiSwapchain *swapchain = wpMemAllocatorAlloc(alloc, 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->width               = extent.width;
	swapchain->height              = extent.height;
	swapchain->current_image_index = 0;

	// Create depth texture if requested
	if (desc.has_depth) {
		VkFormat depth_fmt = _pickDepthFormat(vk_pdev);
		if (depth_fmt == VK_FORMAT_UNDEFINED) _abort("no suitable depth format");

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

		VmaAllocationCreateInfo depth_alloc_info = {};
		depth_alloc_info.flags  = VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT;
		depth_alloc_info.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       = {};
		depth_view_info.sType                       = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
		depth_view_info.image                       = depth_image;
		depth_view_info.viewType                    = VK_IMAGE_VIEW_TYPE_2D;
		depth_view_info.format                      = depth_fmt;
		depth_view_info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
		depth_view_info.subresourceRange.levelCount = 1;
		depth_view_info.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(alloc, 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_fmt;

		swapchain->depth = depth_tex;
	}

	return swapchain;
}

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

	VkDevice vk_device = (VkDevice)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, (VkImageView)tex->view, NULL);
		// VkImage is owned by swapchain — skip vmaDestroyImage
		wpMemAllocatorFree(alloc, (void**)&tex, sizeof(PrRhiTexture));
	}
	wpArrayDealloc(PrRhiTexture *, alloc, &swapchain->images);

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

	vkDestroySwapchainKHR(vk_device, (VkSwapchainKHR)swapchain->handle, NULL);
	wpMemAllocatorFree(alloc, (void**)&swapchain, sizeof(PrRhiSwapchain));
}

PrRhiSwapchainResult prRhiAcquireNextImageVk(PrRhiDevice *device, PrRhiSwapchain *swapchain,
                                               PrRhiSemaphore *signal_semaphore, u32 *out_image_index) {
	VkSemaphore vk_semaphore = signal_semaphore ? (VkSemaphore)signal_semaphore->handle : VK_NULL_HANDLE;
	VkResult res = vkAcquireNextImageKHR((VkDevice)device->handle,
	                                      (VkSwapchainKHR)swapchain->handle,
	                                      UINT64_MAX, vk_semaphore, VK_NULL_HANDLE,
	                                      &swapchain->current_image_index);
	if (res == VK_ERROR_OUT_OF_DATE_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 ? (VkSemaphore)wait_semaphore->handle : VK_NULL_HANDLE;

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

	VkResult res = vkQueuePresentKHR((VkQueue)device->queue, &present_info);
	if (res == VK_ERROR_OUT_OF_DATE_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, WpAllocator *alloc) {
	PrRhiSwapchain *old = *swapchain;
	if (!old) return;

	VkDevice vk_device       = (VkDevice)device->handle;
	VkPhysicalDevice vk_pdev = (VkPhysicalDevice)device->physical_device;

	VkExtent2D extent = { width, height };

	VkSwapchainCreateInfoKHR swapchain_info = {};
	swapchain_info.sType            = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
	swapchain_info.oldSwapchain     = (VkSwapchainKHR)old->handle;
	swapchain_info.surface          = (VkSurfaceKHR)((PrRhiSurface *)old->surface)->handle;
	swapchain_info.minImageCount    = old->image_count;
	swapchain_info.imageFormat      = (VkFormat)old->format;
	swapchain_info.imageColorSpace  = _default_colorspace;
	swapchain_info.imageExtent      = extent;
	swapchain_info.imageArrayLayers = 1;
	swapchain_info.imageUsage       = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
	swapchain_info.preTransform     = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
	swapchain_info.compositeAlpha   = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
	swapchain_info.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, (VkImageView)tex->view, NULL);
		wpMemAllocatorFree(alloc, (void**)&tex, sizeof(PrRhiTexture));
	}
	wpArrayDealloc(PrRhiTexture *, alloc, &old->images);

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

	// Destroy old swapchain
	vkDestroySwapchainKHR(vk_device, (VkSwapchainKHR)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, alloc, 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 *, alloc, 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       = {};
		view_info.sType                       = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
		view_info.image                       = vk_images[i];
		view_info.viewType                    = VK_IMAGE_VIEW_TYPE_2D;
		view_info.format                      = (VkFormat)old->format;
		view_info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
		view_info.subresourceRange.levelCount = 1;
		view_info.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], (VkFormat)old->format,
		                                              extent.width, extent.height, vk_view, alloc);
		wpArrayAppendCapped(PrRhiTexture *, new_images, &tex);
	}

	wpArrayDealloc(VkImage, alloc, &vk_images);

	// Recreate depth
	PrRhiTexture *new_depth = NULL;
	{
		VkFormat depth_fmt = _pickDepthFormat(vk_pdev);
		if (depth_fmt == VK_FORMAT_UNDEFINED) _abort("no suitable depth format (recreate)");

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

		VmaAllocationCreateInfo depth_alloc_info = {};
		depth_alloc_info.flags = VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT;
		depth_alloc_info.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 recreate)");

		VkImageViewCreateInfo depth_view_info       = {};
		depth_view_info.sType                       = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
		depth_view_info.image                       = depth_image;
		depth_view_info.viewType                    = VK_IMAGE_VIEW_TYPE_2D;
		depth_view_info.format                      = depth_fmt;
		depth_view_info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
		depth_view_info.subresourceRange.levelCount = 1;
		depth_view_info.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(alloc, 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;
	}

	// 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;
}

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

PrRhiBuffer *prRhiCreateBufferVk(PrRhiDevice *device, PrRhiBufferDesc desc,
                                  WpAllocator *alloc) {
	VkBufferCreateInfo buf_info = {};
	buf_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
	buf_info.size  = desc.size;
	buf_info.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 = {};
	alloc_info.flags = _toVmaFlags(desc.memory);
	alloc_info.usage = VMA_MEMORY_USAGE_AUTO;

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

	PrRhiBuffer *buffer = wpMemAllocatorAlloc(alloc, sizeof(PrRhiBuffer));
	if (!buffer) _abort("alloc failed for PrRhiBuffer");
	buffer->handle         = vk_buffer;
	buffer->allocation     = vma_alloc;
	buffer->size           = desc.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 = {};
		addr_info.sType  = VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO;
		addr_info.buffer = vk_buffer;
		buffer->device_address = vkGetBufferDeviceAddress((VkDevice)device->handle, &addr_info);
	}

	return buffer;
}

void prRhiDestroyBufferVk(PrRhiDevice *device, PrRhiBuffer *buffer, WpAllocator *alloc) {
	if (!buffer) return;
	vmaDestroyBuffer((VmaAllocator)device->allocator, (VkBuffer)buffer->handle,
	                 (VmaAllocation)buffer->allocation);
	wpMemAllocatorFree(alloc, (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,
                                    WpAllocator *alloc) {
	VkImageCreateInfo img_info           = {};
	img_info.sType                       = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
	img_info.imageType                   = VK_IMAGE_TYPE_2D;
	img_info.format                      = _toVkFormat(desc.format);
	img_info.extent.width                = desc.width;
	img_info.extent.height               = desc.height;
	img_info.extent.depth                = 1;
	img_info.mipLevels                   = desc.mip_levels;
	img_info.arrayLayers                 = 1;
	img_info.samples                     = VK_SAMPLE_COUNT_1_BIT;
	img_info.tiling                      = VK_IMAGE_TILING_OPTIMAL;
	img_info.usage                       = _toVkTextureUsage(desc.usage);
	img_info.initialLayout               = VK_IMAGE_LAYOUT_UNDEFINED;

	VmaAllocationCreateInfo alloc_info = {};
	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       = {};
	view_info.sType                       = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
	view_info.image                       = vk_image;
	view_info.viewType                    = VK_IMAGE_VIEW_TYPE_2D;
	view_info.format                      = _toVkFormat(desc.format);
	view_info.subresourceRange.aspectMask = aspect_mask;
	view_info.subresourceRange.levelCount = desc.mip_levels;
	view_info.subresourceRange.layerCount = 1;

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

	PrRhiTexture *texture = wpMemAllocatorAlloc(alloc, 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;
}

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

	vkDestroyImageView(vk_device, (VkImageView)texture->view, NULL);
	if (texture->allocation) {
		vmaDestroyImage((VmaAllocator)device->allocator, (VkImage)texture->image,
		                (VmaAllocation)texture->allocation);
	}
	wpMemAllocatorFree(alloc, (void**)&texture, sizeof(PrRhiTexture));
}

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

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

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

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

void prRhiDestroySamplerVk(PrRhiDevice *device, PrRhiSampler *sampler, WpAllocator *alloc) {
	if (!sampler) return;
	vkDestroySampler((VkDevice)device->handle, (VkSampler)sampler->handle, NULL);
	wpMemAllocatorFree(alloc, (void**)&sampler, sizeof(PrRhiSampler));
}

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

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

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

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

void prRhiDestroyShaderVk(PrRhiDevice *device, PrRhiShader *shader, WpAllocator *alloc) {
	if (!shader) return;
	vkDestroyShaderModule((VkDevice)device->handle, (VkShaderModule)shader->handle, NULL);
	wpMemAllocatorFree(alloc, (void**)&shader, sizeof(PrRhiShader));
}

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

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

	// Gather descriptor set layouts
	u32 layout_count = (desc.set_layouts && wpArrayCount(desc.set_layouts) > 0)
		? (u32)wpArrayCount(desc.set_layouts) : 0;
	VkDescriptorSetLayout vk_layouts_stack[8];
	VkDescriptorSetLayout *vk_layouts = vk_layouts_stack;

	if (layout_count > 8) {
		vk_layouts = wpArrayAllocCapacity(VkDescriptorSetLayout, alloc, layout_count, WP_ARRAY_INIT_NONE);
		if (!vk_layouts) _abort("alloc failed for VkDescriptorSetLayout array");
	}
	for (u32 i = 0; i < layout_count; ++i) {
		vk_layouts[i] = (VkDescriptorSetLayout)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;
	VkPushConstantRange pc_ranges_stack[8];
	VkPushConstantRange *pc_ranges = pc_ranges_stack;

	if (pc_count > 8) {
		pc_ranges = wpArrayAllocCapacity(VkPushConstantRange, alloc, 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 = {};
	pl_info.sType                  = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
	pl_info.setLayoutCount         = layout_count;
	pl_info.pSetLayouts            = vk_layouts;
	pl_info.pushConstantRangeCount = pc_count;
	pl_info.pPushConstantRanges    = pc_ranges;

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

	if (layout_count > 8) {
		wpArrayDealloc(VkDescriptorSetLayout, alloc, &vk_layouts);
	}
	if (pc_count > 8) {
		wpArrayDealloc(VkPushConstantRange, alloc, &pc_ranges);
	}

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

void prRhiDestroyPipelineLayoutVk(PrRhiDevice *device, PrRhiPipelineLayout *layout,
                                   WpAllocator *alloc) {
	if (!layout) return;
	vkDestroyPipelineLayout((VkDevice)device->handle, (VkPipelineLayout)layout->handle, NULL);
	wpMemAllocatorFree(alloc, (void**)&layout, sizeof(PrRhiPipelineLayout));
}

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

PrRhiPipeline *prRhiCreateGraphicsPipelineVk(PrRhiDevice *device,
                                               PrRhiGraphicsPipelineDesc desc,
                                               WpAllocator *alloc) {
	VkDevice vk_device = (VkDevice)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 = (VkShaderModule)desc.vertex_shader->handle;
		stages[stage_count].pName  = "main";
		++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 = (VkShaderModule)desc.fragment_shader->handle;
		stages[stage_count].pName  = "main";
		++stage_count;
	}

	// Vertex input state
	VkVertexInputBindingDescription vk_bindings[8];
	u32 binding_count = 0;
	if (desc.vertex_bindings && wpArrayCount(desc.vertex_bindings) > 0) {
		binding_count = (u32)wpArrayCount(desc.vertex_bindings);
		for (u32 i = 0; i < binding_count && i < 8; ++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;
		}
	}

	VkVertexInputAttributeDescription vk_attrs[16];
	u32 attr_count = 0;
	if (desc.vertex_attributes && wpArrayCount(desc.vertex_attributes) > 0) {
		attr_count = (u32)wpArrayCount(desc.vertex_attributes);
		for (u32 i = 0; i < attr_count && i < 16; ++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 = {};
	vertex_input.sType                           = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
	vertex_input.vertexBindingDescriptionCount   = binding_count;
	vertex_input.pVertexBindingDescriptions      = vk_bindings;
	vertex_input.vertexAttributeDescriptionCount = attr_count;
	vertex_input.pVertexAttributeDescriptions    = vk_attrs;

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

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

	VkDynamicState dynamic_states_stack[2];
	u32 dynamic_count = 0;
	if (desc.dynamic_viewport) {
		dynamic_states_stack[dynamic_count++] = VK_DYNAMIC_STATE_VIEWPORT;
	}
	if (desc.dynamic_scissor) {
		dynamic_states_stack[dynamic_count++] = VK_DYNAMIC_STATE_SCISSOR;
	}

	VkPipelineDynamicStateCreateInfo dynamic_state = {};
	dynamic_state.sType             = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
	dynamic_state.dynamicStateCount = dynamic_count;
	dynamic_state.pDynamicStates    = dynamic_states_stack;

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

	// Dynamic rendering
	VkPipelineRenderingCreateInfo rendering_info = {};
	rendering_info.sType               = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO;
	rendering_info.depthAttachmentFormat = _toVkFormat(desc.depth_attachment_format);

	VkFormat vk_color_formats[8];
	u32 color_format_count = 0;
	if (desc.color_attachment_formats && wpArrayCount(desc.color_attachment_formats) > 0) {
		color_format_count = (u32)wpArrayCount(desc.color_attachment_formats);
		for (u32 i = 0; i < color_format_count && i < 8; ++i) {
			vk_color_formats[i] = _toVkFormat(desc.color_attachment_formats[i]);
		}
	}
	rendering_info.colorAttachmentCount = color_format_count;
	rendering_info.pColorAttachmentFormats = vk_color_formats;

	// Blending
	VkPipelineColorBlendAttachmentState blend_attachments[8];
	u32 blend_count = 0;
	if (desc.blend_attachments && wpArrayCount(desc.blend_attachments) > 0) {
		blend_count = (u32)wpArrayCount(desc.blend_attachments);
		for (u32 i = 0; i < blend_count && i < 8; ++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 = {};
	blend_state.sType           = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
	blend_state.attachmentCount = blend_count;
	blend_state.pAttachments    = blend_attachments;

	// Rasterization
	VkPipelineRasterizationStateCreateInfo raster = {};
	raster.sType       = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
	raster.lineWidth   = 1.0f;
	raster.cullMode    = VK_CULL_MODE_BACK_BIT;
	raster.frontFace   = VK_FRONT_FACE_COUNTER_CLOCKWISE;

	// Multisampling
	VkPipelineMultisampleStateCreateInfo multisample = {};
	multisample.sType                = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
	multisample.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;

	// Create pipeline
	VkGraphicsPipelineCreateInfo pi = {};
	pi.sType               = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
	pi.pNext               = &rendering_info;
	pi.stageCount          = stage_count;
	pi.pStages             = stages;
	pi.pVertexInputState   = &vertex_input;
	pi.pInputAssemblyState = &input_assembly;
	pi.pViewportState      = &viewport_state;
	pi.pRasterizationState = &raster;
	pi.pMultisampleState   = &multisample;
	pi.pDepthStencilState  = &depth_stencil;
	pi.pColorBlendState    = &blend_state;
	pi.pDynamicState       = &dynamic_state;
	pi.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(alloc, sizeof(PrRhiPipeline));
	if (!pipeline) _abort("alloc failed for PrRhiPipeline");
	pipeline->handle = vk_pipeline;
	return pipeline;
}

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

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

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

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

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

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

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

	VkDescriptorSetLayoutBinding vk_bindings[16];
	VkDescriptorBindingFlags vk_flags[16];

	for (u32 i = 0; i < binding_count && i < 16; ++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 = {};
	flags_info.sType         = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_BINDING_FLAGS_CREATE_INFO;
	flags_info.bindingCount  = binding_count;
	flags_info.pBindingFlags = vk_flags;

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

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

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

void prRhiDestroyDescriptorSetLayoutVk(PrRhiDevice *device, PrRhiDescriptorSetLayout *layout,
                                        WpAllocator *alloc) {
	if (!layout) return;
	vkDestroyDescriptorSetLayout((VkDevice)device->handle, (VkDescriptorSetLayout)layout->handle, NULL);
	wpMemAllocatorFree(alloc, (void**)&layout, sizeof(PrRhiDescriptorSetLayout));
}

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

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

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

	VkDescriptorPoolSize vk_sizes[8];
	for (u32 i = 0; i < pool_size_count && i < 8; ++i) {
		vk_sizes[i].type            = _toVkDescriptorType(desc.pool_sizes[i].type);
		vk_sizes[i].descriptorCount = desc.pool_sizes[i].descriptor_count;
	}

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

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

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

void prRhiDestroyDescriptorPoolVk(PrRhiDevice *device, PrRhiDescriptorPool *pool,
                                   WpAllocator *alloc) {
	if (!pool) return;
	vkDestroyDescriptorPool((VkDevice)device->handle, (VkDescriptorPool)pool->handle, NULL);
	wpMemAllocatorFree(alloc, (void**)&pool, sizeof(PrRhiDescriptorPool));
}

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

PrRhiDescriptorSet *prRhiAllocateDescriptorSetVk(PrRhiDevice *device,
                                                   PrRhiDescriptorPool *pool,
                                                   PrRhiDescriptorSetLayout *layout,
                                                   u32 variable_count, WpAllocator *alloc) {
	VkDevice vk_device = (VkDevice)device->handle;

	VkDescriptorSetVariableDescriptorCountAllocateInfo var_info = {};
	var_info.sType              = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_VARIABLE_DESCRIPTOR_COUNT_ALLOCATE_INFO;
	var_info.descriptorSetCount = 1;
	var_info.pDescriptorCounts  = &variable_count;

	VkDescriptorSetLayout vk_layout = (VkDescriptorSetLayout)layout->handle;

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

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

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

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

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

	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);
			VkDescriptorImageInfo vk_img_info[16];
			u32 img_max = img_count > 16 ? 16 : img_count;
			for (u32 j = 0; j < img_max; ++j) {
				PrRhiDescriptorImageInfo *src = &w->image_info[j];
				vk_img_info[j].sampler     = src->sampler ? (VkSampler)src->sampler->handle : VK_NULL_HANDLE;
				vk_img_info[j].imageView   = src->texture ? (VkImageView)src->texture->view : VK_NULL_HANDLE;
				vk_img_info[j].imageLayout = _toVkImageLayout(src->layout);
			}

			VkWriteDescriptorSet vk_write = {};
			vk_write.sType           = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
			vk_write.dstSet          = (VkDescriptorSet)w->dst_set->handle;
			vk_write.dstBinding      = w->dst_binding;
			vk_write.dstArrayElement = w->dst_array_element;
			vk_write.descriptorCount = img_max;
			vk_write.descriptorType  = vk_type;
			vk_write.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);
			VkDescriptorBufferInfo vk_buf_info[16];
			u32 buf_max = buf_count > 16 ? 16 : buf_count;
			for (u32 j = 0; j < buf_max; ++j) {
				PrRhiDescriptorBufferInfo *src = &w->buffer_info[j];
				vk_buf_info[j].buffer = src->buffer ? (VkBuffer)src->buffer->handle : VK_NULL_HANDLE;
				vk_buf_info[j].offset = src->offset;
				vk_buf_info[j].range  = src->range;
			}

			VkWriteDescriptorSet vk_write = {};
			vk_write.sType           = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
			vk_write.dstSet          = (VkDescriptorSet)w->dst_set->handle;
			vk_write.dstBinding      = w->dst_binding;
			vk_write.dstArrayElement = w->dst_array_element;
			vk_write.descriptorCount = buf_max;
			vk_write.descriptorType  = vk_type;
			vk_write.pBufferInfo     = vk_buf_info;
			vkUpdateDescriptorSets(vk_device, 1, &vk_write, 0, NULL);
		}
	}
}

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

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

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

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

void prRhiDestroyFenceVk(PrRhiDevice *device, PrRhiFence *fence, WpAllocator *alloc) {
	if (!fence) return;
	vkDestroyFence((VkDevice)device->handle, (VkFence)fence->handle, NULL);
	wpMemAllocatorFree(alloc, (void**)&fence, sizeof(PrRhiFence));
}

void prRhiWaitForFencesVk(PrRhiDevice *device, PrRhiFenceArray fences, u32 count,
                           b8 wait_all, u64 timeout_ns) {
	VkFence vk_fences[16];
	u32 real_count = count < 16 ? count : 16;
	for (u32 i = 0; i < real_count; ++i) {
		vk_fences[i] = (VkFence)fences[i]->handle;
	}
	_checkVk(vkWaitForFences((VkDevice)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[16];
	u32 real_count = count < 16 ? count : 16;
	for (u32 i = 0; i < real_count; ++i) {
		vk_fences[i] = (VkFence)fences[i]->handle;
	}
	_checkVk(vkResetFences((VkDevice)device->handle, real_count, vk_fences),
	         "vkResetFences");
}

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

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

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

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

void prRhiDestroySemaphoreVk(PrRhiDevice *device, PrRhiSemaphore *semaphore, WpAllocator *alloc) {
	if (!semaphore) return;
	vkDestroySemaphore((VkDevice)device->handle, (VkSemaphore)semaphore->handle, NULL);
	wpMemAllocatorFree(alloc, (void**)&semaphore, sizeof(PrRhiSemaphore));
}

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

PrRhiCommandPool *prRhiCreateCommandPoolVk(PrRhiDevice *device, PrRhiCommandPoolDesc desc,
                                            WpAllocator *alloc) {
	VkCommandPoolCreateInfo info = {};
	info.sType            = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
	info.flags            = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
	info.queueFamilyIndex = desc.queue_family_index;

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

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

void prRhiDestroyCommandPoolVk(PrRhiDevice *device, PrRhiCommandPool *pool, WpAllocator *alloc) {
	if (!pool) return;
	vkDestroyCommandPool((VkDevice)device->handle, (VkCommandPool)pool->handle, NULL);
	wpMemAllocatorFree(alloc, (void**)&pool, sizeof(PrRhiCommandPool));
}

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

	VkCommandBuffer vk_cbs[16];
	u32 real_count = count < 16 ? count : 16;

	VkCommandBufferArray vk_cb_array = wpArrayAllocCapacity(VkCommandBuffer, alloc, real_count, WP_ARRAY_INIT_NONE);
	if (!vk_cb_array) _abort("alloc failed for VkCommandBuffer array");

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

	for (u32 i = 0; i < real_count; ++i) {
		wpArrayAppendCapped(VkCommandBuffer, vk_cb_array, &vk_cbs[i]);
	}

	PrRhiCommandBufferArray cbs = wpArrayAllocCapacity(PrRhiCommandBuffer *, alloc, real_count, WP_ARRAY_INIT_NONE);
	if (!cbs) _abort("alloc failed for PrRhiCommandBuffer array");

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

	wpArrayDealloc(VkCommandBuffer, alloc, &vk_cb_array);

	return cbs;
}

void prRhiFreeCommandBuffersVk(PrRhiDevice *device, PrRhiCommandPool *pool,
                                u32 count, PrRhiCommandBufferArray buffers) {
	if (!buffers || count == 0) return;

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

	VkCommandBuffer vk_cbs[16];
	u32 real_count = count < 16 ? count : 16;
	for (u32 i = 0; i < real_count; ++i) {
		vk_cbs[i] = (VkCommandBuffer)buffers[i]->handle;
	}

	vkFreeCommandBuffers(vk_device, vk_pool, real_count, vk_cbs);

	// Free PrRhiCommandBuffer structs
	for (u32 i = 0; i < real_count; ++i) {
		// These were allocated from the same allocator
		// Since they're in an array, we need root allocator — skip, caller handles
	}
}

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

void prRhiBeginCommandBufferVk(PrRhiCommandBuffer *cb) {
	VkCommandBufferBeginInfo info = {};
	info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
	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];
	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;
		}

		VkPipelineStageFlags2 src_stage = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT;
		VkPipelineStageFlags2 dst_stage = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT;
		VkAccessFlags2 src_access = 0;
		VkAccessFlags2 dst_access = 0;

		vk_img_barriers[i].sType                           = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2;
		vk_img_barriers[i].srcStageMask                    = src_stage;
		vk_img_barriers[i].srcAccessMask                   = src_access;
		vk_img_barriers[i].dstStageMask                    = dst_stage;
		vk_img_barriers[i].dstAccessMask                   = dst_access;
		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];
	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  = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT;
		vk_buf_barriers[i].srcAccessMask = 0;
		vk_buf_barriers[i].dstStageMask  = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT;
		vk_buf_barriers[i].dstAccessMask = 0;
		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 = {};
	dep.sType                    = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
	dep.imageMemoryBarrierCount  = img_count;
	dep.pImageMemoryBarriers     = vk_img_barriers;
	dep.bufferMemoryBarrierCount = buf_count;
	dep.pBufferMemoryBarriers    = vk_buf_barriers;

	vkCmdPipelineBarrier2(vk_cb, &dep);
}

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

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

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

	VkRenderingAttachmentInfo vk_color[8];
	u32 width = 0, height = 0;

	for (u32 i = 0; i < color_count && i < 8; ++i) {
		PrRhiColorAttachment *src = &color_attachments[i];
		vk_color[i].sType       = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO;
		vk_color[i].imageView   = src->texture ? (VkImageView)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 = {};
	VkRenderingAttachmentInfo *p_depth = NULL;
	if (depth_attachment && depth_attachment->texture) {
		p_depth = &vk_depth;
		vk_depth.sType       = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO;
		vk_depth.imageView   = (VkImageView)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 = {};
	render_area.extent.width  = width;
	render_area.extent.height = height;

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

	vkCmdBeginRendering(vk_cb, &render_info);
}

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

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

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

void prRhiCmdSetScissorVk(PrRhiCommandBuffer *cb, i32 x, i32 y, u32 width, u32 height) {
	VkRect2D scissor = { { x, y }, { width, height } };
	vkCmdSetScissor((VkCommandBuffer)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((VkCommandBuffer)cb->handle, vk_bp, (VkPipeline)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 ? (VkPipelineLayout)layout->handle : VK_NULL_HANDLE;

	u32 set_count = sets ? (u32)wpArrayCount(sets) : 0;
	VkDescriptorSet vk_sets[16];
	u32 real_count = set_count < 16 ? set_count : 16;
	for (u32 i = 0; i < real_count; ++i) {
		vk_sets[i] = (VkDescriptorSet)sets[i]->handle;
	}

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

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

void prRhiCmdBindVertexBuffersVk(PrRhiCommandBuffer *cb, u32 first_binding,
                                  PrRhiBufferArray buffers, const u64 *offsets, u32 count) {
	VkBuffer vk_bufs[16];
	u32 real_count = count < 16 ? count : 16;
	for (u32 i = 0; i < real_count; ++i) {
		vk_bufs[i] = (VkBuffer)buffers[i]->handle;
	}
	vkCmdBindVertexBuffers((VkCommandBuffer)cb->handle, first_binding,
	                        real_count, vk_bufs, (const VkDeviceSize *)offsets);
}

void prRhiCmdBindIndexBufferVk(PrRhiCommandBuffer *cb, PrRhiBuffer *buffer, u64 offset,
                                PrRhiIndexType index_type) {
	vkCmdBindIndexBuffer((VkCommandBuffer)cb->handle,
	                      (VkBuffer)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((VkCommandBuffer)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((VkCommandBuffer)cb->handle, index_count, instance_count,
	                  first_index, vertex_offset, first_instance);
}

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

void prRhiCmdCopyBufferToImageVk(PrRhiCommandBuffer *cb, PrRhiBuffer *src, PrRhiTexture *dst) {
	VkBufferImageCopy region = {};
	region.bufferOffset                    = 0;
	region.bufferRowLength                 = 0;
	region.bufferImageHeight               = 0;
	region.imageSubresource.aspectMask     = VK_IMAGE_ASPECT_COLOR_BIT;
	region.imageSubresource.mipLevel       = 0;
	region.imageSubresource.layerCount     = 1;
	region.imageExtent.width               = dst->width;
	region.imageExtent.height              = dst->height;
	region.imageExtent.depth               = 1;

	vkCmdCopyBufferToImage((VkCommandBuffer)cb->handle,
	                        (VkBuffer)src->handle,
	                        (VkImage)dst->image,
	                        VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
	                        1, &region);
}

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

void prRhiQueueSubmitVk(PrRhiDevice *device, PrRhiCommandBuffer *cb,
                         PrRhiSemaphore *wait_semaphore,
                         PrRhiSemaphore *signal_semaphore, PrRhiFence *fence) {
	VkCommandBuffer vk_cb = (VkCommandBuffer)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] = (VkSemaphore)wait_semaphore->handle;
		wait_count   = 1;
	}

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

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

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