Harbor

branch main
showing the latest snapshot on main
d3d12_ops.odin 50.7 KB · Plain text
backend/d3d12/d3d12_ops.odin 0644 Raw
package d3d12_backend

import "core:log"
import "core:mem"
import "core:strings"

import d3d12 "vendor:directx/d3d12"
import d3dc "vendor:directx/d3d_compiler"
import dxgi "vendor:directx/dxgi"

import bk ".."

active_pool_entry_d3d12 :: 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) -> (^D3D12_Buffer_Entry, bool) {
	if g_d3d == nil do return nil, false
	return active_pool_entry_d3d12(&g_d3d.buffers, handle)
}

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

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

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

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

descriptor_entry_of_kind :: proc(
	handle: bk.Descriptor_Handle,
	kind: Descriptor_Kind,
) -> (
	^D3D12_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) -> (^D3D12_Render_Pass_Entry, bool) {
	if g_d3d == nil do return nil, false
	return active_pool_entry_d3d12(&g_d3d.render_passes, handle)
}

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

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

completed_fence_value_d3d12 :: proc() -> u64 {
	if g_d3d == nil || g_d3d.fence == nil do return 0
	return g_d3d.fence->GetCompletedValue()
}

deferred_release_ready_d3d12 :: proc(item: D3D12_Deferred_Release, completed_fence: u64) -> bool {
	return item.fence_value == 0 || completed_fence >= item.fence_value
}

retire_fence_value_d3d12 :: proc() -> u64 {
	if g_d3d == nil || g_d3d.fence == nil do return 0
	if g_d3d.frame_active do return g_d3d.fence_value
	if g_d3d.fence_value > 1 do return g_d3d.fence_value - 1
	return 0
}

release_deferred_item_d3d12 :: proc(item: D3D12_Deferred_Release) {
	switch item.kind {
	case .Resource:
		if item.resource != nil do item.resource->Release()
	case .Pipeline_State:
		if item.pso != nil do item.pso->Release()
	}
}

drain_deferred_releases_d3d12 :: proc(completed_fence: u64) {
	if g_d3d == nil || len(g_d3d.deferred_releases) == 0 do return
	write_idx := 0
	for item in g_d3d.deferred_releases {
		if deferred_release_ready_d3d12(item, completed_fence) {
			release_deferred_item_d3d12(item)
		} else {
			g_d3d.deferred_releases[write_idx] = item
			write_idx += 1
		}
	}
	resize(&g_d3d.deferred_releases, write_idx)
}

drain_deferred_releases_now_d3d12 :: proc() {
	if g_d3d == nil do return
	drain_deferred_releases_d3d12(completed_fence_value_d3d12())
}

defer_release_resource_d3d12 :: proc(resource: ^d3d12.IResource) {
	if resource == nil do return
	if g_d3d == nil || g_d3d.fence == nil {
		resource->Release()
		return
	}
	fence_value := retire_fence_value_d3d12()
	if deferred_release_ready_d3d12(
		{kind = .Resource, fence_value = fence_value},
		completed_fence_value_d3d12(),
	) {
		resource->Release()
		return
	}
	append(
		&g_d3d.deferred_releases,
		D3D12_Deferred_Release{kind = .Resource, fence_value = fence_value, resource = resource},
	)
}

defer_release_pso_d3d12 :: proc(pso: ^d3d12.IPipelineState) {
	if pso == nil do return
	if g_d3d == nil || g_d3d.fence == nil {
		pso->Release()
		return
	}
	fence_value := retire_fence_value_d3d12()
	if deferred_release_ready_d3d12(
		{kind = .Pipeline_State, fence_value = fence_value},
		completed_fence_value_d3d12(),
	) {
		pso->Release()
		return
	}
	append(
		&g_d3d.deferred_releases,
		D3D12_Deferred_Release{kind = .Pipeline_State, fence_value = fence_value, pso = pso},
	)
}

transition_texture_for_srv_d3d12 :: proc(entry: ^D3D12_Texture_Entry) {
	if g_d3d == nil || entry == nil || entry.resource == nil do return
	transition_texture_to_shader_resource_d3d12(entry)
}

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

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

	is_dynamic := .Host_Visible in desc.memory

	heap_props := d3d12.HEAP_PROPERTIES {
		Type = .UPLOAD if is_dynamic else .DEFAULT,
	}

	res_desc := d3d12.RESOURCE_DESC {
		Dimension = .BUFFER,
		Width = desc.size,
		Height = 1,
		DepthOrArraySize = 1,
		MipLevels = 1,
		SampleDesc = {Count = 1},
		Layout = .ROW_MAJOR,
	}

	initial_state: d3d12.RESOURCE_STATES =
		d3d12.RESOURCE_STATE_GENERIC_READ if is_dynamic else d3d12.RESOURCE_STATE_COMMON

	resource: ^d3d12.IResource
	result := g_d3d.device->CreateCommittedResource(
		&heap_props,
		{},
		&res_desc,
		initial_state,
		nil,
		d3d12.IResource_UUID,
		cast(^rawptr)&resource,
	)
	if result < 0 {
		log.errorf("gpu/d3d12: CreateCommittedResource (buffer) failed: 0x%08X", u32(result))
		return bk.NULL_BUFFER, false
	}

	entry := &g_d3d.buffers[handle]
	entry.resource = resource
	entry.gpu_address = resource->GetGPUVirtualAddress()
	entry.size = desc.size
	entry.usage = desc.usage
	entry.is_dynamic = is_dynamic
	entry.state = initial_state
	entry.active = true

	// Persistently map upload buffers
	if is_dynamic {
		mapped: rawptr
		read_range := d3d12.RANGE{} // No CPU reads
		result = resource->Map(0, &read_range, &mapped)
		if result >= 0 {
			entry.mapped_ptr = mapped
		} else {
			log.errorf("gpu/d3d12: persistent buffer Map failed: 0x%08X", u32(result))
		}
	}

	return handle, true
}

create_buffer_staged_d3d12 :: 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}

	// Create default heap buffer
	heap_props := d3d12.HEAP_PROPERTIES {
		Type = .DEFAULT,
	}
	res_desc := d3d12.RESOURCE_DESC {
		Dimension = .BUFFER,
		Width = u64(size),
		Height = 1,
		DepthOrArraySize = 1,
		MipLevels = 1,
		SampleDesc = {Count = 1},
		Layout = .ROW_MAJOR,
	}

	resource: ^d3d12.IResource
	result := g_d3d.device->CreateCommittedResource(
		&heap_props,
		{},
		&res_desc,
		{.COPY_DEST},
		nil,
		d3d12.IResource_UUID,
		cast(^rawptr)&resource,
	)
	if result < 0 {
		log.errorf(
			"gpu/d3d12: CreateCommittedResource (staged buffer) failed: 0x%08X",
			u32(result),
		)
		return bk.NULL_BUFFER, false
	}

	// Create upload buffer for the initial data
	upload_props := d3d12.HEAP_PROPERTIES {
		Type = .UPLOAD,
	}
	upload_resource: ^d3d12.IResource
	result = g_d3d.device->CreateCommittedResource(
		&upload_props,
		{},
		&res_desc,
		d3d12.RESOURCE_STATE_GENERIC_READ,
		nil,
		d3d12.IResource_UUID,
		cast(^rawptr)&upload_resource,
	)
	if result < 0 {
		resource->Release()
		return bk.NULL_BUFFER, false
	}

	// Map upload buffer and copy data
	mapped: rawptr
	read_range := d3d12.RANGE{}
	result = upload_resource->Map(0, &read_range, &mapped)
	if result >= 0 {
		mem.copy(mapped, data, size)
		upload_resource->Unmap(0, nil)
	}

	// Use a temporary command allocator + list for the copy
	// We reuse the current frame's allocator if a frame is active,
	// otherwise create a one-shot copy
	if g_d3d.frame_active {
		g_d3d.command_list->CopyBufferRegion(resource, 0, upload_resource, 0, u64(size))

		target_state := transition_copy_dest_to_generic_read_d3d12(resource)

		defer_release_resource_d3d12(upload_resource)

		entry := &g_d3d.buffers[handle]
		entry.resource = resource
		entry.gpu_address = resource->GetGPUVirtualAddress()
		entry.size = u64(size)
		entry.usage = usage
		entry.is_dynamic = false
		entry.state = target_state
		entry.active = true
	} else {
		// Outside a frame: create a temp command list, execute, and wait
		temp_alloc: ^d3d12.ICommandAllocator
		result = g_d3d.device->CreateCommandAllocator(
			.DIRECT,
			d3d12.ICommandAllocator_UUID,
			cast(^rawptr)&temp_alloc,
		)
		if result < 0 {
			upload_resource->Release()
			resource->Release()
			return bk.NULL_BUFFER, false
		}

		temp_list: ^d3d12.IGraphicsCommandList
		result = g_d3d.device->CreateCommandList(
			0,
			.DIRECT,
			temp_alloc,
			nil,
			d3d12.IGraphicsCommandList_UUID,
			cast(^rawptr)&temp_list,
		)
		if result < 0 {
			temp_alloc->Release()
			upload_resource->Release()
			resource->Release()
			return bk.NULL_BUFFER, false
		}

		temp_list->CopyBufferRegion(resource, 0, upload_resource, 0, u64(size))

		target_state := transition_copy_dest_to_generic_read_on_list_d3d12(temp_list, resource)

		temp_list->Close()

		cmd := cast(^d3d12.ICommandList)temp_list
		g_d3d.command_queue->ExecuteCommandLists(1, &cmd)
		wait_gpu_idle()

		temp_list->Release()
		temp_alloc->Release()
		upload_resource->Release()

		entry := &g_d3d.buffers[handle]
		entry.resource = resource
		entry.gpu_address = resource->GetGPUVirtualAddress()
		entry.size = u64(size)
		entry.usage = usage
		entry.is_dynamic = false
		entry.state = target_state
		entry.active = true
	}

	return handle, true
}

destroy_buffer_d3d12 :: proc(handle: bk.Buffer_Handle) {
	entry, ok := buffer_entry(handle)
	if !ok {return}
	if entry.resource != nil {
		if entry.mapped_ptr != nil {
			entry.resource->Unmap(0, nil)
		}
		defer_release_resource_d3d12(entry.resource)
	}
	entry^ = {}
}

map_buffer_d3d12 :: proc(handle: bk.Buffer_Handle) -> rawptr {
	entry, ok := buffer_entry(handle)
	if !ok || entry.resource == nil {return nil}

	if entry.mapped_ptr != nil {return entry.mapped_ptr}
	if !entry.is_dynamic {
		log.error("gpu/d3d12: map_buffer requires a Host_Visible buffer")
		return nil
	}

	mapped: rawptr
	read_range := d3d12.RANGE{}
	result := entry.resource->Map(0, &read_range, &mapped)
	if result < 0 {
		log.errorf("gpu/d3d12: buffer Map failed: 0x%08X", u32(result))
		return nil
	}
	entry.mapped_ptr = mapped
	return mapped
}

unmap_buffer_d3d12 :: proc(handle: bk.Buffer_Handle) {
	entry, ok := buffer_entry(handle)
	if !ok || entry.resource == nil {return}

	// Upload heap buffers stay persistently mapped until destroy.
	if entry.is_dynamic do return
	if entry.mapped_ptr != nil {
		entry.resource->Unmap(0, nil)
		entry.mapped_ptr = nil
	}
}

get_buffer_mapped_d3d12 :: proc(handle: bk.Buffer_Handle) -> rawptr {
	entry, ok := buffer_entry(handle)
	if !ok {return nil}

	// Upload buffers are persistently mapped
	if entry.mapped_ptr != nil {return entry.mapped_ptr}

	// Try to map
	return map_buffer_d3d12(handle)
}

bind_vertex_buffer_d3d12 :: proc(ctx: bk.Frame_Context, handle: bk.Buffer_Handle) {
	bind_vertex_buffer_slot_d3d12(ctx, 0, handle, 0, 48)
}

bind_vertex_buffer_slot_d3d12 :: proc(
	ctx: bk.Frame_Context,
	slot: u32,
	handle: bk.Buffer_Handle,
	offset: u64,
	stride: u32,
) {
	entry, ok := buffer_entry(handle)
	if !ok {return}
	if offset > entry.size {
		log.error("gpu/d3d12: vertex buffer bind offset exceeds buffer size")
		return
	}

	vbv := d3d12.VERTEX_BUFFER_VIEW {
		BufferLocation = entry.gpu_address + d3d12.GPU_VIRTUAL_ADDRESS(offset),
		SizeInBytes    = u32(entry.size - offset),
		StrideInBytes  = stride,
	}
	g_d3d.command_list->IASetVertexBuffers(slot, 1, &vbv)
}

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

	ibv := d3d12.INDEX_BUFFER_VIEW {
		BufferLocation = entry.gpu_address,
		SizeInBytes    = u32(entry.size),
		Format         = .R32_UINT,
	}
	g_d3d.command_list->IASetIndexBuffer(&ibv)
}

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

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

	dxgi_fmt := to_dxgi_format(desc.format)

	heap_props := d3d12.HEAP_PROPERTIES {
		Type = .DEFAULT,
	}
	tex_desc := d3d12.RESOURCE_DESC {
		Dimension = .TEXTURE2D,
		Width = u64(desc.width),
		Height = desc.height,
		DepthOrArraySize = 1,
		MipLevels = 1,
		Format = dxgi_fmt,
		SampleDesc = {Count = 1},
	}

	resource: ^d3d12.IResource
	result := g_d3d.device->CreateCommittedResource(
		&heap_props,
		{},
		&tex_desc,
		{.COPY_DEST},
		nil,
		d3d12.IResource_UUID,
		cast(^rawptr)&resource,
	)
	if result < 0 {
		log.errorf("gpu/d3d12: CreateCommittedResource (texture) failed: 0x%08X", u32(result))
		return bk.NULL_TEXTURE, false
	}

	// Upload pixel data if provided
	if pixels != nil {
		pixel_size := format_pixel_size(desc.format)
		upload_size := u64(desc.width) * u64(desc.height) * u64(pixel_size)

		// Get the actual row pitch the GPU needs (may be larger due to alignment)
		layout: d3d12.PLACED_SUBRESOURCE_FOOTPRINT
		num_rows: u32
		row_size: u64
		total_bytes: u64
		g_d3d.device->GetCopyableFootprints(
			&tex_desc,
			0,
			1,
			0,
			&layout,
			&num_rows,
			&row_size,
			&total_bytes,
		)

		// Create upload buffer
		upload_props := d3d12.HEAP_PROPERTIES {
			Type = .UPLOAD,
		}
		upload_desc := d3d12.RESOURCE_DESC {
			Dimension = .BUFFER,
			Width = total_bytes,
			Height = 1,
			DepthOrArraySize = 1,
			MipLevels = 1,
			SampleDesc = {Count = 1},
			Layout = .ROW_MAJOR,
		}

		upload_resource: ^d3d12.IResource
		result = g_d3d.device->CreateCommittedResource(
			&upload_props,
			{},
			&upload_desc,
			d3d12.RESOURCE_STATE_GENERIC_READ,
			nil,
			d3d12.IResource_UUID,
			cast(^rawptr)&upload_resource,
		)
		if result < 0 {
			resource->Release()
			return bk.NULL_TEXTURE, false
		}

		// Map and copy with proper row pitch
		mapped: rawptr
		read_range := d3d12.RANGE{}
		result = upload_resource->Map(0, &read_range, &mapped)
		if result >= 0 {
			src_row_pitch := desc.width * pixel_size
			dst_row_pitch := layout.Footprint.RowPitch
			for row in 0 ..< desc.height {
				src := rawptr(uintptr(pixels) + uintptr(row * src_row_pitch))
				dst := rawptr(uintptr(mapped) + uintptr(row * dst_row_pitch))
				mem.copy(dst, src, int(src_row_pitch))
			}
			upload_resource->Unmap(0, nil)
		}

		// Copy from upload to texture
		dst_loc := d3d12.TEXTURE_COPY_LOCATION {
			pResource = resource,
			Type      = .SUBRESOURCE_INDEX,
		}
		dst_loc.SubresourceIndex = 0

		src_loc := d3d12.TEXTURE_COPY_LOCATION {
			pResource = upload_resource,
			Type      = .PLACED_FOOTPRINT,
		}
		src_loc.PlacedFootprint = layout

		if g_d3d.frame_active {
			g_d3d.command_list->CopyTextureRegion(&dst_loc, 0, 0, 0, &src_loc, nil)
			_ = transition_copy_dest_to_shader_resource_d3d12(resource)
			defer_release_resource_d3d12(upload_resource)
		} else {
			temp_alloc: ^d3d12.ICommandAllocator
			g_d3d.device->CreateCommandAllocator(
				.DIRECT,
				d3d12.ICommandAllocator_UUID,
				cast(^rawptr)&temp_alloc,
			)
			temp_list: ^d3d12.IGraphicsCommandList
			g_d3d.device->CreateCommandList(
				0,
				.DIRECT,
				temp_alloc,
				nil,
				d3d12.IGraphicsCommandList_UUID,
				cast(^rawptr)&temp_list,
			)

			temp_list->CopyTextureRegion(&dst_loc, 0, 0, 0, &src_loc, nil)

			_ = transition_copy_dest_to_shader_resource_on_list_d3d12(temp_list, resource)
			temp_list->Close()

			cmd := cast(^d3d12.ICommandList)temp_list
			g_d3d.command_queue->ExecuteCommandLists(1, &cmd)
			wait_gpu_idle()

			temp_list->Release()
			temp_alloc->Release()
			upload_resource->Release()
		}
	} else {
		_ = transition_copy_dest_to_shader_resource_d3d12(resource)
	}

	// Create SRV in staging heap
	srv_idx, srv_ok := alloc_staging_cbv_srv_uav()
	if !srv_ok {
		defer_release_resource_d3d12(resource)
		return bk.NULL_TEXTURE, false
	}

	srv_desc := d3d12.SHADER_RESOURCE_VIEW_DESC {
		Format                  = dxgi_fmt,
		ViewDimension           = .TEXTURE2D,
		Shader4ComponentMapping = d3d12.DEFAULT_SHADER_4_COMPONENT_MAPPING,
	}
	srv_desc.Texture2D = {
		MostDetailedMip = 0,
		MipLevels       = 1,
	}
	g_d3d.device->CreateShaderResourceView(
		resource,
		&srv_desc,
		get_staging_cbv_srv_uav_cpu_handle(srv_idx),
	)

	entry := &g_d3d.textures[handle]
	entry.resource = resource
	entry.srv_index = srv_idx
	entry.has_srv = true
	entry.width = desc.width
	entry.height = desc.height
	entry.format = dxgi_fmt
	entry.state = d3d12.RESOURCE_STATE_ALL_SHADER_RESOURCE
	entry.active = true
	return handle, true
}

destroy_texture_d3d12 :: proc(handle: bk.Texture_Handle) {
	entry, ok := texture_entry(handle)
	if !ok {return}
	if entry.resource != nil {defer_release_resource_d3d12(entry.resource)}
	entry^ = {}
}

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

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

	dxgi_fmt := to_dxgi_format(desc.format)
	is_depth := .Depth_Stencil_Attachment in desc.usage

	texture_fmt := dxgi_fmt
	if is_depth && .Sampled in desc.usage {
		texture_fmt = depth_format_to_typeless(dxgi_fmt)
	}

	flags: d3d12.RESOURCE_FLAGS
	if .Color_Attachment in desc.usage {flags += {.ALLOW_RENDER_TARGET}}
	if .Depth_Stencil_Attachment in desc.usage {flags += {.ALLOW_DEPTH_STENCIL}}
	if is_depth && !(.Sampled in desc.usage) {flags += {.DENY_SHADER_RESOURCE}}

	heap_props := d3d12.HEAP_PROPERTIES {
		Type = .DEFAULT,
	}
	tex_desc := d3d12.RESOURCE_DESC {
		Dimension = .TEXTURE2D,
		Width = u64(desc.width),
		Height = desc.height,
		DepthOrArraySize = 1,
		MipLevels = 1,
		Format = texture_fmt,
		SampleDesc = {Count = 1},
		Flags = flags,
	}

	initial_state: d3d12.RESOURCE_STATES
	if is_depth {
		initial_state = {.DEPTH_WRITE}
	} else {
		initial_state = d3d12.RESOURCE_STATE_COMMON
	}

	resource: ^d3d12.IResource
	result := g_d3d.device->CreateCommittedResource(
		&heap_props,
		{},
		&tex_desc,
		initial_state,
		nil,
		d3d12.IResource_UUID,
		cast(^rawptr)&resource,
	)
	if result < 0 {
		log.errorf("gpu/d3d12: CreateCommittedResource (image) failed: 0x%08X", u32(result))
		return bk.NULL_TEXTURE, false
	}

	entry := &g_d3d.textures[handle]
	entry.resource = resource
	entry.width = desc.width
	entry.height = desc.height
	entry.format = dxgi_fmt
	entry.usage = desc.usage
	entry.state = initial_state
	entry.deny_srv = .DENY_SHADER_RESOURCE in flags
	entry.active = true
	return handle, true
}

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

	dxgi_fmt := to_dxgi_format(format)
	is_depth := .Depth in aspect || .Stencil in aspect

	if is_depth {
		// Allocate DSV in DSV heap (offset by 1 since index 0 is the default depth buffer)
		dsv_idx := u32(texture) + 1
		dsv_desc := d3d12.DEPTH_STENCIL_VIEW_DESC {
			Format        = dxgi_fmt,
			ViewDimension = .TEXTURE2D,
		}
		g_d3d.device->CreateDepthStencilView(
			entry.resource,
			&dsv_desc,
			get_dsv_cpu_handle(dsv_idx),
		)
		entry.dsv_index = dsv_idx
		entry.has_dsv = true

		// Also create SRV for depth sampling (shadow maps), if resource allows it
		if !entry.deny_srv {
			srv_idx, srv_ok := alloc_staging_cbv_srv_uav()
			if srv_ok {
				srv_fmt := depth_format_to_srv(dxgi_fmt)
				srv_desc := d3d12.SHADER_RESOURCE_VIEW_DESC {
					Format                  = srv_fmt,
					ViewDimension           = .TEXTURE2D,
					Shader4ComponentMapping = d3d12.DEFAULT_SHADER_4_COMPONENT_MAPPING,
				}
				srv_desc.Texture2D = {
					MostDetailedMip = 0,
					MipLevels       = 1,
				}
				g_d3d.device->CreateShaderResourceView(
					entry.resource,
					&srv_desc,
					get_staging_cbv_srv_uav_cpu_handle(srv_idx),
				)
				entry.srv_index = srv_idx
				entry.has_srv = true
			}
		}
	} else {
		if .Color_Attachment in entry.usage {
			rtv_idx := u32(bk.MAX_FRAMES_IN_FLIGHT) + u32(texture)
			rtv_desc := d3d12.RENDER_TARGET_VIEW_DESC {
				Format        = dxgi_fmt,
				ViewDimension = .TEXTURE2D,
			}
			g_d3d.device->CreateRenderTargetView(
				entry.resource,
				&rtv_desc,
				get_rtv_cpu_handle(rtv_idx),
			)
			entry.rtv_index = rtv_idx
			entry.has_rtv = true
		}

		if .Sampled in entry.usage {
			// Create SRV
			srv_idx, srv_ok := alloc_staging_cbv_srv_uav()
			if !srv_ok {return false}

			srv_desc := d3d12.SHADER_RESOURCE_VIEW_DESC {
				Format                  = dxgi_fmt,
				ViewDimension           = .TEXTURE2D,
				Shader4ComponentMapping = d3d12.DEFAULT_SHADER_4_COMPONENT_MAPPING,
			}
			srv_desc.Texture2D = {
				MostDetailedMip = 0,
				MipLevels       = 1,
			}
			g_d3d.device->CreateShaderResourceView(
				entry.resource,
				&srv_desc,
				get_staging_cbv_srv_uav_cpu_handle(srv_idx),
			)
			entry.srv_index = srv_idx
			entry.has_srv = true
		}
	}

	return true
}

read_texture_rgba8_d3d12 :: proc(desc: bk.Readback_Texture_Desc, out: []u8) -> bool {
	entry, ok := texture_entry(desc.texture)
	if !ok do return false
	if g_d3d.frame_active {
		log.error("gpu/d3d12: read_texture_rgba8 requires no active frame")
		return false
	}
	if desc.width != entry.width || desc.height != entry.height {
		log.error("gpu/d3d12: 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/d3d12: read_texture_rgba8 output buffer is too small")
		return false
	}
	if entry.format != .R8G8B8A8_UNORM && entry.format != .R8G8B8A8_UNORM_SRGB && entry.format != .B8G8R8A8_UNORM_SRGB {
		log.error("gpu/d3d12: read_texture_rgba8 requires an 8-bit RGBA/BGRA color texture")
		return false
	}

	texture_desc := d3d12.RESOURCE_DESC {
		Dimension = .TEXTURE2D,
		Width = u64(desc.width),
		Height = desc.height,
		DepthOrArraySize = 1,
		MipLevels = 1,
		Format = entry.format,
		SampleDesc = {Count = 1},
	}
	layout: d3d12.PLACED_SUBRESOURCE_FOOTPRINT
	num_rows: u32
	row_size: u64
	total_bytes: u64
	g_d3d.device->GetCopyableFootprints(&texture_desc, 0, 1, 0, &layout, &num_rows, &row_size, &total_bytes)

	readback_props := d3d12.HEAP_PROPERTIES {Type = .READBACK}
	readback_desc := d3d12.RESOURCE_DESC {
		Dimension = .BUFFER,
		Width = total_bytes,
		Height = 1,
		DepthOrArraySize = 1,
		MipLevels = 1,
		SampleDesc = {Count = 1},
		Layout = .ROW_MAJOR,
	}
	readback: ^d3d12.IResource
	result := g_d3d.device->CreateCommittedResource(
		&readback_props,
		{},
		&readback_desc,
		{.COPY_DEST},
		nil,
		d3d12.IResource_UUID,
		cast(^rawptr)&readback,
	)
	if result < 0 {
		log.errorf("gpu/d3d12: read_texture_rgba8 readback resource failed: 0x%08X", u32(result))
		return false
	}
	defer readback->Release()

	temp_alloc: ^d3d12.ICommandAllocator
	result = g_d3d.device->CreateCommandAllocator(
		.DIRECT,
		d3d12.ICommandAllocator_UUID,
		cast(^rawptr)&temp_alloc,
	)
	if result < 0 do return false
	defer temp_alloc->Release()

	temp_list: ^d3d12.IGraphicsCommandList
	result = g_d3d.device->CreateCommandList(
		0,
		.DIRECT,
		temp_alloc,
		nil,
		d3d12.IGraphicsCommandList_UUID,
		cast(^rawptr)&temp_list,
	)
	if result < 0 do return false
	defer temp_list->Release()

	transition_resource_on_command_list_d3d12(temp_list, entry.resource, entry.state, {.COPY_SOURCE})

	dst_loc := d3d12.TEXTURE_COPY_LOCATION {
		pResource = readback,
		Type      = .PLACED_FOOTPRINT,
	}
	dst_loc.PlacedFootprint = layout
	src_loc := d3d12.TEXTURE_COPY_LOCATION {
		pResource = entry.resource,
		Type      = .SUBRESOURCE_INDEX,
	}
	src_loc.SubresourceIndex = 0
	temp_list->CopyTextureRegion(&dst_loc, 0, 0, 0, &src_loc, nil)
	transition_resource_on_command_list_d3d12(temp_list, entry.resource, {.COPY_SOURCE}, entry.state)
	temp_list->Close()

	cmd := cast(^d3d12.ICommandList)temp_list
	g_d3d.command_queue->ExecuteCommandLists(1, &cmd)
	wait_gpu_idle()

	mapped: rawptr
	read_range := d3d12.RANGE {Begin = 0, End = uint(total_bytes)}
	result = readback->Map(0, &read_range, &mapped)
	if result < 0 {
		log.errorf("gpu/d3d12: read_texture_rgba8 map failed: 0x%08X", u32(result))
		return false
	}
	defer readback->Unmap(0, nil)

	src_row_pitch := int(layout.Footprint.RowPitch)
	dst_row_pitch := int(desc.width * 4)
	for row in 0..<int(desc.height) {
		src := rawptr(uintptr(mapped) + uintptr(row * src_row_pitch))
		dst := rawptr(uintptr(raw_data(out)) + uintptr(row * dst_row_pitch))
		mem.copy(dst, src, dst_row_pitch)
	}
	return true
}

destroy_image_d3d12 :: proc(handle: bk.Texture_Handle) {
	destroy_texture_d3d12(handle)
}

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

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

	staging_idx, staging_ok := alloc_staging_sampler()
	if !staging_ok {return bk.NULL_SAMPLER, false}

	filter := to_d3d12_filter(desc.mag_filter, desc.min_filter, desc.mipmap_mode)
	if desc.enable_compare {
		filter = .COMPARISON_MIN_MAG_LINEAR_MIP_POINT
	}

	sampler_desc := d3d12.SAMPLER_DESC {
		Filter         = filter,
		AddressU       = to_d3d12_address_mode(desc.address_mode_u),
		AddressV       = to_d3d12_address_mode(desc.address_mode_v),
		AddressW       = to_d3d12_address_mode(desc.address_mode_u),
		MaxAnisotropy  = 16 if desc.enable_aniso else 1,
		ComparisonFunc = to_d3d12_compare_func(desc.compare_op) if desc.enable_compare else .ALWAYS,
		MinLOD         = 0,
		MaxLOD         = d3d12.FLOAT32_MAX,
	}
	sampler_desc.BorderColor = {0, 0, 0, 1}

	cpu_handle := get_staging_sampler_cpu_handle(staging_idx)
	g_d3d.device->CreateSampler(&sampler_desc, cpu_handle)

	entry := &g_d3d.samplers[handle]
	entry.cpu_handle = cpu_handle
	entry.staging_index = staging_idx
	entry.active = true
	return handle, true
}

destroy_sampler_d3d12 :: proc(handle: bk.Sampler_Handle) {
	entry, ok := sampler_entry(handle)
	if !ok {return}
	entry.active = false
}

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

create_shader_module_d3d12 :: 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 != .HLSL {
		log.errorf("gpu/d3d12: shader '%s' must be HLSL", desc.name)
		return bk.NULL_SHADER, false
	}

	profile: cstring
	switch desc.stage {
	case .Vertex:
		profile = "vs_5_0"
	case .Fragment:
		profile = "ps_5_0"
	case .Compute:
		profile = "cs_5_0"
	}

	compile_flags: u32 = 0
	when ODIN_DEBUG {
		compile_flags = transmute(u32)d3dc.D3DCOMPILE{.DEBUG, .SKIP_OPTIMIZATION}
	}

	shader_blob: ^d3dc.ID3DBlob
	error_blob: ^d3dc.ID3DBlob
	name_cstr := strings.clone_to_cstring(desc.name, context.temp_allocator)
	entry_point: cstring =
		"main" if desc.entry == "" else strings.clone_to_cstring(desc.entry, context.temp_allocator)

	result := d3dc.Compile(
		raw_data(desc.data),
		uint(len(desc.data)),
		name_cstr,
		nil,
		nil,
		entry_point,
		profile,
		compile_flags,
		0,
		&shader_blob,
		&error_blob,
	)

	if result < 0 {
		if error_blob != nil {
			err_msg := cstring(error_blob->GetBufferPointer())
			log.errorf("gpu/d3d12: D3DCompile failed for '%s': %s", desc.name, err_msg)
			error_blob->Release()
		} else {
			log.errorf("gpu/d3d12: D3DCompile failed for '%s': 0x%08X", desc.name, u32(result))
		}
		return bk.NULL_SHADER, false
	}
	if error_blob != nil {error_blob->Release()}

	// Step 3: Copy bytecode (D3D12 embeds bytecode in PSO, no shader objects)
	blob_ptr := shader_blob->GetBufferPointer()
	blob_size := shader_blob->GetBufferSize()

	bytecode := make([]byte, blob_size)
	mem.copy(raw_data(bytecode), blob_ptr, int(blob_size))
	shader_blob->Release()

	entry := &g_d3d.shaders[handle]
	entry.bytecode = bytecode
	entry.stage = desc.stage
	entry.active = true

	return handle, true
}

destroy_shader_d3d12 :: proc(handle: bk.Shader_Handle) {
	entry, ok := shader_entry(handle)
	if !ok {return}
	delete(entry.bytecode)
	entry^ = {}
}

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

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

	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/d3d12: graphics pipeline shader handle is invalid")
		return bk.NULL_PIPELINE, false
	}

	entry := &g_d3d.pipelines[handle]
	entry.vert_bytecode = make([]byte, len(vert_entry.bytecode))
	entry.frag_bytecode = make([]byte, len(frag_entry.bytecode))
	mem.copy(
		raw_data(entry.vert_bytecode),
		raw_data(vert_entry.bytecode),
		len(vert_entry.bytecode),
	)
	mem.copy(
		raw_data(entry.frag_bytecode),
		raw_data(frag_entry.bytecode),
		len(frag_entry.bytecode),
	)
	entry.vertex_binding_count = u32(
		min(len(desc.vertex_bindings), len(entry.vertex_bindings)),
	)
	for i in 0 ..< int(entry.vertex_binding_count) {
		entry.vertex_bindings[i] = desc.vertex_bindings[i]
	}
	entry.vertex_attribute_count = u32(
		min(len(desc.vertex_attributes), len(entry.vertex_attributes)),
	)
	for i in 0 ..< int(entry.vertex_attribute_count) {
		entry.vertex_attributes[i] = desc.vertex_attributes[i]
	}
	entry.pipeline_topology = desc.topology
	entry.topology = to_d3d12_topology(desc.topology)
	entry.root_sig = g_d3d.graphics_root_sig
	entry.cull_mode = desc.cull_mode
	entry.front_face = desc.front_face
	entry.enable_blending = desc.enable_blending
	entry.blend_mode = desc.blend_mode
	entry.enable_depth_test = desc.enable_depth_test
	entry.depth_format = desc.depth_format
	entry.stencil = desc.stencil
	entry.depth_only = desc.depth_only
	entry.color_attachment_count = desc.color_attachment_count
	entry.color_formats = desc.color_formats
	entry.color_write_masks = desc.color_write_masks
	entry.push_constant_size = desc.push_constant_size
	entry.push_constant_stages = desc.push_constant_stages
	entry.no_draw = desc.cull_mode == .Front_And_Back
	entry.is_compute = false

	pso, pso_ok := create_graphics_pso_for_entry_d3d12(entry, 0, 0)
	if !pso_ok {
		delete(entry.vert_bytecode)
		delete(entry.frag_bytecode)
		entry^ = {}
		return bk.NULL_PIPELINE, false
	}
	entry.pso = pso
	entry.active = true
	return handle, true
}

@(private)
input_rate_for_binding_d3d12 :: proc(
	entry: ^D3D12_Pipeline_Entry,
	binding: u32,
) -> bk.Vertex_Input_Rate {
	for i in 0 ..< int(entry.vertex_binding_count) {
		if entry.vertex_bindings[i].binding == binding {
			return entry.vertex_bindings[i].input_rate
		}
	}
	return .Vertex
}

@(private)
create_graphics_pso_for_entry_d3d12 :: proc(
	entry: ^D3D12_Pipeline_Entry,
	constant: i32,
	slope: f32,
) -> (
	^d3d12.IPipelineState,
	bool,
) {
	if g_d3d == nil ||
	   entry == nil ||
	   len(entry.vert_bytecode) == 0 ||
	   len(entry.frag_bytecode) == 0 {
		return nil, false
	}

	input_descs: [16]d3d12.INPUT_ELEMENT_DESC
	for i in 0 ..< int(entry.vertex_attribute_count) {
		attr := &entry.vertex_attributes[i]
		input_rate := input_rate_for_binding_d3d12(entry, attr.binding)
		input_descs[i] = d3d12.INPUT_ELEMENT_DESC {
			SemanticName         = "TEXCOORD",
			SemanticIndex        = u32(attr.location),
			Format               = to_dxgi_vertex_format(attr.format),
			InputSlot            = attr.binding,
			AlignedByteOffset    = attr.offset,
			InputSlotClass       = .PER_INSTANCE_DATA if input_rate == .Instance else .PER_VERTEX_DATA,
			InstanceDataStepRate = 1 if input_rate == .Instance else 0,
		}
	}

	raster_desc := d3d12.RASTERIZER_DESC {
		FillMode              = .SOLID,
		CullMode              = to_d3d12_cull_mode(entry.cull_mode),
		FrontCounterClockwise = d3d12.BOOL(entry.front_face == .Counter_Clockwise),
		DepthBias             = constant,
		DepthBiasClamp        = 0,
		SlopeScaledDepthBias  = slope,
		DepthClipEnable       = true,
	}

	factors := bk.blend_factors(entry.blend_mode)
	blend_desc: d3d12.BLEND_DESC
	color_count := entry.color_attachment_count
	if !entry.depth_only && color_count == 0 {
		color_count = 1
	}
	for i in 0..<color_count {
		mask := entry.color_write_masks[i]
		if mask == 0 {
			mask = bk.COLOR_WRITE_MASK_ALL
		}
		blend_desc.RenderTarget[i] = d3d12.RENDER_TARGET_BLEND_DESC {
			BlendEnable           = d3d12.BOOL(entry.enable_blending),
			SrcBlend              = to_d3d12_blend_factor(factors.src_color),
			DestBlend             = to_d3d12_blend_factor(factors.dst_color),
			BlendOp               = .ADD,
			SrcBlendAlpha         = to_d3d12_blend_factor(factors.src_alpha),
			DestBlendAlpha        = to_d3d12_blend_factor(factors.dst_alpha),
			BlendOpAlpha          = .ADD,
			RenderTargetWriteMask = mask,
		}
	}

	ds_desc := d3d12.DEPTH_STENCIL_DESC {
		DepthEnable      = d3d12.BOOL(entry.enable_depth_test || entry.depth_only),
		DepthWriteMask   = .ALL,
		DepthFunc        = .LESS,
		StencilEnable    = d3d12.BOOL(entry.stencil.enable),
		StencilReadMask  = entry.stencil.read_mask,
		StencilWriteMask = entry.stencil.write_mask,
		FrontFace        = to_d3d12_stencil_face(entry.stencil.front),
		BackFace         = to_d3d12_stencil_face(entry.stencil.back),
	}

	pso_desc := d3d12.GRAPHICS_PIPELINE_STATE_DESC {
		pRootSignature = g_d3d.graphics_root_sig,
		VS = d3d12.SHADER_BYTECODE {
			pShaderBytecode = raw_data(entry.vert_bytecode),
			BytecodeLength = uint(len(entry.vert_bytecode)),
		},
		PS = d3d12.SHADER_BYTECODE {
			pShaderBytecode = raw_data(entry.frag_bytecode),
			BytecodeLength = uint(len(entry.frag_bytecode)),
		},
		BlendState = blend_desc,
		SampleMask = 0xFFFFFFFF,
		RasterizerState = raster_desc,
		DepthStencilState = ds_desc,
		InputLayout = d3d12.INPUT_LAYOUT_DESC {
			pInputElementDescs = &input_descs[0] if entry.vertex_attribute_count > 0 else nil,
			NumElements = entry.vertex_attribute_count,
		},
		PrimitiveTopologyType = to_d3d12_topology_type(entry.pipeline_topology),
		NumRenderTargets = 0 if entry.depth_only else color_count,
		DSVFormat = to_dxgi_format(entry.depth_format) if entry.depth_format != .Undefined else .D32_FLOAT,
		SampleDesc = {Count = 1},
	}
	if !entry.depth_only {
		for i in 0..<color_count {
			pso_desc.RTVFormats[i] =
				to_dxgi_format(entry.color_formats[i]) if entry.color_formats[i] != .Undefined else .R8G8B8A8_UNORM
		}
	}

	pso: ^d3d12.IPipelineState
	result := g_d3d.device->CreateGraphicsPipelineState(
		&pso_desc,
		d3d12.IPipelineState_UUID,
		cast(^rawptr)&pso,
	)
	if result < 0 {
		log.errorf("gpu/d3d12: CreateGraphicsPipelineState failed: 0x%08X", u32(result))
		when ODIN_DEBUG {dump_debug_messages()}
		return nil, false
	}
	return pso, true
}

depth_bias_pso_d3d12 :: proc(
	entry: ^D3D12_Pipeline_Entry,
	constant: i32,
	slope: f32,
) -> (
	^d3d12.IPipelineState,
	bool,
) {
	if entry == nil {return nil, false}
	if constant == 0 && slope == 0 {
		return entry.pso, entry.pso != nil
	}
	for i in 0 ..< int(entry.depth_bias_count) {
		variant := &entry.depth_bias_variants[i]
		if variant.active && variant.constant == constant && variant.slope == slope {
			return variant.pso, variant.pso != nil
		}
	}
	if entry.depth_bias_count >= MAX_DEPTH_BIAS_PSO_VARIANTS {
		log.error("gpu/d3d12: depth-bias PSO variant cache exhausted")
		return nil, false
	}
	pso, ok := create_graphics_pso_for_entry_d3d12(entry, constant, slope)
	if !ok {return nil, false}
	variant := &entry.depth_bias_variants[entry.depth_bias_count]
	variant.constant = constant
	variant.slope = slope
	variant.pso = pso
	variant.active = true
	entry.depth_bias_count += 1
	return pso, true
}

destroy_graphics_pipeline_d3d12 :: proc(handle: bk.Pipeline_Handle) {
	entry, ok := pipeline_entry(handle)
	if !ok {return}
	if entry.pso != nil {defer_release_pso_d3d12(entry.pso)}
	for i in 0 ..< int(entry.depth_bias_count) {
		if entry.depth_bias_variants[i].pso != nil {
			defer_release_pso_d3d12(entry.depth_bias_variants[i].pso)
		}
	}
	delete(entry.vert_bytecode)
	delete(entry.frag_bytecode)
	entry^ = {}
}

bind_graphics_pipeline_d3d12 :: proc(ctx: bk.Frame_Context, handle: bk.Pipeline_Handle) {
	if g_d3d == nil {return}
	entry, ok := pipeline_entry(handle)
	if !ok {return}

	pso := entry.pso
	if biased_pso, bias_ok := depth_bias_pso_d3d12(
		entry,
		g_d3d.pending_depth_bias_constant,
		g_d3d.pending_depth_bias_slope,
	); bias_ok {
		pso = biased_pso
	}
	g_d3d.command_list->SetPipelineState(pso)
	g_d3d.command_list->IASetPrimitiveTopology(entry.topology)
	g_d3d.command_list->OMSetStencilRef(u32(entry.stencil.reference))
	g_d3d.current_pipeline = handle
}

push_constants_d3d12 :: proc(
	ctx: bk.Frame_Context,
	pipeline: bk.Pipeline_Handle,
	stages: bk.Shader_Stage_Flags,
	offset, size: u32,
	data: rawptr,
) {
	if g_d3d == nil || data == nil || size == 0 {return}

	num_32bit := (size + 3) / 4
	offset_32bit := offset / 4
	g_d3d.command_list->SetGraphicsRoot32BitConstants(0, num_32bit, data, offset_32bit)
}

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

create_compute_pipeline_d3d12 :: 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}

	pso_desc := d3d12.COMPUTE_PIPELINE_STATE_DESC {
		pRootSignature = g_d3d.compute_root_sig,
		CS = d3d12.SHADER_BYTECODE {
			pShaderBytecode = raw_data(shader_entry.bytecode),
			BytecodeLength = uint(len(shader_entry.bytecode)),
		},
	}

	pso: ^d3d12.IPipelineState
	result := g_d3d.device->CreateComputePipelineState(
		&pso_desc,
		d3d12.IPipelineState_UUID,
		cast(^rawptr)&pso,
	)
	if result < 0 {
		log.errorf("gpu/d3d12: CreateComputePipelineState failed: 0x%08X", u32(result))
		return bk.NULL_PIPELINE, false
	}

	entry := &g_d3d.pipelines[handle]
	entry.pso = pso
	entry.root_sig = g_d3d.compute_root_sig
	entry.push_constant_size = push_constant_size
	entry.is_compute = true
	entry.active = true
	return handle, true
}

destroy_compute_pipeline_d3d12 :: proc(handle: bk.Pipeline_Handle) {
	destroy_graphics_pipeline_d3d12(handle)
}

bind_compute_pipeline_d3d12 :: proc(ctx: bk.Frame_Context, handle: bk.Pipeline_Handle) {
	if g_d3d == nil {return}
	entry, ok := pipeline_entry(handle)
	if !ok {return}
	g_d3d.command_list->SetPipelineState(entry.pso)
	g_d3d.command_list->SetComputeRootSignature(entry.root_sig)
}

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

draw_d3d12 :: proc(
	ctx: bk.Frame_Context,
	vertex_count, instance_count: u32,
	first_vertex: u32,
	first_instance: u32,
) {
	if g_d3d == nil {return}
	if entry, ok := pipeline_entry(g_d3d.current_pipeline); ok && entry.no_draw {
		return
	}
	g_d3d.command_list->DrawInstanced(
		vertex_count,
		max(instance_count, 1),
		first_vertex,
		first_instance,
	)
}

draw_indexed_d3d12 :: proc(
	ctx: bk.Frame_Context,
	index_count, instance_count: u32,
	first_index: u32,
	vertex_offset: i32,
	first_instance: u32,
) {
	if g_d3d == nil {return}
	if entry, ok := pipeline_entry(g_d3d.current_pipeline); ok && entry.no_draw {
		return
	}
	g_d3d.command_list->DrawIndexedInstanced(
		index_count,
		max(instance_count, 1),
		first_index,
		vertex_offset,
		first_instance,
	)
}

@(private)
d3d12_indirect_signature :: proc(indexed: bool) -> (^d3d12.ICommandSignature, bool) {
	if g_d3d == nil || g_d3d.device == nil {
		return nil, false
	}
	if indexed {
		if g_d3d.draw_indexed_indirect_signature != nil {
			return g_d3d.draw_indexed_indirect_signature, true
		}
	} else if g_d3d.draw_indirect_signature != nil {
		return g_d3d.draw_indirect_signature, true
	}

	arg := d3d12.INDIRECT_ARGUMENT_DESC {
		Type = .DRAW_INDEXED if indexed else .DRAW,
	}
	desc := d3d12.COMMAND_SIGNATURE_DESC {
		ByteStride = u32(size_of(bk.Indirect_Draw_Indexed_Args)) if indexed else u32(size_of(bk.Indirect_Draw_Args)),
		NumArgumentDescs = 1,
		pArgumentDescs = &arg,
	}
	signature: ^d3d12.ICommandSignature
	result := g_d3d.device->CreateCommandSignature(
		&desc,
		nil,
		d3d12.ICommandSignature_UUID,
		cast(^rawptr)&signature,
	)
	if result < 0 {
		log.errorf("gpu/d3d12: CreateCommandSignature failed: 0x%08X", u32(result))
		return nil, false
	}
	if indexed {
		g_d3d.draw_indexed_indirect_signature = signature
	} else {
		g_d3d.draw_indirect_signature = signature
	}
	return signature, true
}

@(private)
transition_buffer_for_indirect_argument_d3d12 :: proc(entry: ^D3D12_Buffer_Entry) {
	transition_buffer_to_indirect_argument_d3d12(entry)
}

draw_indirect_d3d12 :: proc(
	ctx: bk.Frame_Context,
	argument_buffer: bk.Buffer_Handle,
	argument_offset: u64,
	draw_count: u32,
	stride: u32,
) {
	_ = ctx
	_ = stride
	if g_d3d == nil {return}
	if draw_count != 1 {
		log.error("gpu/d3d12: draw_indirect currently supports one command")
		return
	}
	if entry, ok := pipeline_entry(g_d3d.current_pipeline); ok && entry.no_draw {
		return
	}
	buffer, buffer_ok := buffer_entry(argument_buffer)
	if !buffer_ok {
		log.error("gpu/d3d12: draw_indirect received invalid argument buffer")
		return
	}
	signature, signature_ok := d3d12_indirect_signature(false)
	if !signature_ok {
		return
	}
	transition_buffer_for_indirect_argument_d3d12(buffer)
	g_d3d.command_list->ExecuteIndirect(signature, draw_count, buffer.resource, argument_offset, nil, 0)
}

draw_indexed_indirect_d3d12 :: proc(
	ctx: bk.Frame_Context,
	argument_buffer: bk.Buffer_Handle,
	argument_offset: u64,
	draw_count: u32,
	stride: u32,
) {
	_ = ctx
	_ = stride
	if g_d3d == nil {return}
	if draw_count != 1 {
		log.error("gpu/d3d12: draw_indexed_indirect currently supports one command")
		return
	}
	if entry, ok := pipeline_entry(g_d3d.current_pipeline); ok && entry.no_draw {
		return
	}
	buffer, buffer_ok := buffer_entry(argument_buffer)
	if !buffer_ok {
		log.error("gpu/d3d12: draw_indexed_indirect received invalid argument buffer")
		return
	}
	signature, signature_ok := d3d12_indirect_signature(true)
	if !signature_ok {
		return
	}
	transition_buffer_for_indirect_argument_d3d12(buffer)
	g_d3d.command_list->ExecuteIndirect(signature, draw_count, buffer.resource, argument_offset, nil, 0)
}

@(private)
to_d3d12_blend_factor :: proc(factor: bk.Blend_Factor) -> d3d12.BLEND {
	switch factor {
	case .Zero:
		return .ZERO
	case .One:
		return .ONE
	case .Src_Alpha:
		return .SRC_ALPHA
	case .One_Minus_Src_Alpha:
		return .INV_SRC_ALPHA
	}
	return .ONE
}

// ============================================================================
// Compute dispatch
// ============================================================================

dispatch_compute_d3d12 :: proc(ctx: bk.Frame_Context, groups_x, groups_y, groups_z: u32) {
	if g_d3d == nil {return}
	g_d3d.command_list->Dispatch(groups_x, groups_y, groups_z)
}

compute_barrier_d3d12 :: proc(ctx: bk.Frame_Context) {
	if g_d3d == nil {return}
	// UAV barrier for compute writes -> reads
	barrier := d3d12.RESOURCE_BARRIER {
		Type = .UAV,
	}
	g_d3d.command_list->ResourceBarrier(1, &barrier)
}

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

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

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

destroy_descriptor_set_layout_d3d12 :: proc(handle: bk.Descriptor_Handle) {
	entry, ok := descriptor_entry_of_kind(handle, .Set_Layout)
	if !ok {return}
	entry.active = false
}

create_descriptor_pool_d3d12 :: 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}
	entry := &g_d3d.descriptors[handle]
	entry.kind = .Pool
	entry.active = true
	return handle, true
}

destroy_descriptor_pool_d3d12 :: proc(handle: bk.Descriptor_Handle) {
	entry, ok := descriptor_entry_of_kind(handle, .Pool)
	if !ok {return}
	entry.active = false
}

allocate_descriptor_set_d3d12 :: proc(
	pool: bk.Descriptor_Handle,
	layout: bk.Descriptor_Handle,
) -> (
	bk.Descriptor_Handle,
	bool,
) {
	if _, pool_ok := descriptor_entry_of_kind(pool, .Pool);
	   !pool_ok {return bk.NULL_DESCRIPTOR, false}
	layout_entry, layout_ok := descriptor_entry_of_kind(layout, .Set_Layout)
	if !layout_ok {return bk.NULL_DESCRIPTOR, false}

	handle, ok := alloc_descriptor_handle()
	if !ok {return bk.NULL_DESCRIPTOR, false}

	entry := &g_d3d.descriptors[handle]
	entry.kind = .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.active = true
	return handle, true
}

bind_descriptor_set_d3d12 :: proc(
	ctx: bk.Frame_Context,
	pipeline_handle: bk.Pipeline_Handle,
	set: bk.Descriptor_Handle,
	index: u32,
) {
	if g_d3d == nil {return}
	entry, entry_ok := descriptor_entry_of_kind(set, .Set)
	if !entry_ok {return}

	p, pipeline_ok := pipeline_entry(pipeline_handle)
	if !pipeline_ok {return}
	is_compute := p.is_compute

	for i in 0 ..< int(entry.binding_count) {
		b := &entry.bindings[i]

		switch b.type {
		case .Combined_Image_Sampler:
			// Copy SRV from staging to GPU heap
			if b.texture != bk.NULL_TEXTURE {
				if tex_entry, tex_ok := texture_entry(b.texture); tex_ok && tex_entry.has_srv {
					transition_texture_for_srv_d3d12(tex_entry)
					cpu, gpu, alloc_ok := alloc_gpu_cbv_srv_uav(1)
					if alloc_ok {
						g_d3d.device->CopyDescriptorsSimple(
							1,
							cpu,
							get_staging_cbv_srv_uav_cpu_handle(tex_entry.srv_index),
							.CBV_SRV_UAV,
						)
						if is_compute {
							g_d3d.command_list->SetComputeRootDescriptorTable(1, gpu)
						} else {
							g_d3d.command_list->SetGraphicsRootDescriptorTable(1, gpu)
						}
					}
				}
			}
			// Copy sampler from staging to GPU heap
			if b.sampler != bk.NULL_SAMPLER {
				if sam_entry, sam_ok := sampler_entry(b.sampler); sam_ok {
					cpu, gpu, alloc_ok := alloc_gpu_sampler(1)
					if alloc_ok {
						g_d3d.device->CopyDescriptorsSimple(1, cpu, sam_entry.cpu_handle, .SAMPLER)
						if is_compute {
							g_d3d.command_list->SetComputeRootDescriptorTable(2, gpu)
						} else {
							g_d3d.command_list->SetGraphicsRootDescriptorTable(2, gpu)
						}
					}
				}
			}
		case .Uniform_Buffer:
			if b.buffer != bk.NULL_BUFFER {
				if buf_entry, buf_ok := buffer_entry(b.buffer); buf_ok {
					// Create CBV in GPU heap
					cpu, gpu, alloc_ok := alloc_gpu_cbv_srv_uav(1)
					if alloc_ok {
						cbv_desc := d3d12.CONSTANT_BUFFER_VIEW_DESC {
							BufferLocation = buf_entry.gpu_address,
							SizeInBytes    = u32((b.buf_size + 255) & ~u64(255)), // 256-byte aligned
						}
						g_d3d.device->CreateConstantBufferView(&cbv_desc, cpu)
						if is_compute {
							g_d3d.command_list->SetComputeRootDescriptorTable(3, gpu)
						} else {
							g_d3d.command_list->SetGraphicsRootDescriptorTable(3, gpu)
						}
					}
				}
			}
		case .Storage_Buffer:
			log.error(
				"gpu/d3d12: compute storage-buffer descriptors are not supported by the current root signature",
			)
		}
	}
}

update_descriptor_image_d3d12 :: proc(
	set: bk.Descriptor_Handle,
	binding: u32,
	texture: bk.Texture_Handle,
	sampler: bk.Sampler_Handle,
	layout: bk.Image_Layout,
) {
	entry, ok := descriptor_entry_of_kind(set, .Set)
	if !ok {return}

	for i in 0 ..< int(entry.binding_count) {
		if entry.bindings[i].binding == binding {
			entry.bindings[i].texture = texture
			entry.bindings[i].sampler = sampler
			return
		}
	}
}

update_descriptor_buffer_d3d12 :: proc(
	set: bk.Descriptor_Handle,
	binding: u32,
	buffer: bk.Buffer_Handle,
	size: u64,
) {
	entry, ok := descriptor_entry_of_kind(set, .Set)
	if !ok {return}

	for i in 0 ..< int(entry.binding_count) {
		if entry.bindings[i].binding == binding {
			entry.bindings[i].buffer = buffer
			entry.bindings[i].buf_size = size
			return
		}
	}
}

// ============================================================================
// Render pass / framebuffer operations
// ============================================================================

create_render_pass_d3d12 :: 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}

	entry := &g_d3d.render_passes[handle]
	entry.desc = desc
	entry.active = true
	return handle, true
}

destroy_render_pass_d3d12 :: proc(handle: bk.Render_Pass_Handle) {
	entry, ok := render_pass_entry(handle)
	if !ok {return}
	entry.active = false
}

create_framebuffer_d3d12 :: 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 {return bk.NULL_FRAMEBUFFER, false}

	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
			}
			if view == bk.NULL_TEXTURE {return bk.NULL_FRAMEBUFFER, false}
			color_entry, color_ok := texture_entry(view)
			if !color_ok || !color_entry.has_rtv {return bk.NULL_FRAMEBUFFER, false}
		}
	}
	if pass_entry.desc.has_depth {
		if desc.depth_view == bk.NULL_TEXTURE {return bk.NULL_FRAMEBUFFER, false}
		depth_entry, depth_ok := texture_entry(desc.depth_view)
		if !depth_ok || !depth_entry.has_dsv {return bk.NULL_FRAMEBUFFER, false}
	}

	entry := &g_d3d.framebuffers[handle]
	entry.color_count = desc.color_count
	if entry.color_count == 0 && pass_entry.desc.has_color {
		entry.color_count = pass_entry.desc.color_count
	}
	if entry.color_count == 0 && pass_entry.desc.has_color {
		entry.color_count = 1
	}
	for i in 0..<entry.color_count {
		entry.color_texs[i] = desc.color_views[i]
		if i == 0 && entry.color_texs[i] == bk.NULL_TEXTURE {
			entry.color_texs[i] = desc.color_view
		}
	}
	entry.color_tex = entry.color_texs[0]
	entry.depth_tex = desc.depth_view
	entry.width = desc.width
	entry.height = desc.height
	entry.active = true
	return handle, true
}

destroy_framebuffer_d3d12 :: proc(handle: bk.Framebuffer_Handle) {
	entry, ok := framebuffer_entry(handle)
	if !ok {return}
	entry^ = {}
}