Harbor

branch main
showing the latest snapshot on main
vk_ops.odin 43.4 KB · Plain text
backend/vulkan/vk_ops.odin 0644 Raw
package vk_backend

import bk ".."
import gpu "../../core"
import "../../pipeline"
import "core:log"
import "core:mem"
import vk "vendor:vulkan"

active_pool_entry_vk :: proc(pool: ^[$N]$E, handle: $T) -> (^E, bool) {
	idx, ok := bk.handle_index(handle, N)
	if !ok do return nil, false
	entry := &pool[idx]
	if !entry.active do return nil, false
	return entry, true
}

buffer_entry :: proc(handle: bk.Buffer_Handle) -> (^Vk_Buffer_Entry, bool) {
	if g_vk == nil do return nil, false
	return active_pool_entry_vk(&g_vk.buffers, handle)
}

texture_entry :: proc(handle: bk.Texture_Handle) -> (^Vk_Texture_Entry, bool) {
	if g_vk == nil do return nil, false
	return active_pool_entry_vk(&g_vk.textures, handle)
}

shader_entry :: proc(handle: bk.Shader_Handle) -> (^Vk_Shader_Entry, bool) {
	if g_vk == nil do return nil, false
	return active_pool_entry_vk(&g_vk.shaders, handle)
}

pipeline_entry :: proc(handle: bk.Pipeline_Handle) -> (^Vk_Pipeline_Entry, bool) {
	if g_vk == nil do return nil, false
	return active_pool_entry_vk(&g_vk.pipelines, handle)
}

descriptor_entry :: proc(handle: bk.Descriptor_Handle) -> (^Vk_Descriptor_Entry, bool) {
	if g_vk == nil do return nil, false
	return active_pool_entry_vk(&g_vk.descriptors, handle)
}

descriptor_entry_of_kind :: proc(
	handle: bk.Descriptor_Handle,
	kind: Descriptor_Kind,
) -> (
	^Vk_Descriptor_Entry,
	bool,
) {
	entry, ok := descriptor_entry(handle)
	if !ok || entry.kind != kind do return nil, false
	return entry, true
}

render_pass_entry :: proc(handle: bk.Render_Pass_Handle) -> (^Vk_Render_Pass_Entry, bool) {
	if g_vk == nil do return nil, false
	return active_pool_entry_vk(&g_vk.render_passes, handle)
}

framebuffer_entry :: proc(handle: bk.Framebuffer_Handle) -> (^Vk_Framebuffer_Entry, bool) {
	if g_vk == nil do return nil, false
	return active_pool_entry_vk(&g_vk.framebuffers, handle)
}

sampler_entry :: proc(handle: bk.Sampler_Handle) -> (^Vk_Sampler_Entry, bool) {
	if g_vk == nil do return nil, false
	return active_pool_entry_vk(&g_vk.samplers, handle)
}

// ============================================================================
// Render pass commands
// ============================================================================

begin_render_pass_vk :: proc(ctx: bk.Frame_Context, desc: bk.Render_Pass_Begin_Desc) {
	cmd := get_cmd(ctx)
	pass, pass_ok := render_pass_entry(desc.pass)
	framebuffer, framebuffer_ok := framebuffer_entry(desc.framebuffer)
	if !pass_ok || !framebuffer_ok do return
	pass_desc := pass.desc

	clear_values: [bk.MAX_COLOR_TARGETS + 1]vk.ClearValue
	clear_count: u32
	if pass_desc.has_color {
		color_count := desc.color_count
		if color_count == 0 {
			color_count = pass_desc.color_count
		}
		if color_count == 0 {
			color_count = 1
		}
		for i in 0..<color_count {
			clear_values[clear_count].color.float32 =
				desc.clear_colors[i] if desc.color_count > 0 else desc.clear_color
			clear_count += 1
		}
	}
	if pass_desc.has_depth {
		clear_values[clear_count].depthStencil = {
			depth   = desc.clear_depth,
			stencil = u32(desc.clear_stencil),
		}
		clear_count += 1
	}

	rp_info := vk.RenderPassBeginInfo {
		sType = .RENDER_PASS_BEGIN_INFO,
		renderPass = pass.handle,
		framebuffer = framebuffer.handle,
		renderArea = {offset = {0, 0}, extent = {desc.width, desc.height}},
		clearValueCount = clear_count,
		pClearValues = &clear_values[0],
	}

	vk.CmdBeginRenderPass(cmd, &rp_info, .INLINE)
}

begin_default_pass_vk :: proc(ctx: bk.Frame_Context, clear_color: [4]f32) {
	cmd := get_cmd(ctx)
	sc := &g_vk.swapchain

	clear_values: [2]vk.ClearValue
	clear_values[0].color.float32 = clear_color
	clear_values[1].depthStencil = {
		depth   = 1.0,
		stencil = 0,
	}

	rp_info := vk.RenderPassBeginInfo {
		sType = .RENDER_PASS_BEGIN_INFO,
		renderPass = sc.render_pass,
		framebuffer = sc.framebuffers[g_vk.image_index],
		renderArea = {offset = {0, 0}, extent = sc.extent},
		clearValueCount = 2,
		pClearValues = &clear_values[0],
	}

	vk.CmdBeginRenderPass(cmd, &rp_info, .INLINE)

	// Set default viewport and scissor
	viewport := vk.Viewport {
		x        = 0,
		y        = 0,
		width    = f32(sc.extent.width),
		height   = f32(sc.extent.height),
		minDepth = 0,
		maxDepth = 1,
	}
	vk.CmdSetViewport(cmd, 0, 1, &viewport)

	scissor := vk.Rect2D {
		offset = {0, 0},
		extent = sc.extent,
	}
	vk.CmdSetScissor(cmd, 0, 1, &scissor)
}

end_render_pass_vk :: proc(ctx: bk.Frame_Context) {
	vk.CmdEndRenderPass(get_cmd(ctx))
}

set_viewport_vk :: proc(ctx: bk.Frame_Context, x, y, w, h: f32) {
	viewport := vk.Viewport {
		x        = x,
		y        = y,
		width    = w,
		height   = h,
		minDepth = 0,
		maxDepth = 1,
	}
	vk.CmdSetViewport(get_cmd(ctx), 0, 1, &viewport)
}

set_scissor_vk :: proc(ctx: bk.Frame_Context, x, y: i32, w, h: u32) {
	scissor := vk.Rect2D {
		offset = {x, y},
		extent = {w, h},
	}
	vk.CmdSetScissor(get_cmd(ctx), 0, 1, &scissor)
}

set_depth_bias_vk :: proc(ctx: bk.Frame_Context, constant, slope: f32) {
	vk.CmdSetDepthBias(get_cmd(ctx), constant, 0, slope)
}

// ============================================================================
// Draw commands
// ============================================================================

draw_vk :: proc(
	ctx: bk.Frame_Context,
	vertex_count, instance_count: u32,
	first_vertex: u32,
	first_instance: u32,
) {
	vk.CmdDraw(get_cmd(ctx), vertex_count, instance_count, first_vertex, first_instance)
}

draw_indexed_vk :: proc(
	ctx: bk.Frame_Context,
	index_count, instance_count: u32,
	first_index: u32,
	vertex_offset: i32,
	first_instance: u32,
) {
	vk.CmdDrawIndexed(
		get_cmd(ctx),
		index_count,
		instance_count,
		first_index,
		vertex_offset,
		first_instance,
	)
}

draw_indirect_vk :: proc(
	ctx: bk.Frame_Context,
	argument_buffer: bk.Buffer_Handle,
	argument_offset: u64,
	draw_count: u32,
	stride: u32,
) {
	if draw_count != 1 {
		log.error("gpu/vulkan: draw_indirect currently supports one command")
		return
	}
	entry, ok := buffer_entry(argument_buffer)
	if !ok {
		log.error("gpu/vulkan: draw_indirect received invalid argument buffer")
		return
	}
	vk.CmdDrawIndirect(get_cmd(ctx), entry.buffer.buffer, vk.DeviceSize(argument_offset), draw_count, stride)
}

draw_indexed_indirect_vk :: proc(
	ctx: bk.Frame_Context,
	argument_buffer: bk.Buffer_Handle,
	argument_offset: u64,
	draw_count: u32,
	stride: u32,
) {
	if draw_count != 1 {
		log.error("gpu/vulkan: draw_indexed_indirect currently supports one command")
		return
	}
	entry, ok := buffer_entry(argument_buffer)
	if !ok {
		log.error("gpu/vulkan: draw_indexed_indirect received invalid argument buffer")
		return
	}
	vk.CmdDrawIndexedIndirect(get_cmd(ctx), entry.buffer.buffer, vk.DeviceSize(argument_offset), draw_count, stride)
}

// ============================================================================
// Compute commands
// ============================================================================

dispatch_compute_vk :: proc(ctx: bk.Frame_Context, groups_x, groups_y, groups_z: u32) {
	vk.CmdDispatch(get_cmd(ctx), groups_x, groups_y, groups_z)
}

compute_barrier_vk :: proc(ctx: bk.Frame_Context) {
	barrier := vk.MemoryBarrier {
		sType         = .MEMORY_BARRIER,
		srcAccessMask = {.SHADER_WRITE},
		dstAccessMask = {
			.SHADER_READ,
			.SHADER_WRITE,
			.VERTEX_ATTRIBUTE_READ,
			.INDEX_READ,
			.INDIRECT_COMMAND_READ,
		},
	}
	vk.CmdPipelineBarrier(
		get_cmd(ctx),
		{.COMPUTE_SHADER},
		{.DRAW_INDIRECT, .VERTEX_INPUT, .VERTEX_SHADER, .FRAGMENT_SHADER, .COMPUTE_SHADER},
		{},
		1,
		&barrier,
		0,
		nil,
		0,
		nil,
	)
}

// ============================================================================
// Buffer operations
// ============================================================================

create_buffer_vk :: proc(desc: bk.Buffer_Desc) -> (bk.Buffer_Handle, bool) {
	handle, ok := alloc_buffer_handle()
	if !ok {return bk.NULL_BUFFER, false}

	buf, buf_ok := gpu.create_buffer(
		&g_vk.device,
		vk.DeviceSize(desc.size),
		to_vk_buffer_usage(desc.usage),
		to_vk_memory_properties(desc.memory),
	)
	if !buf_ok {
		return bk.NULL_BUFFER, false
	}

	g_vk.buffers[handle].buffer = buf
	g_vk.buffers[handle].memory = desc.memory
	g_vk.buffers[handle].active = true
	return handle, true
}

create_buffer_staged_vk :: proc(
	data: rawptr,
	size: int,
	usage: bk.Buffer_Usage_Flags,
) -> (
	bk.Buffer_Handle,
	bool,
) {
	handle, ok := alloc_buffer_handle()
	if !ok {return bk.NULL_BUFFER, false}

	buf, buf_ok := gpu.create_buffer_staged(
		&g_vk.device,
		g_vk.cmd_pool,
		g_vk.device.graphics_queue,
		data,
		size,
		to_vk_buffer_usage(usage),
	)
	if !buf_ok {
		return bk.NULL_BUFFER, false
	}

	g_vk.buffers[handle].buffer = buf
	g_vk.buffers[handle].active = true
	return handle, true
}

destroy_buffer_vk :: proc(handle: bk.Buffer_Handle) {
	entry, ok := buffer_entry(handle)
	if !ok {return}
	gpu.destroy_buffer(&g_vk.device, &entry.buffer)
	entry.active = false
}

map_buffer_vk :: proc(handle: bk.Buffer_Handle) -> rawptr {
	entry, ok := buffer_entry(handle)
	if !ok {return nil}
	if .Host_Visible not_in entry.memory {
		log.error("gpu/vk: map_buffer requires a Host_Visible buffer")
		return nil
	}

	mapped: rawptr
	result := vk.MapMemory(
		g_vk.device.device,
		entry.buffer.memory,
		0,
		entry.buffer.size,
		{},
		&mapped,
	)
	if result != .SUCCESS {return nil}

	entry.buffer.mapped = mapped
	return mapped
}

unmap_buffer_vk :: proc(handle: bk.Buffer_Handle) {
	entry, ok := buffer_entry(handle)
	if !ok {return}

	vk.UnmapMemory(g_vk.device.device, entry.buffer.memory)
	entry.buffer.mapped = nil
}

get_buffer_mapped_vk :: proc(handle: bk.Buffer_Handle) -> rawptr {
	entry, ok := buffer_entry(handle)
	if !ok {return nil}
	if .Host_Visible not_in entry.memory {
		log.error("gpu/vk: get_buffer_mapped requires a Host_Visible buffer")
		return nil
	}
	return entry.buffer.mapped
}

bind_vertex_buffer_vk :: proc(ctx: bk.Frame_Context, handle: bk.Buffer_Handle) {
	bind_vertex_buffer_slot_vk(ctx, 0, handle, 0, 0)
}

bind_vertex_buffer_slot_vk :: proc(
	ctx: bk.Frame_Context,
	slot: u32,
	handle: bk.Buffer_Handle,
	offset: u64,
	stride: u32,
) {
	entry, ok := buffer_entry(handle)
	if !ok {return}
	buf := entry.buffer.buffer
	vk_offset := vk.DeviceSize(offset)
	vk.CmdBindVertexBuffers(get_cmd(ctx), slot, 1, &buf, &vk_offset)
}

bind_index_buffer_vk :: proc(ctx: bk.Frame_Context, handle: bk.Buffer_Handle) {
	entry, ok := buffer_entry(handle)
	if !ok {return}
	buf := entry.buffer.buffer
	vk.CmdBindIndexBuffer(get_cmd(ctx), buf, 0, .UINT32)
}

// ============================================================================
// Texture operations
// ============================================================================

create_texture_vk :: proc(desc: bk.Texture_Desc, pixels: rawptr) -> (bk.Texture_Handle, bool) {
	handle, ok := alloc_texture_handle()
	if !ok {return bk.NULL_TEXTURE, false}

	vk_format := to_vk_format(desc.format)
	pxl_size := format_pixel_size(desc.format)
	image_size := vk.DeviceSize(desc.width) * vk.DeviceSize(desc.height) * vk.DeviceSize(pxl_size)

	// Create staging buffer
	staging, staging_ok := gpu.create_buffer(
		&g_vk.device,
		image_size,
		{.TRANSFER_SRC},
		{.HOST_VISIBLE, .HOST_COHERENT},
	)
	if !staging_ok {
		return bk.NULL_TEXTURE, false
	}

	// Copy pixels to staging
	mapped: rawptr
	vk.MapMemory(g_vk.device.device, staging.memory, 0, image_size, {}, &mapped)
	mem.copy(mapped, pixels, int(image_size))
	vk.UnmapMemory(g_vk.device.device, staging.memory)

	// Create GPU image
	img, img_ok := gpu.create_image(
		&g_vk.device,
		desc.width,
		desc.height,
		vk_format,
		.OPTIMAL,
		{.SAMPLED, .TRANSFER_DST},
		{.DEVICE_LOCAL},
	)
	if !img_ok {
		gpu.destroy_buffer(&g_vk.device, &staging)
		return bk.NULL_TEXTURE, false
	}

	// Transition to TRANSFER_DST
	gpu.transition_image_layout(
		&g_vk.device,
		g_vk.cmd_pool,
		g_vk.device.graphics_queue,
		img.image,
		.UNDEFINED,
		.TRANSFER_DST_OPTIMAL,
	)

	// Copy staging buffer to image
	gpu.copy_buffer_to_image(
		&g_vk.device,
		g_vk.cmd_pool,
		g_vk.device.graphics_queue,
		staging.buffer,
		img.image,
		desc.width,
		desc.height,
	)

	// Transition to SHADER_READ_ONLY
	gpu.transition_image_layout(
		&g_vk.device,
		g_vk.cmd_pool,
		g_vk.device.graphics_queue,
		img.image,
		.TRANSFER_DST_OPTIMAL,
		.SHADER_READ_ONLY_OPTIMAL,
	)

	// Create image view
	view, view_ok := gpu.create_image_view(&g_vk.device, img.image, vk_format)
	if !view_ok {
		gpu.destroy_image(&g_vk.device, &img)
		gpu.destroy_buffer(&g_vk.device, &staging)
		return bk.NULL_TEXTURE, false
	}
	img.view = view

	// Cleanup staging
	gpu.destroy_buffer(&g_vk.device, &staging)

	g_vk.textures[handle].image = img
	g_vk.textures[handle].active = true
	return handle, true
}

destroy_texture_vk :: proc(handle: bk.Texture_Handle) {
	entry, ok := texture_entry(handle)
	if !ok {return}
	gpu.destroy_image(&g_vk.device, &entry.image)
	entry.active = false
}

readback_layout_access_stage_vk :: proc(
	layout: bk.Image_Layout,
) -> (
	access: vk.AccessFlags,
	stage: vk.PipelineStageFlags,
	ok: bool,
) {
	switch layout {
	case .Color_Attachment:
		return {.COLOR_ATTACHMENT_WRITE}, {.COLOR_ATTACHMENT_OUTPUT}, true
	case .Shader_Read_Only:
		return {.SHADER_READ}, {.FRAGMENT_SHADER}, true
	case .Transfer_Dst:
		return {.TRANSFER_WRITE}, {.TRANSFER}, true
	case .Undefined, .Depth_Stencil_Attachment, .Depth_Stencil_Read_Only, .Present_Src:
		return {}, {}, false
	}
	return {}, {}, false
}

read_texture_rgba8_vk :: proc(desc: bk.Readback_Texture_Desc, out: []u8) -> bool {
	entry, ok := texture_entry(desc.texture)
	if !ok do return false
	if entry.image.format != .R8G8B8A8_UNORM && entry.image.format != .R8G8B8A8_SRGB && entry.image.format != .B8G8R8A8_SRGB {
		log.error("gpu/vk: read_texture_rgba8 requires an 8-bit RGBA/BGRA color texture")
		return false
	}
	if desc.width != entry.image.width || desc.height != entry.image.height {
		log.error("gpu/vk: read_texture_rgba8 dimensions do not match texture")
		return false
	}
	required_size := int(desc.width * desc.height * 4)
	if len(out) < required_size {
		log.error("gpu/vk: read_texture_rgba8 output buffer is too small")
		return false
	}

	src_access, src_stage, layout_ok := readback_layout_access_stage_vk(desc.current_layout)
	if !layout_ok {
		log.error("gpu/vk: read_texture_rgba8 unsupported source layout")
		return false
	}

	staging, staging_ok := gpu.create_buffer(
		&g_vk.device,
		vk.DeviceSize(required_size),
		{.TRANSFER_DST},
		{.HOST_VISIBLE, .HOST_COHERENT},
	)
	if !staging_ok do return false
	defer gpu.destroy_buffer(&g_vk.device, &staging)

	cmd, cmd_ok := gpu.begin_single_time_commands(&g_vk.device, g_vk.cmd_pool)
	if !cmd_ok do return false

	old_layout := to_vk_image_layout(desc.current_layout)
	barrier_to_copy := vk.ImageMemoryBarrier {
		sType               = .IMAGE_MEMORY_BARRIER,
		srcAccessMask       = src_access,
		dstAccessMask       = {.TRANSFER_READ},
		oldLayout           = old_layout,
		newLayout           = .TRANSFER_SRC_OPTIMAL,
		srcQueueFamilyIndex = vk.QUEUE_FAMILY_IGNORED,
		dstQueueFamilyIndex = vk.QUEUE_FAMILY_IGNORED,
		image               = entry.image.image,
		subresourceRange    = {
			aspectMask     = {.COLOR},
			baseMipLevel   = 0,
			levelCount     = 1,
			baseArrayLayer = 0,
			layerCount     = 1,
		},
	}
	vk.CmdPipelineBarrier(cmd, src_stage, {.TRANSFER}, {}, 0, nil, 0, nil, 1, &barrier_to_copy)

	region := vk.BufferImageCopy {
		bufferOffset      = 0,
		bufferRowLength   = 0,
		bufferImageHeight = 0,
		imageSubresource  = {
			aspectMask     = {.COLOR},
			mipLevel       = 0,
			baseArrayLayer = 0,
			layerCount     = 1,
		},
		imageOffset = {0, 0, 0},
		imageExtent = {desc.width, desc.height, 1},
	}
	vk.CmdCopyImageToBuffer(cmd, entry.image.image, .TRANSFER_SRC_OPTIMAL, staging.buffer, 1, &region)

	dst_access, dst_stage, _ := readback_layout_access_stage_vk(desc.current_layout)
	barrier_from_copy := vk.ImageMemoryBarrier {
		sType               = .IMAGE_MEMORY_BARRIER,
		srcAccessMask       = {.TRANSFER_READ},
		dstAccessMask       = dst_access,
		oldLayout           = .TRANSFER_SRC_OPTIMAL,
		newLayout           = old_layout,
		srcQueueFamilyIndex = vk.QUEUE_FAMILY_IGNORED,
		dstQueueFamilyIndex = vk.QUEUE_FAMILY_IGNORED,
		image               = entry.image.image,
		subresourceRange    = barrier_to_copy.subresourceRange,
	}
	vk.CmdPipelineBarrier(cmd, {.TRANSFER}, dst_stage, {}, 0, nil, 0, nil, 1, &barrier_from_copy)

	gpu.end_single_time_commands(&g_vk.device, g_vk.cmd_pool, g_vk.device.graphics_queue, cmd)

	mapped: rawptr
	result := vk.MapMemory(g_vk.device.device, staging.memory, 0, vk.DeviceSize(required_size), {}, &mapped)
	if result != .SUCCESS {
		log.errorf("gpu/vk: read_texture_rgba8 map failed: %v", result)
		return false
	}
	defer vk.UnmapMemory(g_vk.device.device, staging.memory)

	src := ([^]u8)(mapped)
	for i in 0..<required_size {
		out[i] = src[i]
	}
	return true
}

// ============================================================================
// Image operations (render targets, depth buffers)
// ============================================================================

create_image_vk :: proc(desc: bk.Texture_Desc) -> (bk.Texture_Handle, bool) {
	handle, ok := alloc_texture_handle()
	if !ok {return bk.NULL_TEXTURE, false}

	img, img_ok := gpu.create_image(
		&g_vk.device,
		desc.width,
		desc.height,
		to_vk_format(desc.format),
		.OPTIMAL,
		to_vk_image_usage(desc.usage),
		{.DEVICE_LOCAL},
	)
	if !img_ok {
		return bk.NULL_TEXTURE, false
	}

	g_vk.textures[handle].image = img
	g_vk.textures[handle].active = true
	return handle, true
}

create_image_view_vk :: proc(
	texture: bk.Texture_Handle,
	format: bk.Format,
	aspect: bk.Image_Aspect_Flags,
) -> bool {
	entry, ok := texture_entry(texture)
	if !ok {return false}

	// Destroy existing view if any
	if entry.image.view != 0 {
		vk.DestroyImageView(g_vk.device.device, entry.image.view, nil)
		entry.image.view = 0
	}

	view, view_ok := gpu.create_image_view(
		&g_vk.device,
		entry.image.image,
		to_vk_format(format),
		to_vk_image_aspect(aspect),
	)
	if !view_ok {return false}

	entry.image.view = view
	return true
}

destroy_image_vk :: proc(handle: bk.Texture_Handle) {
	destroy_texture_vk(handle)
}

// ============================================================================
// Sampler operations
// ============================================================================

create_sampler_vk :: proc(desc: bk.Sampler_Desc) -> (bk.Sampler_Handle, bool) {
	handle, ok := alloc_sampler_handle()
	if !ok {return bk.NULL_SAMPLER, false}

	max_aniso := g_vk.device.properties.limits.maxSamplerAnisotropy

	sampler_info := vk.SamplerCreateInfo {
		sType                   = .SAMPLER_CREATE_INFO,
		magFilter               = to_vk_filter(desc.mag_filter),
		minFilter               = to_vk_filter(desc.min_filter),
		addressModeU            = to_vk_address_mode(desc.address_mode_u),
		addressModeV            = to_vk_address_mode(desc.address_mode_v),
		addressModeW            = to_vk_address_mode(desc.address_mode_u),
		anisotropyEnable        = b32(desc.enable_aniso),
		maxAnisotropy           = max_aniso if desc.enable_aniso else 1.0,
		borderColor             = .INT_OPAQUE_BLACK,
		unnormalizedCoordinates = false,
		compareEnable           = b32(desc.enable_compare),
		compareOp               = to_vk_compare_op(desc.compare_op),
		mipmapMode              = .LINEAR if desc.mipmap_mode == .Linear else .NEAREST,
		mipLodBias              = 0,
		minLod                  = 0,
		maxLod                  = 0,
	}

	sampler: vk.Sampler
	result := vk.CreateSampler(g_vk.device.device, &sampler_info, nil, &sampler)
	if result != .SUCCESS {
		log.errorf("gpu/vk: vkCreateSampler failed: %v", result)
		return bk.NULL_SAMPLER, false
	}

	entry := &g_vk.samplers[handle]
	entry.handle = sampler
	entry.active = true
	return handle, true
}

destroy_sampler_vk :: proc(handle: bk.Sampler_Handle) {
	entry, ok := sampler_entry(handle)
	if !ok {return}
	vk.DestroySampler(g_vk.device.device, entry.handle, nil)
	entry.active = false
}

// ============================================================================
// Shader operations
// ============================================================================

create_shader_module_vk :: proc(desc: bk.Shader_Module_Desc) -> (bk.Shader_Handle, bool) {
	handle, ok := alloc_shader_handle()
	if !ok {return bk.NULL_SHADER, false}

	if desc.format != .SPIRV {
		log.errorf("gpu/vk: shader '%s' must be SPIR-V", desc.name)
		return bk.NULL_SHADER, false
	}

	mod, mod_ok := pipeline.create_shader_module_from_bytes(g_vk.device.device, desc.data)
	if !mod_ok {
		return bk.NULL_SHADER, false
	}

	g_vk.shaders[handle].module = mod
	g_vk.shaders[handle].active = true
	return handle, true
}

destroy_shader_vk :: proc(handle: bk.Shader_Handle) {
	entry, ok := shader_entry(handle)
	if !ok {return}
	pipeline.destroy_shader_module(g_vk.device.device, &entry.module)
	entry.active = false
}

// ============================================================================
// Graphics pipeline operations
// ============================================================================

create_graphics_pipeline_vk :: proc(desc: bk.Pipeline_Desc) -> (bk.Pipeline_Handle, bool) {
	handle, ok := alloc_pipeline_handle()
	if !ok {return bk.NULL_PIPELINE, false}

	// Get shader modules
	vert_entry, vert_ok := shader_entry(desc.vert_shader)
	frag_entry, frag_ok := shader_entry(desc.frag_shader)
	if !vert_ok || !frag_ok {
		log.error("gpu/vk: graphics pipeline shader handle is invalid")
		return bk.NULL_PIPELINE, false
	}
	vert_mod := vert_entry.module.handle
	frag_mod := frag_entry.module.handle

	// Get render pass
	render_pass, render_pass_ok := render_pass_entry(desc.render_pass)
	if !render_pass_ok {
		log.error("gpu/vk: graphics pipeline render pass handle is invalid")
		return bk.NULL_PIPELINE, false
	}
	rp := render_pass.handle

	// Convert descriptor layouts
	layouts := make([]vk.DescriptorSetLayout, len(desc.descriptor_layouts), context.temp_allocator)
	for i in 0 ..< len(desc.descriptor_layouts) {
		layout_entry, layout_ok := descriptor_entry_of_kind(
			desc.descriptor_layouts[i],
			.Set_Layout,
		)
		if !layout_ok {
			log.error("gpu/vk: graphics pipeline descriptor layout handle is invalid")
			return bk.NULL_PIPELINE, false
		}
		layouts[i] = layout_entry.set_layout
	}

	// Convert vertex bindings
	bindings := make(
		[]vk.VertexInputBindingDescription,
		len(desc.vertex_bindings),
		context.temp_allocator,
	)
	for i in 0 ..< len(desc.vertex_bindings) {
		bindings[i] = vk.VertexInputBindingDescription {
			binding   = desc.vertex_bindings[i].binding,
			stride    = desc.vertex_bindings[i].stride,
			inputRate = .INSTANCE if desc.vertex_bindings[i].input_rate == .Instance else .VERTEX,
		}
	}

	// Convert vertex attributes
	attrs := make(
		[]vk.VertexInputAttributeDescription,
		len(desc.vertex_attributes),
		context.temp_allocator,
	)
	for i in 0 ..< len(desc.vertex_attributes) {
		attrs[i] = vk.VertexInputAttributeDescription {
			location = desc.vertex_attributes[i].location,
			binding  = desc.vertex_attributes[i].binding,
			format   = to_vk_vertex_format(desc.vertex_attributes[i].format),
			offset   = desc.vertex_attributes[i].offset,
		}
	}

	config := pipeline.Pipeline_Config {
		vert_module            = vert_mod,
		frag_module            = frag_mod,
		render_pass            = rp,
		topology               = to_vk_topology(desc.topology),
		polygon_mode           = .FILL,
		cull_mode              = to_vk_cull_mode(desc.cull_mode),
		front_face             = to_vk_front_face(desc.front_face),
		enable_blending        = desc.enable_blending,
		blend_mode             = desc.blend_mode,
		enable_depth_test      = desc.enable_depth_test,
		enable_depth_bias      = desc.enable_depth_bias,
		stencil                = desc.stencil,
		depth_only             = desc.depth_only,
		color_attachment_count = desc.color_attachment_count,
		push_constant_size     = desc.push_constant_size,
		push_constant_stages   = to_vk_shader_stages(desc.push_constant_stages),
		descriptor_set_layouts = layouts,
		binding_descriptions   = bindings,
		attribute_descriptions = attrs,
	}

	gp, gp_ok := pipeline.create_graphics_pipeline(&g_vk.device, config)
	if !gp_ok {
		return bk.NULL_PIPELINE, false
	}

	g_vk.pipelines[handle].graphics = gp
	g_vk.pipelines[handle].is_compute = false
	g_vk.pipelines[handle].active = true
	return handle, true
}

destroy_graphics_pipeline_vk :: proc(handle: bk.Pipeline_Handle) {
	p, ok := pipeline_entry(handle)
	if !ok {return}
	pipeline.destroy_graphics_pipeline(&g_vk.device, &p.graphics)
	p.active = false
}

bind_graphics_pipeline_vk :: proc(ctx: bk.Frame_Context, handle: bk.Pipeline_Handle) {
	p, ok := pipeline_entry(handle)
	if !ok {return}
	vk.CmdBindPipeline(get_cmd(ctx), .GRAPHICS, p.graphics.pipeline)
}

push_constants_vk :: proc(
	ctx: bk.Frame_Context,
	pipeline_handle: bk.Pipeline_Handle,
	stages: bk.Shader_Stage_Flags,
	offset, size: u32,
	data: rawptr,
) {
	p, ok := pipeline_entry(pipeline_handle)
	if !ok {return}
	layout := p.compute.layout if p.is_compute else p.graphics.layout
	vk.CmdPushConstants(get_cmd(ctx), layout, to_vk_shader_stages(stages), offset, size, data)
}

// ============================================================================
// Compute pipeline operations
// ============================================================================

create_compute_pipeline_vk :: proc(
	shader: bk.Shader_Handle,
	num_buffers: u32,
	push_constant_size: u32,
) -> (
	bk.Pipeline_Handle,
	bool,
) {
	handle, ok := alloc_pipeline_handle()
	if !ok {return bk.NULL_PIPELINE, false}

	shader_entry, shader_ok := shader_entry(shader)
	if !shader_ok {return bk.NULL_PIPELINE, false}
	shader_mod := shader_entry.module.handle

	cp, cp_ok := pipeline.create_compute_pipeline(
		&g_vk.device,
		shader_mod,
		num_buffers,
		push_constant_size,
	)
	if !cp_ok {
		return bk.NULL_PIPELINE, false
	}

	g_vk.pipelines[handle].compute = cp
	g_vk.pipelines[handle].is_compute = true
	g_vk.pipelines[handle].active = true
	return handle, true
}

destroy_compute_pipeline_vk :: proc(handle: bk.Pipeline_Handle) {
	p, ok := pipeline_entry(handle)
	if !ok {return}
	pipeline.destroy_compute_pipeline(&g_vk.device, &p.compute)
	p.active = false
}

bind_compute_pipeline_vk :: proc(ctx: bk.Frame_Context, handle: bk.Pipeline_Handle) {
	p, ok := pipeline_entry(handle)
	if !ok {return}
	vk.CmdBindPipeline(get_cmd(ctx), .COMPUTE, p.compute.pipeline)
}

// ============================================================================
// Descriptor operations
// ============================================================================

create_descriptor_set_layout_vk :: proc(
	bindings: []bk.Descriptor_Set_Layout_Binding,
) -> (
	bk.Descriptor_Handle,
	bool,
) {
	handle, ok := alloc_descriptor_handle()
	if !ok {return bk.NULL_DESCRIPTOR, false}

	vk_bindings := make([]vk.DescriptorSetLayoutBinding, len(bindings), context.temp_allocator)
	for i in 0 ..< len(bindings) {
		vk_bindings[i] = vk.DescriptorSetLayoutBinding {
			binding         = bindings[i].binding,
			descriptorType  = to_vk_descriptor_type(bindings[i].type),
			descriptorCount = bindings[i].count,
			stageFlags      = to_vk_shader_stages(bindings[i].stages),
		}
	}

	create_info := vk.DescriptorSetLayoutCreateInfo {
		sType        = .DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
		bindingCount = u32(len(vk_bindings)),
		pBindings    = raw_data(vk_bindings),
	}

	layout: vk.DescriptorSetLayout
	result := vk.CreateDescriptorSetLayout(g_vk.device.device, &create_info, nil, &layout)
	if result != .SUCCESS {
		log.errorf("gpu/vk: vkCreateDescriptorSetLayout failed: %v", result)
		return bk.NULL_DESCRIPTOR, false
	}

	entry := &g_vk.descriptors[handle]
	entry.set_layout = layout
	entry.layout_count = u32(min(len(bindings), 16))
	for i in 0 ..< int(entry.layout_count) {
		entry.layout_bindings[i] = bindings[i]
	}
	entry.kind = .Set_Layout
	entry.active = true
	return handle, true
}

destroy_descriptor_set_layout_vk :: proc(handle: bk.Descriptor_Handle) {
	entry, ok := descriptor_entry_of_kind(handle, .Set_Layout)
	if !ok {return}
	vk.DestroyDescriptorSetLayout(g_vk.device.device, entry.set_layout, nil)
	entry.active = false
}

create_descriptor_pool_vk :: proc(
	max_sets: u32,
	types: []bk.Descriptor_Type,
	counts: []u32,
) -> (
	bk.Descriptor_Handle,
	bool,
) {
	handle, ok := alloc_descriptor_handle()
	if !ok {return bk.NULL_DESCRIPTOR, false}

	pool_sizes := make([]vk.DescriptorPoolSize, len(types), context.temp_allocator)
	for i in 0 ..< len(types) {
		pool_sizes[i] = vk.DescriptorPoolSize {
			type            = to_vk_descriptor_type(types[i]),
			descriptorCount = counts[i],
		}
	}

	create_info := vk.DescriptorPoolCreateInfo {
		sType         = .DESCRIPTOR_POOL_CREATE_INFO,
		flags         = {.FREE_DESCRIPTOR_SET},
		maxSets       = max_sets,
		poolSizeCount = u32(len(pool_sizes)),
		pPoolSizes    = raw_data(pool_sizes),
	}

	pool: vk.DescriptorPool
	result := vk.CreateDescriptorPool(g_vk.device.device, &create_info, nil, &pool)
	if result != .SUCCESS {
		log.errorf("gpu/vk: vkCreateDescriptorPool failed: %v", result)
		return bk.NULL_DESCRIPTOR, false
	}

	entry := &g_vk.descriptors[handle]
	entry.pool = pool
	entry.kind = .Pool
	entry.active = true
	return handle, true
}

destroy_descriptor_pool_vk :: proc(handle: bk.Descriptor_Handle) {
	entry, ok := descriptor_entry_of_kind(handle, .Pool)
	if !ok {return}
	vk.DestroyDescriptorPool(g_vk.device.device, entry.pool, nil)
	entry.active = false
}

allocate_descriptor_set_vk :: proc(
	pool: bk.Descriptor_Handle,
	layout: bk.Descriptor_Handle,
) -> (
	bk.Descriptor_Handle,
	bool,
) {
	handle, ok := alloc_descriptor_handle()
	if !ok {return bk.NULL_DESCRIPTOR, false}

	pool_entry, pool_ok := descriptor_entry_of_kind(pool, .Pool)
	layout_entry, layout_ok := descriptor_entry_of_kind(layout, .Set_Layout)
	if !pool_ok || !layout_ok {return bk.NULL_DESCRIPTOR, false}
	vk_pool := pool_entry.pool
	vk_layout := layout_entry.set_layout

	layout_copy := vk_layout
	alloc_info := vk.DescriptorSetAllocateInfo {
		sType              = .DESCRIPTOR_SET_ALLOCATE_INFO,
		descriptorPool     = vk_pool,
		descriptorSetCount = 1,
		pSetLayouts        = &layout_copy,
	}

	set: vk.DescriptorSet
	result := vk.AllocateDescriptorSets(g_vk.device.device, &alloc_info, &set)
	if result != .SUCCESS {
		log.errorf("gpu/vk: vkAllocateDescriptorSets failed: %v", result)
		return bk.NULL_DESCRIPTOR, false
	}

	entry := &g_vk.descriptors[handle]
	entry.set = set
	entry.binding_count = layout_entry.layout_count
	for i in 0 ..< int(entry.binding_count) {
		entry.bindings[i].binding = layout_entry.layout_bindings[i].binding
		entry.bindings[i].type = layout_entry.layout_bindings[i].type
	}
	entry.kind = .Set
	entry.active = true
	return handle, true
}

bind_descriptor_set_vk :: proc(
	ctx: bk.Frame_Context,
	pipeline_handle: bk.Pipeline_Handle,
	set: bk.Descriptor_Handle,
	index: u32,
) {
	cmd := get_cmd(ctx)
	p, pipeline_ok := pipeline_entry(pipeline_handle)
	set_entry, set_ok := descriptor_entry_of_kind(set, .Set)
	if !pipeline_ok || !set_ok {return}

	vk_set := set_entry.set
	bind_point := vk.PipelineBindPoint.COMPUTE if p.is_compute else vk.PipelineBindPoint.GRAPHICS
	layout := p.compute.layout if p.is_compute else p.graphics.layout

	vk.CmdBindDescriptorSets(cmd, bind_point, layout, index, 1, &vk_set, 0, nil)
}

update_descriptor_image_vk :: proc(
	set: bk.Descriptor_Handle,
	binding: u32,
	texture: bk.Texture_Handle,
	sampler: bk.Sampler_Handle,
	layout: bk.Image_Layout,
) {
	set_entry, set_ok := descriptor_entry_of_kind(set, .Set)
	texture_entry, texture_ok := texture_entry(texture)
	sampler_entry, sampler_ok := sampler_entry(sampler)
	if !set_ok || !texture_ok || !sampler_ok {return}
	vk_set := set_entry.set
	image_view := texture_entry.image.view
	vk_sampler := sampler_entry.handle

	image_info := vk.DescriptorImageInfo {
		sampler     = vk_sampler,
		imageView   = image_view,
		imageLayout = to_vk_image_layout(layout),
	}

	write := vk.WriteDescriptorSet {
		sType           = .WRITE_DESCRIPTOR_SET,
		dstSet          = vk_set,
		dstBinding      = binding,
		dstArrayElement = 0,
		descriptorCount = 1,
		descriptorType  = .COMBINED_IMAGE_SAMPLER,
		pImageInfo      = &image_info,
	}

	vk.UpdateDescriptorSets(g_vk.device.device, 1, &write, 0, nil)
}

update_descriptor_buffer_vk :: proc(
	set: bk.Descriptor_Handle,
	binding: u32,
	buffer: bk.Buffer_Handle,
	size: u64,
) {
	entry, entry_ok := descriptor_entry_of_kind(set, .Set)
	buffer_entry, buffer_ok := buffer_entry(buffer)
	if !entry_ok || !buffer_ok {return}
	vk_set := entry.set
	vk_buffer := buffer_entry.buffer.buffer
	descriptor_type: vk.DescriptorType
	binding_found := false
	for i in 0 ..< int(entry.binding_count) {
		if entry.bindings[i].binding == binding {
			descriptor_type = to_vk_descriptor_type(entry.bindings[i].type)
			binding_found = true
			break
		}
	}
	if !binding_found {
		log.errorf("gpu/vk: descriptor buffer binding %d is not in set layout", binding)
		return
	}

	buffer_info := vk.DescriptorBufferInfo {
		buffer = vk_buffer,
		offset = 0,
		range  = vk.DeviceSize(size),
	}

	write := vk.WriteDescriptorSet {
		sType           = .WRITE_DESCRIPTOR_SET,
		dstSet          = vk_set,
		dstBinding      = binding,
		dstArrayElement = 0,
		descriptorCount = 1,
		descriptorType  = descriptor_type,
		pBufferInfo     = &buffer_info,
	}

	vk.UpdateDescriptorSets(g_vk.device.device, 1, &write, 0, nil)
}

// ============================================================================
// Render pass objects
// ============================================================================

@(private)
resolve_color_final_layout_vk :: proc(desc: bk.Render_Pass_Desc) -> bk.Image_Layout {
	if desc.color_final_layout != .Undefined {
		return desc.color_final_layout
	}
	return .Present_Src
}

@(private)
resolve_depth_final_layout_vk :: proc(desc: bk.Render_Pass_Desc) -> bk.Image_Layout {
	if desc.depth_final_layout != .Undefined {
		return desc.depth_final_layout
	}
	return .Depth_Stencil_Read_Only if desc.depth_only else .Depth_Stencil_Attachment
}

@(private)
attachment_initial_layout_vk :: proc(
	load_op: bk.Attachment_Load_Op,
	attachment_layout: bk.Image_Layout,
) -> vk.ImageLayout {
	switch load_op {
	case .Load:
		return to_vk_image_layout(attachment_layout)
	case .Clear:
		return .UNDEFINED
	case .Dont_Care:
		return .UNDEFINED
	}
	return .UNDEFINED
}

create_render_pass_vk :: proc(desc: bk.Render_Pass_Desc) -> (bk.Render_Pass_Handle, bool) {
	handle, ok := alloc_render_pass_handle()
	if !ok {return bk.NULL_RENDER_PASS, false}

	if desc.depth_only {
		depth_final_layout := resolve_depth_final_layout_vk(desc)
		stencil_load := desc.stencil_load_op if desc.has_stencil else .Dont_Care
		stencil_store := desc.stencil_store_op if desc.has_stencil else .Dont_Care
		// Depth-only render pass (shadow maps)
		depth_attachment := vk.AttachmentDescription {
			format         = to_vk_format(desc.depth_format),
			samples        = {._1},
			loadOp         = to_vk_attachment_load_op(desc.depth_load_op),
			storeOp        = to_vk_attachment_store_op(desc.depth_store_op),
			stencilLoadOp  = to_vk_attachment_load_op(stencil_load),
			stencilStoreOp = to_vk_attachment_store_op(stencil_store),
			initialLayout  = attachment_initial_layout_vk(
				desc.depth_load_op,
				.Depth_Stencil_Attachment,
			),
			finalLayout    = to_vk_image_layout(depth_final_layout),
		}

		depth_ref := vk.AttachmentReference {
			attachment = 0,
			layout     = .DEPTH_STENCIL_ATTACHMENT_OPTIMAL,
		}

		subpass := vk.SubpassDescription {
			pipelineBindPoint       = .GRAPHICS,
			colorAttachmentCount    = 0,
			pDepthStencilAttachment = &depth_ref,
		}

		dependencies := [2]vk.SubpassDependency {
			{
				srcSubpass = vk.SUBPASS_EXTERNAL,
				dstSubpass = 0,
				srcStageMask = {.FRAGMENT_SHADER},
				dstStageMask = {.EARLY_FRAGMENT_TESTS},
				srcAccessMask = {.SHADER_READ},
				dstAccessMask = {.DEPTH_STENCIL_ATTACHMENT_WRITE},
				dependencyFlags = {.BY_REGION},
			},
			{
				srcSubpass = 0,
				dstSubpass = vk.SUBPASS_EXTERNAL,
				srcStageMask = {.LATE_FRAGMENT_TESTS},
				dstStageMask = {.FRAGMENT_SHADER},
				srcAccessMask = {.DEPTH_STENCIL_ATTACHMENT_WRITE},
				dstAccessMask = {.SHADER_READ},
				dependencyFlags = {.BY_REGION},
			},
		}

		rp_info := vk.RenderPassCreateInfo {
			sType           = .RENDER_PASS_CREATE_INFO,
			attachmentCount = 1,
			pAttachments    = &depth_attachment,
			subpassCount    = 1,
			pSubpasses      = &subpass,
			dependencyCount = 2,
			pDependencies   = &dependencies[0],
		}

		rp: vk.RenderPass
		result := vk.CreateRenderPass(g_vk.device.device, &rp_info, nil, &rp)
		if result != .SUCCESS {
			log.errorf("gpu/vk: vkCreateRenderPass (depth-only) failed: %v", result)
			return bk.NULL_RENDER_PASS, false
		}

		g_vk.render_passes[handle].handle = rp
		g_vk.render_passes[handle].desc = desc
		g_vk.render_passes[handle].active = true
		return handle, true
	}

	// Standard color + optional depth render pass
	attachments: [bk.MAX_COLOR_TARGETS + 1]vk.AttachmentDescription
	attachment_count: u32 = 0
	depth_final_layout := resolve_depth_final_layout_vk(desc)
	color_count := desc.color_count
	legacy_single_color_desc := false
	if desc.has_color && color_count == 0 {
		color_count = 1
		legacy_single_color_desc = true
	}

	if desc.has_color {
		for i in 0..<color_count {
			final_layout := desc.color_final_layouts[i]
			if final_layout == .Undefined {
				final_layout = resolve_color_final_layout_vk(desc)
			}
			load_op := desc.color_load_ops[i]
			store_op := desc.color_store_ops[i]
			format := desc.color_formats[i]
			if legacy_single_color_desc && i == 0 {
				load_op = desc.color_load_op
				store_op = desc.color_store_op
				format = desc.color_format
			} else if format == .Undefined {
				format = desc.color_format
			}
			attachments[attachment_count] = vk.AttachmentDescription {
				format         = to_vk_format(format),
				samples        = {._1},
				loadOp         = to_vk_attachment_load_op(load_op),
				storeOp        = to_vk_attachment_store_op(store_op),
				stencilLoadOp  = .DONT_CARE,
				stencilStoreOp = .DONT_CARE,
				initialLayout  = attachment_initial_layout_vk(load_op, .Color_Attachment),
				finalLayout    = to_vk_image_layout(final_layout),
			}
			attachment_count += 1
		}
	}

	if desc.has_depth {
		stencil_load := desc.stencil_load_op if desc.has_stencil else .Dont_Care
		stencil_store := desc.stencil_store_op if desc.has_stencil else .Dont_Care
		attachments[attachment_count] = vk.AttachmentDescription {
			format         = to_vk_format(desc.depth_format),
			samples        = {._1},
			loadOp         = to_vk_attachment_load_op(desc.depth_load_op),
			storeOp        = to_vk_attachment_store_op(desc.depth_store_op),
			stencilLoadOp  = to_vk_attachment_load_op(stencil_load),
			stencilStoreOp = to_vk_attachment_store_op(stencil_store),
			initialLayout  = attachment_initial_layout_vk(
				desc.depth_load_op,
				.Depth_Stencil_Attachment,
			),
			finalLayout    = to_vk_image_layout(depth_final_layout),
		}
		attachment_count += 1
	}

	color_refs: [bk.MAX_COLOR_TARGETS]vk.AttachmentReference
	for i in 0..<color_count {
		color_refs[i] = vk.AttachmentReference {
			attachment = i,
			layout     = .COLOR_ATTACHMENT_OPTIMAL,
		}
	}

	depth_ref := vk.AttachmentReference {
		attachment = color_count if desc.has_color else 0,
		layout     = .DEPTH_STENCIL_ATTACHMENT_OPTIMAL,
	}

	subpass := vk.SubpassDescription {
		pipelineBindPoint       = .GRAPHICS,
		colorAttachmentCount    = color_count if desc.has_color else 0,
		pColorAttachments       = &color_refs[0] if desc.has_color else nil,
		pDepthStencilAttachment = &depth_ref if desc.has_depth else nil,
	}

	dependencies: [2]vk.SubpassDependency
	dependency_count: u32 = 1
	dependencies[0] = vk.SubpassDependency {
		srcSubpass      = vk.SUBPASS_EXTERNAL,
		dstSubpass      = 0,
		srcStageMask    = {.COLOR_ATTACHMENT_OUTPUT, .EARLY_FRAGMENT_TESTS},
		srcAccessMask   = {},
		dstStageMask    = {.COLOR_ATTACHMENT_OUTPUT, .EARLY_FRAGMENT_TESTS},
		dstAccessMask   = {.COLOR_ATTACHMENT_WRITE, .DEPTH_STENCIL_ATTACHMENT_WRITE},
		dependencyFlags = {.BY_REGION},
	}
	needs_color_shader_read_dependency := false
	for i in 0..<color_count {
		if desc.color_final_layouts[i] == .Shader_Read_Only ||
		   (i == 0 && desc.color_final_layouts[i] == .Undefined && resolve_color_final_layout_vk(desc) == .Shader_Read_Only) {
			needs_color_shader_read_dependency = true
		}
	}
	if desc.has_color && needs_color_shader_read_dependency {
		dependencies[1] = vk.SubpassDependency {
			srcSubpass      = 0,
			dstSubpass      = vk.SUBPASS_EXTERNAL,
			srcStageMask    = {.COLOR_ATTACHMENT_OUTPUT},
			srcAccessMask   = {.COLOR_ATTACHMENT_WRITE},
			dstStageMask    = {.FRAGMENT_SHADER},
			dstAccessMask   = {.SHADER_READ},
			dependencyFlags = {.BY_REGION},
		}
		dependency_count = 2
	}

	rp_info := vk.RenderPassCreateInfo {
		sType           = .RENDER_PASS_CREATE_INFO,
		attachmentCount = attachment_count,
		pAttachments    = &attachments[0],
		subpassCount    = 1,
		pSubpasses      = &subpass,
		dependencyCount = dependency_count,
		pDependencies   = &dependencies[0],
	}

	rp: vk.RenderPass
	result := vk.CreateRenderPass(g_vk.device.device, &rp_info, nil, &rp)
	if result != .SUCCESS {
		log.errorf("gpu/vk: vkCreateRenderPass failed: %v", result)
		return bk.NULL_RENDER_PASS, false
	}

	g_vk.render_passes[handle].handle = rp
	g_vk.render_passes[handle].desc = desc
	g_vk.render_passes[handle].active = true
	return handle, true
}

destroy_render_pass_vk :: proc(handle: bk.Render_Pass_Handle) {
	entry, ok := render_pass_entry(handle)
	if !ok {return}
	// Don't destroy the swapchain's render pass (managed by swapchain lifecycle)
	if entry.handle != g_vk.swapchain.render_pass {
		vk.DestroyRenderPass(g_vk.device.device, entry.handle, nil)
	}
	entry.active = false
}

// ============================================================================
// Framebuffer operations
// ============================================================================

create_framebuffer_vk :: proc(desc: bk.Framebuffer_Desc) -> (bk.Framebuffer_Handle, bool) {
	handle, ok := alloc_framebuffer_handle()
	if !ok {return bk.NULL_FRAMEBUFFER, false}

	pass_entry, pass_ok := render_pass_entry(desc.pass)
	if !pass_ok {
		log.error("gpu/vk: framebuffer render pass is not active")
		return bk.NULL_FRAMEBUFFER, false
	}

	attachments: [bk.MAX_COLOR_TARGETS + 1]vk.ImageView
	attachment_count: u32
	if pass_entry.desc.has_color {
		color_count := desc.color_count
		if color_count == 0 {
			color_count = pass_entry.desc.color_count
		}
		if color_count == 0 {
			color_count = 1
		}
		for i in 0..<color_count {
			view := desc.color_views[i]
			if i == 0 && view == bk.NULL_TEXTURE {
				view = desc.color_view
			}
			color_entry, color_ok := texture_entry(view)
			if !color_ok {
				log.error("gpu/vk: framebuffer color attachment is not active")
				return bk.NULL_FRAMEBUFFER, false
			}
			attachments[attachment_count] = color_entry.image.view
			attachment_count += 1
		}
	}
	if pass_entry.desc.has_depth {
		depth_entry, depth_ok := texture_entry(desc.depth_view)
		if !depth_ok {
			log.error("gpu/vk: framebuffer depth attachment is not active")
			return bk.NULL_FRAMEBUFFER, false
		}
		attachments[attachment_count] = depth_entry.image.view
		attachment_count += 1
	}
	if attachment_count == 0 {
		log.error("gpu/vk: framebuffer needs at least one attachment")
		return bk.NULL_FRAMEBUFFER, false
	}

	fb_info := vk.FramebufferCreateInfo {
		sType           = .FRAMEBUFFER_CREATE_INFO,
		renderPass      = pass_entry.handle,
		attachmentCount = attachment_count,
		pAttachments    = &attachments[0],
		width           = desc.width,
		height          = desc.height,
		layers          = desc.layers if desc.layers != 0 else 1,
	}

	fb: vk.Framebuffer
	result := vk.CreateFramebuffer(g_vk.device.device, &fb_info, nil, &fb)
	if result != .SUCCESS {
		log.errorf("gpu/vk: vkCreateFramebuffer failed: %v", result)
		return bk.NULL_FRAMEBUFFER, false
	}

	g_vk.framebuffers[handle].handle = fb
	g_vk.framebuffers[handle].active = true
	return handle, true
}

destroy_framebuffer_vk :: proc(handle: bk.Framebuffer_Handle) {
	entry, ok := framebuffer_entry(handle)
	if !ok {return}
	vk.DestroyFramebuffer(g_vk.device.device, entry.handle, nil)
	entry.active = false
}