Harbor

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

import "core:log"
import win32 "core:sys/windows"

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

import bk ".."

// --- Global D3D12 state ---

@(private)
g_d3d: ^D3D12_State

// Frame resource for double buffering
Frame_Resource :: struct {
	command_allocator: ^d3d12.ICommandAllocator,
	fence_value:       u64,
}

// Linear descriptor allocator for shader-visible GPU heaps
Descriptor_Allocator :: struct {
	offset:   u32,
	capacity: u32,
}

D3D12_State :: struct {
	// Core objects
	device:                      ^d3d12.IDevice,
	command_queue:               ^d3d12.ICommandQueue,
	swap_chain:                  ^dxgi.ISwapChain3,
	command_list:                ^d3d12.IGraphicsCommandList,

	// Double buffering
	frame_resources:             [bk.MAX_FRAMES_IN_FLIGHT]Frame_Resource,
	backbuffers:                 [bk.MAX_FRAMES_IN_FLIGHT]^d3d12.IResource,
	fence:                       ^d3d12.IFence,
	fence_event:                 d3d12.HANDLE,
	fence_value:                 u64,
	current_frame:               u32,
	back_buffer_idx:             u32,

	// Depth buffer
	depth_buffer:                ^d3d12.IResource,
	depth_format:                dxgi.FORMAT,

	// Descriptor heaps (non-shader-visible, for staging)
	rtv_heap:                    ^d3d12.IDescriptorHeap,
	dsv_heap:                    ^d3d12.IDescriptorHeap,
	staging_cbv_srv_uav_heap:    ^d3d12.IDescriptorHeap,
	staging_sampler_heap:        ^d3d12.IDescriptorHeap,

	// Descriptor heaps (shader-visible, for GPU binding)
	gpu_cbv_srv_uav_heap:        ^d3d12.IDescriptorHeap,
	gpu_sampler_heap:            ^d3d12.IDescriptorHeap,

	// Per-frame linear allocators for GPU heaps
	gpu_cbv_srv_uav_alloc:       [bk.MAX_FRAMES_IN_FLIGHT]Descriptor_Allocator,
	gpu_sampler_alloc:           [bk.MAX_FRAMES_IN_FLIGHT]Descriptor_Allocator,

	// Descriptor increment sizes
	rtv_inc:                     u32,
	dsv_inc:                     u32,
	cbv_srv_uav_inc:             u32,
	sampler_inc:                 u32,

	// Staging heap free indices (simple linear counter, no free list)
	staging_cbv_srv_uav_next:    u32,
	staging_sampler_next:        u32,

	// Root signatures
	graphics_root_sig:           ^d3d12.IRootSignature,
	compute_root_sig:            ^d3d12.IRootSignature,
	draw_indirect_signature:     ^d3d12.ICommandSignature,
	draw_indexed_indirect_signature: ^d3d12.ICommandSignature,

	// Dimensions
	width:                       u32,
	height:                      u32,

	// Default render pass
	default_render_pass:         bk.Render_Pass_Handle,

	// Frame state
	frame_active:                bool,
	render_pass_active:          bool,
	clear_color:                 [4]f32,
	deferred_releases:           [dynamic]D3D12_Deferred_Release,

	// Current pipeline (for topology at draw time)
	current_pipeline:            bk.Pipeline_Handle,
	pending_depth_bias_constant: i32,
	pending_depth_bias_slope:    f32,

	// Handle pools
	buffers:                     [MAX_BUFFERS]D3D12_Buffer_Entry,
	textures:                    [MAX_TEXTURES]D3D12_Texture_Entry,
	pipelines:                   [MAX_PIPELINES]D3D12_Pipeline_Entry,
	shaders:                     [MAX_SHADERS]D3D12_Shader_Entry,
	descriptors:                 [MAX_DESCRIPTORS]D3D12_Descriptor_Entry,
	render_passes:               [MAX_RENDER_PASSES]D3D12_Render_Pass_Entry,
	framebuffers:                [MAX_FRAMEBUFFERS]D3D12_Framebuffer_Entry,
	samplers:                    [MAX_SAMPLERS]D3D12_Sampler_Entry,
}

// Pool sizes
MAX_BUFFERS :: 1024
MAX_TEXTURES :: 512
MAX_PIPELINES :: 128
MAX_SHADERS :: 256
MAX_DESCRIPTORS :: 512
MAX_RENDER_PASSES :: 32
MAX_FRAMEBUFFERS :: 64
MAX_SAMPLERS :: 64

MAX_DEPTH_BIAS_PSO_VARIANTS :: 16

// Push constant slot (must match D3D11 convention: b13)
PUSH_CONSTANT_SLOT :: 13
PUSH_CONSTANT_MAX_SIZE :: 240 // 60 DWORDs; D3D12 root sig limit is 64 DWORDs total, 3 used by descriptor tables

// Descriptor heap sizes
STAGING_CBV_SRV_UAV_SIZE :: 4096
STAGING_SAMPLER_SIZE :: 256
GPU_CBV_SRV_UAV_SIZE :: 4096
GPU_SAMPLER_SIZE :: 256
GPU_CBV_SRV_UAV_PER_FRAME :: GPU_CBV_SRV_UAV_SIZE / bk.MAX_FRAMES_IN_FLIGHT
GPU_SAMPLER_PER_FRAME :: GPU_SAMPLER_SIZE / bk.MAX_FRAMES_IN_FLIGHT

D3D12_Deferred_Release_Kind :: enum {
	Resource,
	Pipeline_State,
}

D3D12_Deferred_Release :: struct {
	kind:        D3D12_Deferred_Release_Kind,
	fence_value: u64,
	resource:    ^d3d12.IResource,
	pso:         ^d3d12.IPipelineState,
}

// --- Pool entry types ---

D3D12_Buffer_Entry :: struct {
	resource:    ^d3d12.IResource,
	gpu_address: d3d12.GPU_VIRTUAL_ADDRESS,
	mapped_ptr:  rawptr,
	size:        u64,
	usage:       bk.Buffer_Usage_Flags,
	is_dynamic:  bool,
	state:       d3d12.RESOURCE_STATES,
	active:      bool,
}

D3D12_Texture_Entry :: struct {
	resource:  ^d3d12.IResource,
	srv_index: u32, // index in staging CBV/SRV/UAV heap
	rtv_index: u32, // index in RTV heap
	dsv_index: u32, // index in DSV heap
	has_srv:   bool,
	has_rtv:   bool,
	has_dsv:   bool,
	deny_srv:  bool, // resource created with DENY_SHADER_RESOURCE
	width:     u32,
	height:    u32,
	format:    dxgi.FORMAT,
	usage:     bk.Image_Usage_Flags,
	state:     d3d12.RESOURCE_STATES,
	active:    bool,
}

D3D12_Pipeline_Entry :: struct {
	pso:                    ^d3d12.IPipelineState,
	topology:               d3d12.PRIMITIVE_TOPOLOGY,
	root_sig:               ^d3d12.IRootSignature,
	depth_bias_constant:    i32,
	depth_bias_slope:       f32,
	no_draw:                bool,
	push_constant_size:     u32,
	push_constant_stages:   bk.Shader_Stage_Flags,
	vert_bytecode:          []byte,
	frag_bytecode:          []byte,
	vertex_bindings:        [16]bk.Vertex_Binding,
	vertex_binding_count:   u32,
	vertex_attributes:      [16]bk.Vertex_Attribute,
	vertex_attribute_count: u32,
	pipeline_topology:      bk.Topology,
	cull_mode:              bk.Cull_Mode,
	front_face:             bk.Front_Face,
	enable_blending:        bool,
	blend_mode:             bk.Blend_Mode,
	enable_depth_test:      bool,
	depth_format:           bk.Format,
	stencil:                bk.Stencil_State,
	depth_only:             bool,
	color_attachment_count: u32,
	color_formats:          [bk.MAX_COLOR_TARGETS]bk.Format,
	color_write_masks:      [bk.MAX_COLOR_TARGETS]u8,
	depth_bias_variants:    [MAX_DEPTH_BIAS_PSO_VARIANTS]D3D12_Depth_Bias_PSO,
	depth_bias_count:       u32,
	is_compute:             bool,
	active:                 bool,
}

D3D12_Depth_Bias_PSO :: struct {
	constant: i32,
	slope:    f32,
	pso:      ^d3d12.IPipelineState,
	active:   bool,
}

D3D12_Shader_Entry :: struct {
	bytecode: []byte, // Raw DXBC bytecode (kept alive for PSO creation)
	stage:    bk.Shader_Stage,
	active:   bool,
}

D3D12_Descriptor_Binding :: struct {
	binding:  u32,
	type:     bk.Descriptor_Type,
	buffer:   bk.Buffer_Handle,
	texture:  bk.Texture_Handle,
	sampler:  bk.Sampler_Handle,
	buf_size: u64,
}

D3D12_Descriptor_Entry :: struct {
	kind:            Descriptor_Kind,
	layout_bindings: [16]bk.Descriptor_Set_Layout_Binding,
	layout_count:    u32,
	bindings:        [16]D3D12_Descriptor_Binding,
	binding_count:   u32,
	active:          bool,
}

Descriptor_Kind :: enum {
	Set_Layout,
	Pool,
	Set,
}

D3D12_Render_Pass_Entry :: struct {
	desc:   bk.Render_Pass_Desc,
	active: bool,
}

D3D12_Framebuffer_Entry :: struct {
	color_tex:   bk.Texture_Handle,
	color_texs:  [bk.MAX_COLOR_TARGETS]bk.Texture_Handle,
	color_count: u32,
	depth_tex:   bk.Texture_Handle,
	width:       u32,
	height:      u32,
	active:      bool,
}

D3D12_Sampler_Entry :: struct {
	cpu_handle:    d3d12.CPU_DESCRIPTOR_HANDLE,
	staging_index: u32,
	active:        bool,
}

// --- Init ---

init_d3d12_backend :: proc(
	surface: bk.Surface_Desc,
	width, height: u32,
	title: cstring,
) -> (
	backend: bk.Backend,
	ok: bool,
) {
	state := new(D3D12_State)
	if state == nil {
		log.error("gpu/d3d12: failed to allocate D3D12 state")
		return {}, false
	}
	g_d3d = state

	if surface.kind != .Win32 || surface.win32.hwnd == nil {
		log.error("gpu/d3d12: failed to get Win32 window handle")
		free(state)
		g_d3d = nil
		return {}, false
	}
	hwnd := dxgi.HWND(surface.win32.hwnd)

	// Enable debug layer in debug builds
	when ODIN_DEBUG {
		debug: ^d3d12.IDebug
		if d3d12.GetDebugInterface(d3d12.IDebug_UUID, cast(^rawptr)&debug) >= 0 {
			debug->EnableDebugLayer()
			debug->Release()
		}
	}

	// Create D3D12 device
	result := d3d12.CreateDevice(nil, ._12_0, d3d12.IDevice_UUID, cast(^rawptr)&state.device)
	if result < 0 {
		log.errorf("gpu/d3d12: D3D12CreateDevice failed: 0x%08X", u32(result))
		free(state)
		g_d3d = nil
		return {}, false
	}

	// Create command queue
	queue_desc := d3d12.COMMAND_QUEUE_DESC {
		Type = .DIRECT,
	}
	result = state.device->CreateCommandQueue(
		&queue_desc,
		d3d12.ICommandQueue_UUID,
		cast(^rawptr)&state.command_queue,
	)
	if result < 0 {
		log.errorf("gpu/d3d12: CreateCommandQueue failed: 0x%08X", u32(result))
		shutdown_d3d12()
		return {}, false
	}

	// Create DXGI factory
	dxgi_flags: dxgi.CREATE_FACTORY
	when ODIN_DEBUG {
		dxgi_flags = {.DEBUG}
	}
	dxgi_factory: ^dxgi.IFactory4
	result = dxgi.CreateDXGIFactory2(dxgi_flags, dxgi.IFactory4_UUID, cast(^rawptr)&dxgi_factory)
	if result < 0 {
		log.errorf("gpu/d3d12: CreateDXGIFactory2 failed: 0x%08X", u32(result))
		shutdown_d3d12()
		return {}, false
	}
	defer dxgi_factory->Release()

	// Create swap chain
	sc_desc := dxgi.SWAP_CHAIN_DESC1 {
		Width = width,
		Height = height,
		Format = .R8G8B8A8_UNORM,
		SampleDesc = {Count = 1, Quality = 0},
		BufferUsage = {.RENDER_TARGET_OUTPUT},
		BufferCount = 2,
		SwapEffect = .FLIP_DISCARD,
		Scaling = .STRETCH,
	}

	swap_chain1: ^dxgi.ISwapChain1
	result = dxgi_factory->CreateSwapChainForHwnd(
		state.command_queue,
		hwnd,
		&sc_desc,
		nil,
		nil,
		&swap_chain1,
	)
	if result < 0 {
		log.errorf("gpu/d3d12: CreateSwapChainForHwnd failed: 0x%08X", u32(result))
		shutdown_d3d12()
		return {}, false
	}

	// Query ISwapChain3 for GetCurrentBackBufferIndex
	result = swap_chain1->QueryInterface(dxgi.ISwapChain3_UUID, cast(^rawptr)&state.swap_chain)
	swap_chain1->Release()
	if result < 0 {
		log.errorf("gpu/d3d12: QueryInterface ISwapChain3 failed: 0x%08X", u32(result))
		shutdown_d3d12()
		return {}, false
	}

	state.width = width
	state.height = height

	// Cache descriptor increment sizes
	state.rtv_inc = state.device->GetDescriptorHandleIncrementSize(.RTV)
	state.dsv_inc = state.device->GetDescriptorHandleIncrementSize(.DSV)
	state.cbv_srv_uav_inc = state.device->GetDescriptorHandleIncrementSize(.CBV_SRV_UAV)
	state.sampler_inc = state.device->GetDescriptorHandleIncrementSize(.SAMPLER)

	// Create descriptor heaps
	if !create_descriptor_heaps(state) {
		shutdown_d3d12()
		return {}, false
	}

	// Create backbuffer RTVs
	if !create_backbuffer_rtvs(state) {
		shutdown_d3d12()
		return {}, false
	}

	// Create depth buffer
	state.depth_format = .D32_FLOAT
	if !create_depth_buffer(state, width, height) {
		shutdown_d3d12()
		return {}, false
	}

	// Create per-frame command allocators
	for i in 0 ..< bk.MAX_FRAMES_IN_FLIGHT {
		result = state.device->CreateCommandAllocator(
			.DIRECT,
			d3d12.ICommandAllocator_UUID,
			cast(^rawptr)&state.frame_resources[i].command_allocator,
		)
		if result < 0 {
			log.errorf("gpu/d3d12: CreateCommandAllocator failed: 0x%08X", u32(result))
			shutdown_d3d12()
			return {}, false
		}
	}

	// Create command list (initially closed)
	result = state.device->CreateCommandList(
		0,
		.DIRECT,
		state.frame_resources[0].command_allocator,
		nil,
		d3d12.IGraphicsCommandList_UUID,
		cast(^rawptr)&state.command_list,
	)
	if result < 0 {
		log.errorf("gpu/d3d12: CreateCommandList failed: 0x%08X", u32(result))
		shutdown_d3d12()
		return {}, false
	}
	state.command_list->Close()

	// Create fence
	result = state.device->CreateFence(0, {}, d3d12.IFence_UUID, cast(^rawptr)&state.fence)
	if result < 0 {
		log.errorf("gpu/d3d12: CreateFence failed: 0x%08X", u32(result))
		shutdown_d3d12()
		return {}, false
	}
	state.fence_value = 1
	when ODIN_OS == .Windows {
		state.fence_event = win32.CreateEventW(nil, false, false, nil)
	}

	// Create root signatures
	if !create_root_signatures(state) {
		shutdown_d3d12()
		return {}, false
	}

	// Register default render pass
	rp_handle, rp_ok := alloc_render_pass_handle()
	if !rp_ok {
		log.error("gpu/d3d12: failed to allocate default render pass handle")
		shutdown_d3d12()
		return {}, false
	}
	state.render_passes[rp_handle].desc = bk.Render_Pass_Desc {
		has_color    = true,
		has_depth    = true,
		color_format = .R8G8B8A8_UNORM,
		depth_format = .D32_SFLOAT,
	}
	state.render_passes[rp_handle].active = true
	state.default_render_pass = rp_handle

	// Initialize per-frame GPU descriptor allocators
	for i in 0 ..< bk.MAX_FRAMES_IN_FLIGHT {
		state.gpu_cbv_srv_uav_alloc[i] = Descriptor_Allocator {
			offset   = u32(i) * GPU_CBV_SRV_UAV_PER_FRAME,
			capacity = GPU_CBV_SRV_UAV_PER_FRAME,
		}
		state.gpu_sampler_alloc[i] = Descriptor_Allocator {
			offset   = u32(i) * GPU_SAMPLER_PER_FRAME,
			capacity = GPU_SAMPLER_PER_FRAME,
		}
	}

	// Populate backend vtable
	backend = bk.Backend {
		capabilities                  = bk.implemented_base_capabilities(
			bk.MAX_COLOR_TARGETS,
			PUSH_CONSTANT_MAX_SIZE,
		),
		shutdown                      = shutdown_d3d12,
		wait_idle                     = wait_idle_d3d12,
		begin_frame                   = begin_frame_d3d12,
		end_frame                     = end_frame_d3d12,
		on_resize                     = on_resize_d3d12,
		get_extent                    = get_extent_d3d12,
		current_frame_index           = get_current_frame_d3d12,
		begin_render_pass             = begin_render_pass_d3d12,
		begin_default_pass            = begin_default_pass_d3d12,
		end_render_pass               = end_render_pass_d3d12,
		set_viewport                  = set_viewport_d3d12,
		set_scissor                   = set_scissor_d3d12,
		set_depth_bias                = set_depth_bias_d3d12,
		create_graphics_pipeline      = create_graphics_pipeline_d3d12,
		destroy_graphics_pipeline     = destroy_graphics_pipeline_d3d12,
		bind_graphics_pipeline        = bind_graphics_pipeline_d3d12,
		push_constants                = push_constants_d3d12,
		create_buffer                 = create_buffer_d3d12,
		create_buffer_staged          = create_buffer_staged_d3d12,
		destroy_buffer                = destroy_buffer_d3d12,
		map_buffer                    = map_buffer_d3d12,
		unmap_buffer                  = unmap_buffer_d3d12,
		get_buffer_mapped             = get_buffer_mapped_d3d12,
		bind_vertex_buffer            = bind_vertex_buffer_d3d12,
		bind_vertex_buffer_slot       = bind_vertex_buffer_slot_d3d12,
		bind_index_buffer             = bind_index_buffer_d3d12,
		create_texture                = create_texture_d3d12,
		destroy_texture               = destroy_texture_d3d12,
		read_texture_rgba8            = read_texture_rgba8_d3d12,
		create_sampler                = create_sampler_d3d12,
		destroy_sampler               = destroy_sampler_d3d12,
		create_image                  = create_image_d3d12,
		create_image_view             = create_image_view_d3d12,
		destroy_image                 = destroy_image_d3d12,
		create_descriptor_set_layout  = create_descriptor_set_layout_d3d12,
		destroy_descriptor_set_layout = destroy_descriptor_set_layout_d3d12,
		create_descriptor_pool        = create_descriptor_pool_d3d12,
		destroy_descriptor_pool       = destroy_descriptor_pool_d3d12,
		allocate_descriptor_set       = allocate_descriptor_set_d3d12,
		bind_descriptor_set           = bind_descriptor_set_d3d12,
		update_descriptor_image       = update_descriptor_image_d3d12,
		update_descriptor_buffer      = update_descriptor_buffer_d3d12,
		create_render_pass            = create_render_pass_d3d12,
		destroy_render_pass           = destroy_render_pass_d3d12,
		create_framebuffer            = create_framebuffer_d3d12,
		destroy_framebuffer           = destroy_framebuffer_d3d12,
		create_shader_module          = create_shader_module_d3d12,
		destroy_shader                = destroy_shader_d3d12,
		draw                          = draw_d3d12,
		draw_indexed                  = draw_indexed_d3d12,
		draw_indirect                 = draw_indirect_d3d12,
		draw_indexed_indirect         = draw_indexed_indirect_d3d12,
		create_compute_pipeline       = create_compute_pipeline_d3d12,
		destroy_compute_pipeline      = destroy_compute_pipeline_d3d12,
		bind_compute_pipeline         = bind_compute_pipeline_d3d12,
		dispatch_compute              = dispatch_compute_d3d12,
		compute_barrier               = compute_barrier_d3d12,
		get_default_render_pass       = get_default_render_pass_d3d12,
		get_depth_format              = get_depth_format_d3d12,
	}

	bk.backend_initialized = true
	return backend, true
}

get_current_frame_d3d12 :: proc() -> u32 {
	return g_d3d.current_frame
}

// --- Handle allocation helpers ---

@(private)
alloc_buffer_handle :: proc() -> (bk.Buffer_Handle, bool) {
	for i in 1 ..< u64(MAX_BUFFERS) {
		if !g_d3d.buffers[i].active {
			return bk.Buffer_Handle(i), true
		}
	}
	return bk.NULL_BUFFER, false
}

@(private)
alloc_texture_handle :: proc() -> (bk.Texture_Handle, bool) {
	for i in 1 ..< u64(MAX_TEXTURES) {
		if !g_d3d.textures[i].active {
			return bk.Texture_Handle(i), true
		}
	}
	return bk.NULL_TEXTURE, false
}

@(private)
alloc_pipeline_handle :: proc() -> (bk.Pipeline_Handle, bool) {
	for i in 1 ..< u64(MAX_PIPELINES) {
		if !g_d3d.pipelines[i].active {
			return bk.Pipeline_Handle(i), true
		}
	}
	return bk.NULL_PIPELINE, false
}

@(private)
alloc_shader_handle :: proc() -> (bk.Shader_Handle, bool) {
	for i in 1 ..< u64(MAX_SHADERS) {
		if !g_d3d.shaders[i].active {
			return bk.Shader_Handle(i), true
		}
	}
	return bk.NULL_SHADER, false
}

@(private)
alloc_descriptor_handle :: proc() -> (bk.Descriptor_Handle, bool) {
	for i in 1 ..< u64(MAX_DESCRIPTORS) {
		if !g_d3d.descriptors[i].active {
			return bk.Descriptor_Handle(i), true
		}
	}
	return bk.NULL_DESCRIPTOR, false
}

@(private)
alloc_render_pass_handle :: proc() -> (bk.Render_Pass_Handle, bool) {
	for i in 1 ..< u64(MAX_RENDER_PASSES) {
		if !g_d3d.render_passes[i].active {
			return bk.Render_Pass_Handle(i), true
		}
	}
	return bk.NULL_RENDER_PASS, false
}

@(private)
alloc_framebuffer_handle :: proc() -> (bk.Framebuffer_Handle, bool) {
	for i in 1 ..< u64(MAX_FRAMEBUFFERS) {
		if !g_d3d.framebuffers[i].active {
			return bk.Framebuffer_Handle(i), true
		}
	}
	return bk.NULL_FRAMEBUFFER, false
}

@(private)
alloc_sampler_handle :: proc() -> (bk.Sampler_Handle, bool) {
	for i in 1 ..< u64(MAX_SAMPLERS) {
		if !g_d3d.samplers[i].active {
			return bk.Sampler_Handle(i), true
		}
	}
	return bk.NULL_SAMPLER, false
}

// --- Internal helpers ---

@(private)
create_descriptor_heaps :: proc(state: ^D3D12_State) -> bool {
	result: d3d12.HRESULT

	// RTV heap (non-shader-visible)
	rtv_desc := d3d12.DESCRIPTOR_HEAP_DESC {
		Type           = .RTV,
		NumDescriptors = bk.MAX_FRAMES_IN_FLIGHT + MAX_TEXTURES, // backbuffers + render targets
	}
	result = state.device->CreateDescriptorHeap(
		&rtv_desc,
		d3d12.IDescriptorHeap_UUID,
		cast(^rawptr)&state.rtv_heap,
	)
	if result < 0 {
		log.error("gpu/d3d12: failed to create RTV heap")
		return false
	}

	// DSV heap (non-shader-visible)
	dsv_desc := d3d12.DESCRIPTOR_HEAP_DESC {
		Type           = .DSV,
		NumDescriptors = 1 + MAX_TEXTURES, // default depth + depth targets
	}
	result = state.device->CreateDescriptorHeap(
		&dsv_desc,
		d3d12.IDescriptorHeap_UUID,
		cast(^rawptr)&state.dsv_heap,
	)
	if result < 0 {
		log.error("gpu/d3d12: failed to create DSV heap")
		return false
	}

	// Staging CBV/SRV/UAV heap (non-shader-visible)
	staging_csv_desc := d3d12.DESCRIPTOR_HEAP_DESC {
		Type           = .CBV_SRV_UAV,
		NumDescriptors = STAGING_CBV_SRV_UAV_SIZE,
	}
	result = state.device->CreateDescriptorHeap(
		&staging_csv_desc,
		d3d12.IDescriptorHeap_UUID,
		cast(^rawptr)&state.staging_cbv_srv_uav_heap,
	)
	if result < 0 {
		log.error("gpu/d3d12: failed to create staging CBV/SRV/UAV heap")
		return false
	}

	// Staging sampler heap (non-shader-visible)
	staging_sampler_desc := d3d12.DESCRIPTOR_HEAP_DESC {
		Type           = .SAMPLER,
		NumDescriptors = STAGING_SAMPLER_SIZE,
	}
	result = state.device->CreateDescriptorHeap(
		&staging_sampler_desc,
		d3d12.IDescriptorHeap_UUID,
		cast(^rawptr)&state.staging_sampler_heap,
	)
	if result < 0 {
		log.error("gpu/d3d12: failed to create staging sampler heap")
		return false
	}

	// GPU CBV/SRV/UAV heap (shader-visible)
	gpu_csv_desc := d3d12.DESCRIPTOR_HEAP_DESC {
		Type           = .CBV_SRV_UAV,
		NumDescriptors = GPU_CBV_SRV_UAV_SIZE,
		Flags          = {.SHADER_VISIBLE},
	}
	result = state.device->CreateDescriptorHeap(
		&gpu_csv_desc,
		d3d12.IDescriptorHeap_UUID,
		cast(^rawptr)&state.gpu_cbv_srv_uav_heap,
	)
	if result < 0 {
		log.error("gpu/d3d12: failed to create GPU CBV/SRV/UAV heap")
		return false
	}

	// GPU sampler heap (shader-visible)
	gpu_sampler_desc := d3d12.DESCRIPTOR_HEAP_DESC {
		Type           = .SAMPLER,
		NumDescriptors = GPU_SAMPLER_SIZE,
		Flags          = {.SHADER_VISIBLE},
	}
	result = state.device->CreateDescriptorHeap(
		&gpu_sampler_desc,
		d3d12.IDescriptorHeap_UUID,
		cast(^rawptr)&state.gpu_sampler_heap,
	)
	if result < 0 {
		log.error("gpu/d3d12: failed to create GPU sampler heap")
		return false
	}

	return true
}

@(private)
create_backbuffer_rtvs :: proc(state: ^D3D12_State) -> bool {
	rtv_start: d3d12.CPU_DESCRIPTOR_HANDLE
	state.rtv_heap->GetCPUDescriptorHandleForHeapStart(&rtv_start)

	for i in 0 ..< u32(bk.MAX_FRAMES_IN_FLIGHT) {
		result := state.swap_chain->GetBuffer(
			i,
			d3d12.IResource_UUID,
			cast(^rawptr)&state.backbuffers[i],
		)
		if result < 0 {
			log.errorf("gpu/d3d12: GetBuffer(%d) failed: 0x%08X", i, u32(result))
			return false
		}

		rtv_handle := d3d12.CPU_DESCRIPTOR_HANDLE {
			ptr = rtv_start.ptr + uint(i * state.rtv_inc),
		}
		// Create RTV with SRGB format for correct gamma (matching D3D11 behavior)
		rtv_desc := d3d12.RENDER_TARGET_VIEW_DESC {
			Format        = .R8G8B8A8_UNORM_SRGB,
			ViewDimension = .TEXTURE2D,
		}
		state.device->CreateRenderTargetView(state.backbuffers[i], &rtv_desc, rtv_handle)
	}
	return true
}

@(private)
create_depth_buffer :: proc(state: ^D3D12_State, width, height: u32) -> bool {
	heap_props := d3d12.HEAP_PROPERTIES {
		Type = .DEFAULT,
	}
	depth_desc := d3d12.RESOURCE_DESC {
		Dimension = .TEXTURE2D,
		Width = u64(width),
		Height = height,
		DepthOrArraySize = 1,
		MipLevels = 1,
		Format = state.depth_format,
		SampleDesc = {Count = 1, Quality = 0},
		Flags = {.ALLOW_DEPTH_STENCIL},
	}
	clear_value := d3d12.CLEAR_VALUE {
		Format = state.depth_format,
	}
	clear_value.DepthStencil = d3d12.DEPTH_STENCIL_VALUE {
		Depth   = 1.0,
		Stencil = 0,
	}

	result := state.device->CreateCommittedResource(
		&heap_props,
		{},
		&depth_desc,
		{.DEPTH_WRITE},
		&clear_value,
		d3d12.IResource_UUID,
		cast(^rawptr)&state.depth_buffer,
	)
	if result < 0 {
		log.errorf("gpu/d3d12: CreateCommittedResource (depth) failed: 0x%08X", u32(result))
		return false
	}

	// Create DSV
	dsv_start: d3d12.CPU_DESCRIPTOR_HANDLE
	state.dsv_heap->GetCPUDescriptorHandleForHeapStart(&dsv_start)

	dsv_desc := d3d12.DEPTH_STENCIL_VIEW_DESC {
		Format        = state.depth_format,
		ViewDimension = .TEXTURE2D,
	}
	state.device->CreateDepthStencilView(state.depth_buffer, &dsv_desc, dsv_start)

	return true
}

@(private)
create_root_signatures :: proc(state: ^D3D12_State) -> bool {
	// Root parameter [0]: 64 DWORDs of push constants at b13
	// Root parameter [1]: SRV table t0..t15
	// Root parameter [2]: Sampler table s0..s15
	// Root parameter [3]: CBV table b0..b12

	srv_range := d3d12.DESCRIPTOR_RANGE {
		RangeType          = .SRV,
		NumDescriptors     = 16,
		BaseShaderRegister = 0,
	}
	sampler_range := d3d12.DESCRIPTOR_RANGE {
		RangeType          = .SAMPLER,
		NumDescriptors     = 16,
		BaseShaderRegister = 0,
	}
	cbv_range := d3d12.DESCRIPTOR_RANGE {
		RangeType          = .CBV,
		NumDescriptors     = 13,
		BaseShaderRegister = 0,
	}

	root_params: [4]d3d12.ROOT_PARAMETER
	// [0] Push constants
	root_params[0].ParameterType = ._32BIT_CONSTANTS
	root_params[0].Constants = d3d12.ROOT_CONSTANTS {
		ShaderRegister = PUSH_CONSTANT_SLOT,
		RegisterSpace  = 0,
		Num32BitValues = PUSH_CONSTANT_MAX_SIZE / 4,
	}
	root_params[0].ShaderVisibility = .ALL

	// [1] SRV table
	root_params[1].ParameterType = .DESCRIPTOR_TABLE
	root_params[1].DescriptorTable = d3d12.ROOT_DESCRIPTOR_TABLE {
		NumDescriptorRanges = 1,
		pDescriptorRanges   = &srv_range,
	}
	root_params[1].ShaderVisibility = .ALL

	// [2] Sampler table
	root_params[2].ParameterType = .DESCRIPTOR_TABLE
	root_params[2].DescriptorTable = d3d12.ROOT_DESCRIPTOR_TABLE {
		NumDescriptorRanges = 1,
		pDescriptorRanges   = &sampler_range,
	}
	root_params[2].ShaderVisibility = .ALL

	// [3] CBV table
	root_params[3].ParameterType = .DESCRIPTOR_TABLE
	root_params[3].DescriptorTable = d3d12.ROOT_DESCRIPTOR_TABLE {
		NumDescriptorRanges = 1,
		pDescriptorRanges   = &cbv_range,
	}
	root_params[3].ShaderVisibility = .ALL

	root_sig_desc := d3d12.ROOT_SIGNATURE_DESC {
		NumParameters = 4,
		pParameters   = &root_params[0],
		Flags         = {.ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT},
	}

	sig_blob: ^d3d12.IBlob
	err_blob: ^d3d12.IBlob
	result := d3d12.SerializeRootSignature(&root_sig_desc, ._1_0, &sig_blob, &err_blob)
	if result < 0 {
		if err_blob != nil {
			log.errorf(
				"gpu/d3d12: SerializeRootSignature failed: %s",
				cstring(err_blob->GetBufferPointer()),
			)
			err_blob->Release()
		}
		return false
	}
	defer sig_blob->Release()
	if err_blob != nil {err_blob->Release()}

	blob_ptr := sig_blob->GetBufferPointer()
	blob_size := sig_blob->GetBufferSize()

	result = state.device->CreateRootSignature(
		0,
		blob_ptr,
		blob_size,
		d3d12.IRootSignature_UUID,
		cast(^rawptr)&state.graphics_root_sig,
	)
	if result < 0 {
		log.errorf("gpu/d3d12: CreateRootSignature failed: 0x%08X", u32(result))
		when ODIN_DEBUG {dump_debug_messages()}
		return false
	}

	// Use same root sig for compute
	state.compute_root_sig = state.graphics_root_sig
	state.graphics_root_sig->AddRef()

	return true
}

@(private)
release_backbuffers_and_depth :: proc(state: ^D3D12_State) {
	for i in 0 ..< bk.MAX_FRAMES_IN_FLIGHT {
		if state.backbuffers[i] != nil {
			state.backbuffers[i]->Release()
			state.backbuffers[i] = nil
		}
	}
	if state.depth_buffer != nil {
		state.depth_buffer->Release()
		state.depth_buffer = nil
	}
}

// --- Descriptor heap helpers ---

@(private)
get_rtv_cpu_handle :: proc(index: u32) -> d3d12.CPU_DESCRIPTOR_HANDLE {
	start: d3d12.CPU_DESCRIPTOR_HANDLE
	g_d3d.rtv_heap->GetCPUDescriptorHandleForHeapStart(&start)
	return d3d12.CPU_DESCRIPTOR_HANDLE{ptr = start.ptr + uint(index * g_d3d.rtv_inc)}
}

@(private)
get_dsv_cpu_handle :: proc(index: u32) -> d3d12.CPU_DESCRIPTOR_HANDLE {
	start: d3d12.CPU_DESCRIPTOR_HANDLE
	g_d3d.dsv_heap->GetCPUDescriptorHandleForHeapStart(&start)
	return d3d12.CPU_DESCRIPTOR_HANDLE{ptr = start.ptr + uint(index * g_d3d.dsv_inc)}
}

@(private)
get_staging_cbv_srv_uav_cpu_handle :: proc(index: u32) -> d3d12.CPU_DESCRIPTOR_HANDLE {
	start: d3d12.CPU_DESCRIPTOR_HANDLE
	g_d3d.staging_cbv_srv_uav_heap->GetCPUDescriptorHandleForHeapStart(&start)
	return d3d12.CPU_DESCRIPTOR_HANDLE{ptr = start.ptr + uint(index * g_d3d.cbv_srv_uav_inc)}
}

@(private)
get_staging_sampler_cpu_handle :: proc(index: u32) -> d3d12.CPU_DESCRIPTOR_HANDLE {
	start: d3d12.CPU_DESCRIPTOR_HANDLE
	g_d3d.staging_sampler_heap->GetCPUDescriptorHandleForHeapStart(&start)
	return d3d12.CPU_DESCRIPTOR_HANDLE{ptr = start.ptr + uint(index * g_d3d.sampler_inc)}
}

@(private)
alloc_staging_cbv_srv_uav :: proc() -> (u32, bool) {
	if g_d3d.staging_cbv_srv_uav_next >= STAGING_CBV_SRV_UAV_SIZE {
		return 0, false
	}
	idx := g_d3d.staging_cbv_srv_uav_next
	g_d3d.staging_cbv_srv_uav_next += 1
	return idx, true
}

@(private)
alloc_staging_sampler :: proc() -> (u32, bool) {
	if g_d3d.staging_sampler_next >= STAGING_SAMPLER_SIZE {
		return 0, false
	}
	idx := g_d3d.staging_sampler_next
	g_d3d.staging_sampler_next += 1
	return idx, true
}

// Allocate from GPU-visible CBV/SRV/UAV heap for current frame
@(private)
alloc_gpu_cbv_srv_uav :: proc(
	count: u32,
) -> (
	d3d12.CPU_DESCRIPTOR_HANDLE,
	d3d12.GPU_DESCRIPTOR_HANDLE,
	bool,
) {
	alloc := &g_d3d.gpu_cbv_srv_uav_alloc[g_d3d.current_frame]
	base := u32(g_d3d.current_frame) * GPU_CBV_SRV_UAV_PER_FRAME
	local_offset := alloc.offset - base
	if local_offset + count > alloc.capacity {
		return {}, {}, false
	}

	cpu_start: d3d12.CPU_DESCRIPTOR_HANDLE
	gpu_start: d3d12.GPU_DESCRIPTOR_HANDLE
	g_d3d.gpu_cbv_srv_uav_heap->GetCPUDescriptorHandleForHeapStart(&cpu_start)
	g_d3d.gpu_cbv_srv_uav_heap->GetGPUDescriptorHandleForHeapStart(&gpu_start)

	cpu := d3d12.CPU_DESCRIPTOR_HANDLE {
		ptr = cpu_start.ptr + uint(alloc.offset * g_d3d.cbv_srv_uav_inc),
	}
	gpu := d3d12.GPU_DESCRIPTOR_HANDLE {
		ptr = gpu_start.ptr + u64(alloc.offset * g_d3d.cbv_srv_uav_inc),
	}
	alloc.offset += count
	return cpu, gpu, true
}

// Allocate from GPU-visible sampler heap for current frame
@(private)
alloc_gpu_sampler :: proc(
	count: u32,
) -> (
	d3d12.CPU_DESCRIPTOR_HANDLE,
	d3d12.GPU_DESCRIPTOR_HANDLE,
	bool,
) {
	alloc := &g_d3d.gpu_sampler_alloc[g_d3d.current_frame]
	base := u32(g_d3d.current_frame) * GPU_SAMPLER_PER_FRAME
	local_offset := alloc.offset - base
	if local_offset + count > alloc.capacity {
		return {}, {}, false
	}

	cpu_start: d3d12.CPU_DESCRIPTOR_HANDLE
	gpu_start: d3d12.GPU_DESCRIPTOR_HANDLE
	g_d3d.gpu_sampler_heap->GetCPUDescriptorHandleForHeapStart(&cpu_start)
	g_d3d.gpu_sampler_heap->GetGPUDescriptorHandleForHeapStart(&gpu_start)

	cpu := d3d12.CPU_DESCRIPTOR_HANDLE {
		ptr = cpu_start.ptr + uint(alloc.offset * g_d3d.sampler_inc),
	}
	gpu := d3d12.GPU_DESCRIPTOR_HANDLE {
		ptr = gpu_start.ptr + u64(alloc.offset * g_d3d.sampler_inc),
	}
	alloc.offset += count
	return cpu, gpu, true
}

// --- Transition helpers ---

@(private)
transition_resource_on_command_list_d3d12 :: proc(
	list: ^d3d12.IGraphicsCommandList,
	resource: ^d3d12.IResource,
	before, after: d3d12.RESOURCE_STATES,
) {
	if before == after {return}
	if list == nil || resource == nil {return}
	barrier := d3d12.RESOURCE_BARRIER {
		Type = .TRANSITION,
	}
	barrier.Transition = d3d12.RESOURCE_TRANSITION_BARRIER {
		pResource   = resource,
		Subresource = d3d12.RESOURCE_BARRIER_ALL_SUBRESOURCES,
		StateBefore = before,
		StateAfter  = after,
	}
	list->ResourceBarrier(1, &barrier)
}

@(private)
transition_resource :: proc(resource: ^d3d12.IResource, before, after: d3d12.RESOURCE_STATES) {
	if g_d3d == nil {return}
	transition_resource_on_command_list_d3d12(g_d3d.command_list, resource, before, after)
}

@(private)
transition_texture_to_render_target_d3d12 :: proc(entry: ^D3D12_Texture_Entry) {
	if entry == nil {return}
	target_state: d3d12.RESOURCE_STATES = {.RENDER_TARGET}
	if entry.state == target_state {return}
	transition_resource(entry.resource, entry.state, target_state)
	entry.state = target_state
}

@(private)
transition_texture_to_depth_write_d3d12 :: proc(entry: ^D3D12_Texture_Entry) {
	if entry == nil {return}
	target_state: d3d12.RESOURCE_STATES = {.DEPTH_WRITE}
	if entry.state == target_state {return}
	transition_resource(entry.resource, entry.state, target_state)
	entry.state = target_state
}

@(private)
transition_texture_to_shader_resource_d3d12 :: proc(entry: ^D3D12_Texture_Entry) {
	if entry == nil {return}
	target_state := d3d12.RESOURCE_STATE_ALL_SHADER_RESOURCE
	if entry.state == target_state {return}
	transition_resource(entry.resource, entry.state, target_state)
	entry.state = target_state
}

@(private)
transition_buffer_to_indirect_argument_d3d12 :: proc(entry: ^D3D12_Buffer_Entry) {
	if entry == nil {return}
	target_state: d3d12.RESOURCE_STATES = {.INDIRECT_ARGUMENT}
	if entry.state == target_state {return}
	transition_resource(entry.resource, entry.state, target_state)
	entry.state = target_state
}

@(private)
transition_backbuffer_to_render_target_d3d12 :: proc(resource: ^d3d12.IResource) {
	transition_resource(resource, d3d12.RESOURCE_STATE_PRESENT, {.RENDER_TARGET})
}

@(private)
transition_backbuffer_to_present_d3d12 :: proc(resource: ^d3d12.IResource) {
	transition_resource(resource, {.RENDER_TARGET}, d3d12.RESOURCE_STATE_PRESENT)
}

@(private)
transition_copy_dest_to_generic_read_d3d12 :: proc(resource: ^d3d12.IResource) -> d3d12.RESOURCE_STATES {
	target_state := d3d12.RESOURCE_STATE_GENERIC_READ
	transition_resource(resource, {.COPY_DEST}, target_state)
	return target_state
}

@(private)
transition_copy_dest_to_shader_resource_d3d12 :: proc(resource: ^d3d12.IResource) -> d3d12.RESOURCE_STATES {
	target_state := d3d12.RESOURCE_STATE_ALL_SHADER_RESOURCE
	transition_resource(resource, {.COPY_DEST}, target_state)
	return target_state
}

@(private)
transition_copy_dest_to_generic_read_on_list_d3d12 :: proc(
	list: ^d3d12.IGraphicsCommandList,
	resource: ^d3d12.IResource,
) -> d3d12.RESOURCE_STATES {
	target_state := d3d12.RESOURCE_STATE_GENERIC_READ
	transition_resource_on_command_list_d3d12(list, resource, {.COPY_DEST}, target_state)
	return target_state
}

@(private)
transition_copy_dest_to_shader_resource_on_list_d3d12 :: proc(
	list: ^d3d12.IGraphicsCommandList,
	resource: ^d3d12.IResource,
) -> d3d12.RESOURCE_STATES {
	target_state := d3d12.RESOURCE_STATE_ALL_SHADER_RESOURCE
	transition_resource_on_command_list_d3d12(list, resource, {.COPY_DEST}, target_state)
	return target_state
}

// --- GPU idle helper ---

@(private)
wait_gpu_idle :: proc() {
	if g_d3d == nil || g_d3d.command_queue == nil || g_d3d.fence == nil {return}

	val := g_d3d.fence_value
	g_d3d.command_queue->Signal(g_d3d.fence, val)
	g_d3d.fence_value += 1

	if g_d3d.fence->GetCompletedValue() < val {
		g_d3d.fence->SetEventOnCompletion(val, g_d3d.fence_event)
		when ODIN_OS == .Windows {
			win32.WaitForSingleObject(g_d3d.fence_event, win32.INFINITE)
		}
	}
}

// --- Debug helper ---

@(private)
dump_debug_messages :: proc() {
	if g_d3d == nil || g_d3d.device == nil {return}
	info_queue: ^d3d12.IInfoQueue
	iq_result := g_d3d.device->QueryInterface(d3d12.IInfoQueue_UUID, cast(^rawptr)&info_queue)
	if iq_result < 0 || info_queue == nil {return}
	defer info_queue->Release()

	num_msgs := info_queue->GetNumStoredMessages()
	if num_msgs == 0 {return}
	log.errorf("gpu/d3d12: %d debug messages queued:", num_msgs)
	for i in 0 ..< num_msgs {
		msg_size: d3d12.SIZE_T
		info_queue->GetMessageA(i, nil, &msg_size)
		if msg_size > 0 {
			buf := make([]byte, msg_size)
			defer delete(buf)
			msg := cast(^d3d12.MESSAGE)&buf[0]
			info_queue->GetMessageA(i, msg, &msg_size)
			if msg.pDescription != nil {
				log.errorf("  [%d] %s", i, msg.pDescription)
			}
		}
	}
	info_queue->ClearStoredMessages()
}