Harbor

branch main
showing the latest snapshot on main
d3d11_ops.odin 40.5 KB · Plain text
backend/d3d11/d3d11_ops.odin 0644 Raw
package d3d11_backend

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

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

import bk ".."

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

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

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

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

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

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

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

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

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

create_buffer_d3d11 :: 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

	bind_flags: d3d11.BIND_FLAGS
	if .Vertex in desc.usage {bind_flags += {.VERTEX_BUFFER}}
	if .Index in desc.usage {bind_flags += {.INDEX_BUFFER}}
	if .Uniform in desc.usage {bind_flags += {.CONSTANT_BUFFER}}
	if .Storage in desc.usage {bind_flags += {.UNORDERED_ACCESS, .SHADER_RESOURCE}}

	usage: d3d11.USAGE = .DEFAULT
	cpu_access: d3d11.CPU_ACCESS_FLAGS
	if is_dynamic {
		usage = .DYNAMIC
		cpu_access = {.WRITE}
	}

	buf_desc := d3d11.BUFFER_DESC {
		ByteWidth      = u32(desc.size),
		Usage          = usage,
		BindFlags      = bind_flags,
		CPUAccessFlags = cpu_access,
	}

	// Storage buffers need structured buffer flags
	if .Storage in desc.usage {
		buf_desc.MiscFlags = {.BUFFER_STRUCTURED}
		buf_desc.StructureByteStride = 4 // default stride, updated on bind
	}
	if .Indirect_Argument in desc.usage {
		buf_desc.MiscFlags += {.DRAWINDIRECT_ARGS}
	}

	buf: ^d3d11.IBuffer
	result := g_d3d.device->CreateBuffer(&buf_desc, nil, &buf)
	if result < 0 {
		log.errorf("gpu/d3d11: CreateBuffer failed: 0x%08X", u32(result))
		return bk.NULL_BUFFER, false
	}

	entry := &g_d3d.buffers[handle]
	entry.buffer = buf
	entry.size = desc.size
	entry.usage = desc.usage
	entry.is_dynamic = is_dynamic
	entry.active = true

	// Create UAV and SRV for storage buffers
	if .Storage in desc.usage {
		num_elements := u32(desc.size) / buf_desc.StructureByteStride

		uav_desc: d3d11.UNORDERED_ACCESS_VIEW_DESC
		uav_desc.Format = .UNKNOWN
		uav_desc.ViewDimension = .BUFFER
		uav_desc.Buffer = {
			NumElements = num_elements,
		}
		g_d3d.device->CreateUnorderedAccessView(buf, &uav_desc, &entry.uav)

		srv_desc: d3d11.SHADER_RESOURCE_VIEW_DESC
		srv_desc.Format = .UNKNOWN
		srv_desc.ViewDimension = .BUFFER
		srv_desc.Buffer = {
			NumElements = num_elements,
		}
		g_d3d.device->CreateShaderResourceView(buf, &srv_desc, &entry.srv)
	}

	// Map persistent for dynamic buffers
	if is_dynamic {
		mapped: d3d11.MAPPED_SUBRESOURCE
		result = g_d3d.ctx->Map(buf, 0, .WRITE_DISCARD, {}, &mapped)
		if result >= 0 {
			entry.mapped_ptr = mapped.pData
			g_d3d.ctx->Unmap(buf, 0)
		}
	}

	return handle, true
}

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

	bind_flags: d3d11.BIND_FLAGS
	if .Vertex in usage {bind_flags += {.VERTEX_BUFFER}}
	if .Index in usage {bind_flags += {.INDEX_BUFFER}}
	if .Uniform in usage {bind_flags += {.CONSTANT_BUFFER}}
	if .Storage in usage {bind_flags += {.UNORDERED_ACCESS, .SHADER_RESOURCE}}

	buf_desc := d3d11.BUFFER_DESC {
		ByteWidth = u32(size),
		Usage     = .DEFAULT,
		BindFlags = bind_flags,
	}

	// Storage buffers need structured buffer flags
	if .Storage in usage {
		buf_desc.MiscFlags = {.BUFFER_STRUCTURED}
		buf_desc.StructureByteStride = 4
	}
	if .Indirect_Argument in usage {
		buf_desc.MiscFlags += {.DRAWINDIRECT_ARGS}
	}

	init_data := d3d11.SUBRESOURCE_DATA {
		pSysMem = data,
	}

	buf: ^d3d11.IBuffer
	result := g_d3d.device->CreateBuffer(&buf_desc, &init_data, &buf)
	if result < 0 {
		log.errorf("gpu/d3d11: CreateBuffer (staged) failed: 0x%08X", u32(result))
		return bk.NULL_BUFFER, false
	}

	entry := &g_d3d.buffers[handle]
	entry.buffer = buf
	entry.size = u64(size)
	entry.usage = usage
	entry.is_dynamic = false
	entry.active = true

	// Create UAV and SRV for storage buffers
	if .Storage in usage {
		stride := buf_desc.StructureByteStride if buf_desc.StructureByteStride > 0 else 4
		num_elements := u32(size) / stride

		uav_desc: d3d11.UNORDERED_ACCESS_VIEW_DESC
		uav_desc.Format = .UNKNOWN
		uav_desc.ViewDimension = .BUFFER
		uav_desc.Buffer = {
			NumElements = num_elements,
		}
		g_d3d.device->CreateUnorderedAccessView(buf, &uav_desc, &entry.uav)

		srv_desc: d3d11.SHADER_RESOURCE_VIEW_DESC
		srv_desc.Format = .UNKNOWN
		srv_desc.ViewDimension = .BUFFER
		srv_desc.Buffer = {
			NumElements = num_elements,
		}
		g_d3d.device->CreateShaderResourceView(buf, &srv_desc, &entry.srv)
	}

	return handle, true
}

destroy_buffer_d3d11 :: proc(handle: bk.Buffer_Handle) {
	entry, ok := buffer_entry(handle)
	if !ok {return}
	if entry.uav != nil {entry.uav->Release()}
	if entry.srv != nil {entry.srv->Release()}
	if entry.buffer != nil {entry.buffer->Release()}
	entry^ = {}
}

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

	mapped: d3d11.MAPPED_SUBRESOURCE
	result := g_d3d.ctx->Map(entry.buffer, 0, .WRITE_DISCARD, {}, &mapped)
	if result < 0 {return nil}

	entry.mapped_ptr = mapped.pData
	return mapped.pData
}

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

	g_d3d.ctx->Unmap(entry.buffer, 0)
	// Keep mapped_ptr valid since D3D11 dynamic buffers can be re-mapped
}

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

	// For dynamic buffers, re-map to get a valid pointer
	if entry.is_dynamic && entry.buffer != nil {
		mapped: d3d11.MAPPED_SUBRESOURCE
		result := g_d3d.ctx->Map(entry.buffer, 0, .WRITE_DISCARD, {}, &mapped)
		if result >= 0 {
			entry.mapped_ptr = mapped.pData
			// Leave mapped -- caller will write, then we unmap on next frame
			return mapped.pData
		}
	}
	return entry.mapped_ptr
}

bind_vertex_buffer_d3d11 :: proc(ctx: bk.Frame_Context, handle: bk.Buffer_Handle) {
	bind_vertex_buffer_slot_d3d11(ctx, 0, handle, 0, g_d3d.current_vertex_stride)
}

bind_vertex_buffer_slot_d3d11 :: proc(
	ctx: bk.Frame_Context,
	slot: u32,
	handle: bk.Buffer_Handle,
	offset: u64,
	stride: u32,
) {
	entry, ok := buffer_entry(handle)
	if !ok {return}

	// D3D11 requires Unmap before GPU can read the buffer
	if entry.mapped_ptr != nil {
		g_d3d.ctx->Unmap(entry.buffer, 0)
		entry.mapped_ptr = nil
	}

	bind_stride := stride
	if bind_stride == 0 {
		bind_stride = g_d3d.current_vertex_stride
	}
	if offset > u64(max(u32)) {
		log.error("gpu/d3d11: vertex buffer bind offset exceeds D3D11 u32 range")
		return
	}
	bind_offset := u32(offset)

	g_d3d.ctx->IASetVertexBuffers(slot, 1, &entry.buffer, &bind_stride, &bind_offset)
}

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

	// D3D11 requires Unmap before GPU can read the buffer
	if entry.mapped_ptr != nil {
		g_d3d.ctx->Unmap(entry.buffer, 0)
		entry.mapped_ptr = nil
	}

	g_d3d.ctx->IASetIndexBuffer(entry.buffer, .R32_UINT, 0)
}

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

create_texture_d3d11 :: 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)
	pixel_size := format_pixel_size(desc.format)

	tex_desc := d3d11.TEXTURE2D_DESC {
		Width = desc.width,
		Height = desc.height,
		MipLevels = 1,
		ArraySize = 1,
		Format = dxgi_fmt,
		SampleDesc = {Count = 1, Quality = 0},
		Usage = .DEFAULT,
		BindFlags = {.SHADER_RESOURCE},
	}

	init_data := d3d11.SUBRESOURCE_DATA {
		pSysMem     = pixels,
		SysMemPitch = desc.width * pixel_size,
	}

	tex: ^d3d11.ITexture2D
	result := g_d3d.device->CreateTexture2D(&tex_desc, &init_data if pixels != nil else nil, &tex)
	if result < 0 {
		log.errorf("gpu/d3d11: CreateTexture2D failed: 0x%08X", u32(result))
		return bk.NULL_TEXTURE, false
	}

	// Create shader resource view
	srv_desc := d3d11.SHADER_RESOURCE_VIEW_DESC {
		Format        = dxgi_fmt,
		ViewDimension = .TEXTURE2D,
	}
	srv_desc.Texture2D = {
		MostDetailedMip = 0,
		MipLevels       = 1,
	}

	srv: ^d3d11.IShaderResourceView
	result = g_d3d.device->CreateShaderResourceView(tex, &srv_desc, &srv)
	if result < 0 {
		log.errorf("gpu/d3d11: CreateShaderResourceView failed: 0x%08X", u32(result))
		tex->Release()
		return bk.NULL_TEXTURE, false
	}

	entry := &g_d3d.textures[handle]
	entry.texture = tex
	entry.srv = srv
	entry.width = desc.width
	entry.height = desc.height
	entry.format = dxgi_fmt
	entry.active = true
	return handle, true
}

destroy_texture_d3d11 :: proc(handle: bk.Texture_Handle) {
	entry, ok := texture_entry(handle)
	if !ok {return}
	if entry.srv != nil {entry.srv->Release()}
	if entry.rtv != nil {entry.rtv->Release()}
	if entry.dsv != nil {entry.dsv->Release()}
	if entry.texture != nil {entry.texture->Release()}
	entry^ = {}
}

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

create_image_d3d11 :: 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

	// Depth textures need typeless format if also sampled
	texture_fmt := dxgi_fmt
	if is_depth && .Sampled in desc.usage {
		texture_fmt = depth_format_to_typeless(dxgi_fmt)
	}

	bind_flags: d3d11.BIND_FLAGS
	if .Sampled in desc.usage {bind_flags += {.SHADER_RESOURCE}}
	if .Color_Attachment in desc.usage {bind_flags += {.RENDER_TARGET}}
	if .Depth_Stencil_Attachment in desc.usage {bind_flags += {.DEPTH_STENCIL}}

	tex_desc := d3d11.TEXTURE2D_DESC {
		Width = desc.width,
		Height = desc.height,
		MipLevels = 1,
		ArraySize = 1,
		Format = texture_fmt,
		SampleDesc = {Count = 1, Quality = 0},
		Usage = .DEFAULT,
		BindFlags = bind_flags,
	}

	tex: ^d3d11.ITexture2D
	result := g_d3d.device->CreateTexture2D(&tex_desc, nil, &tex)
	if result < 0 {
		log.errorf("gpu/d3d11: CreateTexture2D (image) failed: 0x%08X", u32(result))
		return bk.NULL_TEXTURE, false
	}

	entry := &g_d3d.textures[handle]
	entry.texture = tex
	entry.usage = desc.usage
	entry.width = desc.width
	entry.height = desc.height
	entry.format = dxgi_fmt
	entry.active = true
	return handle, true
}

create_image_view_d3d11 :: 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 {
		// Create DSV
		if entry.dsv != nil {
			entry.dsv->Release()
			entry.dsv = nil
		}

		dsv_desc := d3d11.DEPTH_STENCIL_VIEW_DESC {
			Format        = dxgi_fmt,
			ViewDimension = .TEXTURE2D,
		}

		result := g_d3d.device->CreateDepthStencilView(entry.texture, &dsv_desc, &entry.dsv)
		if result < 0 {return false}

		// Also create SRV for depth sampling (shadow maps)
		if entry.srv != nil {
			entry.srv->Release()
			entry.srv = nil
		}
		srv_fmt := depth_format_to_srv(dxgi_fmt)
		srv_desc := d3d11.SHADER_RESOURCE_VIEW_DESC {
			Format        = srv_fmt,
			ViewDimension = .TEXTURE2D,
		}
		srv_desc.Texture2D = {
			MostDetailedMip = 0,
			MipLevels       = 1,
		}
		g_d3d.device->CreateShaderResourceView(entry.texture, &srv_desc, &entry.srv)
	} else {
		if .Color_Attachment in entry.usage {
			if entry.rtv != nil {
				entry.rtv->Release()
				entry.rtv = nil
			}
			rtv_desc := d3d11.RENDER_TARGET_VIEW_DESC {
				Format        = dxgi_fmt,
				ViewDimension = .TEXTURE2D,
			}
			result := g_d3d.device->CreateRenderTargetView(entry.texture, &rtv_desc, &entry.rtv)
			if result < 0 {return false}
		}

		if .Sampled in entry.usage {
			// Create SRV for color
			if entry.srv != nil {
				entry.srv->Release()
				entry.srv = nil
			}

			srv_desc := d3d11.SHADER_RESOURCE_VIEW_DESC {
				Format        = dxgi_fmt,
				ViewDimension = .TEXTURE2D,
			}
			srv_desc.Texture2D = {
				MostDetailedMip = 0,
				MipLevels       = 1,
			}

			result := g_d3d.device->CreateShaderResourceView(entry.texture, &srv_desc, &entry.srv)
			if result < 0 {return false}
		}
	}

	return true
}

read_texture_rgba8_d3d11 :: proc(desc: bk.Readback_Texture_Desc, out: []u8) -> bool {
	entry, ok := texture_entry(desc.texture)
	if !ok do return false
	if desc.width != entry.width || desc.height != entry.height {
		log.error("gpu/d3d11: 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/d3d11: 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/d3d11: read_texture_rgba8 requires an 8-bit RGBA/BGRA color texture")
		return false
	}

	staging_desc := d3d11.TEXTURE2D_DESC {
		Width = desc.width,
		Height = desc.height,
		MipLevels = 1,
		ArraySize = 1,
		Format = entry.format,
		SampleDesc = {Count = 1, Quality = 0},
		Usage = .STAGING,
		CPUAccessFlags = {.READ},
	}
	staging: ^d3d11.ITexture2D
	result := g_d3d.device->CreateTexture2D(&staging_desc, nil, &staging)
	if result < 0 {
		log.errorf("gpu/d3d11: read_texture_rgba8 staging texture failed: 0x%08X", u32(result))
		return false
	}
	defer staging->Release()

	g_d3d.ctx->CopyResource(cast(^d3d11.IResource)staging, cast(^d3d11.IResource)entry.texture)

	mapped: d3d11.MAPPED_SUBRESOURCE
	result = g_d3d.ctx->Map(cast(^d3d11.IResource)staging, 0, .READ, {}, &mapped)
	if result < 0 {
		log.errorf("gpu/d3d11: read_texture_rgba8 map failed: 0x%08X", u32(result))
		return false
	}
	defer g_d3d.ctx->Unmap(cast(^d3d11.IResource)staging, 0)

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

destroy_image_d3d11 :: proc(handle: bk.Texture_Handle) {
	destroy_texture_d3d11(handle)
}

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

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

	filter := to_d3d11_filter(desc.mag_filter, desc.min_filter, desc.mipmap_mode)
	if desc.enable_compare {
		// Use comparison filter for shadow sampling
		filter = .COMPARISON_MIN_MAG_LINEAR_MIP_POINT
	}

	sampler_desc := d3d11.SAMPLER_DESC {
		Filter         = filter,
		AddressU       = to_d3d11_address_mode(desc.address_mode_u),
		AddressV       = to_d3d11_address_mode(desc.address_mode_v),
		AddressW       = to_d3d11_address_mode(desc.address_mode_u),
		MipLODBias     = 0,
		MaxAnisotropy  = 16 if desc.enable_aniso else 1,
		ComparisonFunc = to_d3d11_compare_func(desc.compare_op) if desc.enable_compare else .ALWAYS,
		MinLOD         = 0,
		MaxLOD         = d3d11.FLOAT32_MAX,
	}
	// Border color: opaque black
	sampler_desc.BorderColor = {0, 0, 0, 1}

	ss: ^d3d11.ISamplerState
	result := g_d3d.device->CreateSamplerState(&sampler_desc, &ss)
	if result < 0 {
		log.errorf("gpu/d3d11: CreateSamplerState failed: 0x%08X", u32(result))
		return bk.NULL_SAMPLER, false
	}

	entry := &g_d3d.samplers[handle]
	entry.state = ss
	entry.active = true
	return handle, true
}

destroy_sampler_d3d11 :: proc(handle: bk.Sampler_Handle) {
	entry, ok := sampler_entry(handle)
	if !ok {return}
	if entry.state != nil {entry.state->Release()}
	entry.active = false
}

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

create_shader_module_d3d11 :: 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/d3d11: 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"
	}

	entry_point: cstring =
		"main" if desc.entry == "" else strings.clone_to_cstring(desc.entry, context.temp_allocator)

	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)

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

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

	// Step 3: Create shader object
	entry := &g_d3d.shaders[handle]
	entry.stage = desc.stage
	entry.active = true

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

	switch desc.stage {
	case .Vertex:
		vs: ^d3d11.IVertexShader
		result = g_d3d.device->CreateVertexShader(blob_ptr, blob_size, nil, &vs)
		if result < 0 {
			log.errorf("gpu/d3d11: CreateVertexShader failed: 0x%08X", u32(result))
			shader_blob->Release()
			entry.active = false
			return bk.NULL_SHADER, false
		}
		entry.vs = vs
		entry.vs_blob = cast(^d3d11.IBlob)shader_blob // Keep alive for CreateInputLayout
	case .Fragment:
		ps: ^d3d11.IPixelShader
		result = g_d3d.device->CreatePixelShader(blob_ptr, blob_size, nil, &ps)
		if result < 0 {
			log.errorf("gpu/d3d11: CreatePixelShader failed: 0x%08X", u32(result))
			shader_blob->Release()
			entry.active = false
			return bk.NULL_SHADER, false
		}
		entry.ps = ps
		shader_blob->Release()
	case .Compute:
		cs: ^d3d11.IComputeShader
		result = g_d3d.device->CreateComputeShader(blob_ptr, blob_size, nil, &cs)
		if result < 0 {
			log.errorf("gpu/d3d11: CreateComputeShader failed: 0x%08X", u32(result))
			shader_blob->Release()
			entry.active = false
			return bk.NULL_SHADER, false
		}
		entry.cs = cs
		shader_blob->Release()
	}

	return handle, true
}

destroy_shader_d3d11 :: proc(handle: bk.Shader_Handle) {
	entry, ok := shader_entry(handle)
	if !ok {return}
	if entry.vs != nil {entry.vs->Release()}
	if entry.ps != nil {entry.ps->Release()}
	if entry.cs != nil {entry.cs->Release()}
	if entry.vs_blob != nil {entry.vs_blob->Release()}
	entry^ = {}
}

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

input_rate_for_binding :: proc(desc: bk.Pipeline_Desc, binding: u32) -> bk.Vertex_Input_Rate {
	for vb in desc.vertex_bindings {
		if vb.binding == binding {
			return vb.input_rate
		}
	}
	return .Vertex
}

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

	entry := &g_d3d.pipelines[handle]

	// Get shader objects
	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/d3d11: graphics pipeline shader handle is invalid")
		return bk.NULL_PIPELINE, false
	}
	entry.vs = vert_entry.vs
	entry.ps = frag_entry.ps

	// AddRef since pipeline keeps references
	if entry.vs != nil {entry.vs->AddRef()}
	if entry.ps != nil {entry.ps->AddRef()}

	// Create input layout from VS blob + vertex attributes
	if vert_entry.vs_blob != nil && len(desc.vertex_attributes) > 0 {
		input_descs: [16]d3d11.INPUT_ELEMENT_DESC
		for i in 0 ..< len(desc.vertex_attributes) {
			attr := &desc.vertex_attributes[i]
			// Luma HLSL uses TEXCOORD for all vertex inputs with incrementing index
			input_descs[i] = d3d11.INPUT_ELEMENT_DESC {
				SemanticName         = "TEXCOORD",
				SemanticIndex        = u32(attr.location),
				Format               = to_dxgi_vertex_format(attr.format),
				InputSlot            = attr.binding,
				AlignedByteOffset    = attr.offset,
				InputSlotClass       = .INSTANCE_DATA if input_rate_for_binding(desc, attr.binding) == .Instance else .VERTEX_DATA,
				InstanceDataStepRate = 1 if input_rate_for_binding(desc, attr.binding) == .Instance else 0,
			}
		}

		result := g_d3d.device->CreateInputLayout(
			&input_descs[0],
			u32(len(desc.vertex_attributes)),
			vert_entry.vs_blob->GetBufferPointer(),
			vert_entry.vs_blob->GetBufferSize(),
			&entry.input_layout,
		)
		if result < 0 {
			log.errorf("gpu/d3d11: CreateInputLayout failed: 0x%08X", u32(result))
			destroy_graphics_pipeline_d3d11(handle)
			return bk.NULL_PIPELINE, false
		}
	}

	// Create rasterizer state
	raster_desc := d3d11.RASTERIZER_DESC {
		FillMode              = .SOLID,
		CullMode              = to_d3d11_cull_mode(desc.cull_mode),
		FrontCounterClockwise = d3d11.BOOL(desc.front_face == .Counter_Clockwise),
		DepthBias             = 0,
		DepthBiasClamp        = 0,
		SlopeScaledDepthBias  = 0,
		DepthClipEnable       = d3d11.BOOL(true),
		ScissorEnable         = d3d11.BOOL(true),
	}
	entry.rasterizer_desc = raster_desc

	result := g_d3d.device->CreateRasterizerState(&raster_desc, &entry.rasterizer_state)
	if result < 0 {
		log.errorf("gpu/d3d11: CreateRasterizerState failed: 0x%08X", u32(result))
		destroy_graphics_pipeline_d3d11(handle)
		return bk.NULL_PIPELINE, false
	}

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

	result = g_d3d.device->CreateBlendState(&blend_desc, &entry.blend_state)
	if result < 0 {
		log.errorf("gpu/d3d11: CreateBlendState failed: 0x%08X", u32(result))
		destroy_graphics_pipeline_d3d11(handle)
		return bk.NULL_PIPELINE, false
	}

	// Create depth stencil state
	ds_desc := d3d11.DEPTH_STENCIL_DESC {
		DepthEnable    = d3d11.BOOL(desc.enable_depth_test),
		DepthWriteMask = .ALL,
		DepthFunc      = .LESS,
		StencilEnable    = d3d11.BOOL(desc.stencil.enable),
		StencilReadMask  = desc.stencil.read_mask,
		StencilWriteMask = desc.stencil.write_mask,
		FrontFace        = to_d3d11_stencil_face(desc.stencil.front),
		BackFace         = to_d3d11_stencil_face(desc.stencil.back),
	}

	// For depth-only (shadow) passes, write depth but don't test color
	if desc.depth_only {
		ds_desc.DepthEnable = true
	}

	result = g_d3d.device->CreateDepthStencilState(&ds_desc, &entry.depth_stencil_state)
	if result < 0 {
		log.errorf("gpu/d3d11: CreateDepthStencilState failed: 0x%08X", u32(result))
		destroy_graphics_pipeline_d3d11(handle)
		return bk.NULL_PIPELINE, false
	}

	entry.topology = to_d3d11_topology(desc.topology)
	entry.vertex_stride = desc.vertex_bindings[0].stride if len(desc.vertex_bindings) > 0 else 48
	entry.push_constant_size = desc.push_constant_size
	entry.push_constant_stages = desc.push_constant_stages
	entry.stencil_ref = u32(desc.stencil.reference)
	entry.no_draw = desc.cull_mode == .Front_And_Back
	entry.is_compute = false
	entry.active = true
	return handle, true
}

destroy_graphics_pipeline_d3d11 :: proc(handle: bk.Pipeline_Handle) {
	entry, ok := pipeline_entry(handle)
	if !ok {return}
	if !entry.is_compute {
		if entry.vs != nil {entry.vs->Release()}
		if entry.ps != nil {entry.ps->Release()}
		if entry.input_layout != nil {entry.input_layout->Release()}
		if entry.rasterizer_state != nil {entry.rasterizer_state->Release()}
		if entry.blend_state != nil {entry.blend_state->Release()}
		if entry.depth_stencil_state != nil {entry.depth_stencil_state->Release()}
	}
	entry^ = {}
}

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

	g_d3d.ctx->VSSetShader(entry.vs, nil, 0)
	g_d3d.ctx->PSSetShader(entry.ps, nil, 0)
	g_d3d.ctx->IASetInputLayout(entry.input_layout)
	g_d3d.ctx->IASetPrimitiveTopology(entry.topology)
	g_d3d.ctx->RSSetState(entry.rasterizer_state)
	g_d3d.ctx->OMSetBlendState(entry.blend_state, nil, 0xFFFFFFFF)
	g_d3d.ctx->OMSetDepthStencilState(entry.depth_stencil_state, entry.stencil_ref)

	// Cache pipeline state for subsequent bind calls
	g_d3d.current_pipeline = handle
	g_d3d.current_vertex_stride = entry.vertex_stride
	g_d3d.current_rasterizer_desc = entry.rasterizer_desc
	if g_d3d.current_rasterizer_state != nil {
		g_d3d.current_rasterizer_state->Release()
	}
	g_d3d.current_rasterizer_state = entry.rasterizer_state
	entry.rasterizer_state->AddRef()
}

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

	// Update push constant buffer via Map/Unmap
	mapped: d3d11.MAPPED_SUBRESOURCE
	result := g_d3d.ctx->Map(g_d3d.push_constant_buf, 0, .WRITE_DISCARD, {}, &mapped)
	if result < 0 {return}

	dst := rawptr(uintptr(mapped.pData) + uintptr(offset))
	mem.copy(dst, data, int(size))

	g_d3d.ctx->Unmap(g_d3d.push_constant_buf, 0)

	// Bind to the reserved slot for all requested stages
	if .Vertex in stages {
		g_d3d.ctx->VSSetConstantBuffers(PUSH_CONSTANT_SLOT, 1, &g_d3d.push_constant_buf)
	}
	if .Fragment in stages {
		g_d3d.ctx->PSSetConstantBuffers(PUSH_CONSTANT_SLOT, 1, &g_d3d.push_constant_buf)
	}
	if .Compute in stages {
		g_d3d.ctx->CSSetConstantBuffers(PUSH_CONSTANT_SLOT, 1, &g_d3d.push_constant_buf)
	}
}

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

create_compute_pipeline_d3d11 :: 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 || shader_entry.cs == nil {
		return bk.NULL_PIPELINE, false
	}

	entry := &g_d3d.pipelines[handle]
	entry.cs = shader_entry.cs
	entry.cs->AddRef()
	entry.push_constant_size = push_constant_size
	entry.is_compute = true
	entry.active = true
	return handle, true
}

destroy_compute_pipeline_d3d11 :: proc(handle: bk.Pipeline_Handle) {
	entry, ok := pipeline_entry(handle)
	if !ok {return}
	if entry.is_compute && entry.cs != nil {
		entry.cs->Release()
	}
	entry^ = {}
}

bind_compute_pipeline_d3d11 :: proc(ctx: bk.Frame_Context, handle: bk.Pipeline_Handle) {
	if g_d3d == nil {return}
	entry, ok := pipeline_entry(handle)
	if !ok {return}
	g_d3d.ctx->CSSetShader(entry.cs, nil, 0)
}

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

draw_d3d11 :: 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
	}
	if instance_count <= 1 && first_instance == 0 {
		g_d3d.ctx->Draw(vertex_count, first_vertex)
	} else {
		g_d3d.ctx->DrawInstanced(vertex_count, instance_count, first_vertex, first_instance)
	}
}

draw_indexed_d3d11 :: 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
	}
	if instance_count <= 1 && first_instance == 0 {
		g_d3d.ctx->DrawIndexed(index_count, first_index, vertex_offset)
	} else {
		g_d3d.ctx->DrawIndexedInstanced(
			index_count,
			instance_count,
			first_index,
			vertex_offset,
			first_instance,
		)
	}
}

draw_indirect_d3d11 :: 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/d3d11: draw_indirect currently supports one command")
		return
	}
	if argument_offset > u64(max(u32)) || (argument_offset & 3) != 0 {
		log.error("gpu/d3d11: draw_indirect received invalid argument offset")
		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/d3d11: draw_indirect received invalid argument buffer")
		return
	}
	g_d3d.ctx->DrawInstancedIndirect(buffer.buffer, u32(argument_offset))
}

draw_indexed_indirect_d3d11 :: 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/d3d11: draw_indexed_indirect currently supports one command")
		return
	}
	if argument_offset > u64(max(u32)) || (argument_offset & 3) != 0 {
		log.error("gpu/d3d11: draw_indexed_indirect received invalid argument offset")
		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/d3d11: draw_indexed_indirect received invalid argument buffer")
		return
	}
	g_d3d.ctx->DrawIndexedInstancedIndirect(buffer.buffer, u32(argument_offset))
}

@(private)
to_d3d11_blend_factor :: proc(factor: bk.Blend_Factor) -> d3d11.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_d3d11 :: proc(ctx: bk.Frame_Context, groups_x, groups_y, groups_z: u32) {
	if g_d3d == nil {return}
	g_d3d.ctx->Dispatch(groups_x, groups_y, groups_z)
}

compute_barrier_d3d11 :: proc(ctx: bk.Frame_Context) {
	// D3D11 immediate context handles barriers implicitly between dispatches
	// No explicit barrier needed
}

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

create_descriptor_set_layout_d3d11 :: 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_d3d11 :: proc(handle: bk.Descriptor_Handle) {
	entry, ok := descriptor_entry_of_kind(handle, .Set_Layout)
	if !ok {return}
	entry.active = false
}

create_descriptor_pool_d3d11 :: proc(
	max_sets: u32,
	types: []bk.Descriptor_Type,
	counts: []u32,
) -> (
	bk.Descriptor_Handle,
	bool,
) {
	// D3D11 has no descriptor pools -- just return a valid handle
	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_d3d11 :: proc(handle: bk.Descriptor_Handle) {
	entry, ok := descriptor_entry_of_kind(handle, .Pool)
	if !ok {return}
	entry.active = false
}

allocate_descriptor_set_d3d11 :: 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

	// Copy layout bindings into set as initial binding records
	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_d3d11 :: 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]
		slot := b.binding

		switch b.type {
		case .Combined_Image_Sampler:
			// Bind texture SRV + sampler
			if b.texture != bk.NULL_TEXTURE {
				if tex_entry, tex_ok := texture_entry(b.texture); tex_ok && tex_entry.srv != nil {
					if is_compute {
						g_d3d.ctx->CSSetShaderResources(slot, 1, &tex_entry.srv)
					} else {
						g_d3d.ctx->VSSetShaderResources(slot, 1, &tex_entry.srv)
						g_d3d.ctx->PSSetShaderResources(slot, 1, &tex_entry.srv)
					}
				}
			}
			if b.sampler != bk.NULL_SAMPLER {
				if sam_entry, sam_ok := sampler_entry(b.sampler);
				   sam_ok && sam_entry.state != nil {
					if is_compute {
						g_d3d.ctx->CSSetSamplers(slot, 1, &sam_entry.state)
					} else {
						g_d3d.ctx->VSSetSamplers(slot, 1, &sam_entry.state)
						g_d3d.ctx->PSSetSamplers(slot, 1, &sam_entry.state)
					}
				}
			}
		case .Uniform_Buffer:
			if b.buffer != bk.NULL_BUFFER {
				if buf_entry, buf_ok := buffer_entry(b.buffer); buf_ok && buf_entry.buffer != nil {
					if is_compute {
						g_d3d.ctx->CSSetConstantBuffers(slot, 1, &buf_entry.buffer)
					} else {
						g_d3d.ctx->VSSetConstantBuffers(slot, 1, &buf_entry.buffer)
						g_d3d.ctx->PSSetConstantBuffers(slot, 1, &buf_entry.buffer)
					}
				}
			}
		case .Storage_Buffer:
			if b.buffer != bk.NULL_BUFFER {
				if buf_entry, buf_ok := buffer_entry(b.buffer); buf_ok && buf_entry.buffer != nil {
					if is_compute {
						if buf_entry.uav != nil {
							g_d3d.ctx->CSSetUnorderedAccessViews(slot, 1, &buf_entry.uav, nil)
						}
					} else {
						if buf_entry.srv != nil {
							g_d3d.ctx->VSSetShaderResources(slot, 1, &buf_entry.srv)
							g_d3d.ctx->PSSetShaderResources(slot, 1, &buf_entry.srv)
						}
					}
				}
			}
		}
	}
}

update_descriptor_image_d3d11 :: 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_d3d11 :: 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_d3d11 :: proc(desc: bk.Render_Pass_Desc) -> (bk.Render_Pass_Handle, bool) {
	// D3D11 render passes are metadata-only
	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_d3d11 :: proc(handle: bk.Render_Pass_Handle) {
	entry, ok := render_pass_entry(handle)
	if !ok {return}
	entry.active = false
}

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

	entry := &g_d3d.framebuffers[handle]
	entry.color_tex = desc.color_view
	entry.depth_tex = desc.depth_view
	entry.width = desc.width
	entry.height = desc.height

	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
		}
		entry.rtv_count = color_count
		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}
			entry.color_texs[i] = view
			if tex_entry, tex_ok := texture_entry(view); tex_ok {
				entry.rtvs[i] = tex_entry.rtv
			}
			if entry.rtvs[i] == nil {return bk.NULL_FRAMEBUFFER, false}
		}
		entry.color_tex = entry.color_texs[0]
		entry.rtv = entry.rtvs[0]
	}

	if pass_entry.desc.has_depth {
		if desc.depth_view == bk.NULL_TEXTURE {return bk.NULL_FRAMEBUFFER, false}
		if tex_entry, tex_ok := texture_entry(desc.depth_view); tex_ok {
			entry.dsv = tex_entry.dsv
		}
		if entry.dsv == nil {return bk.NULL_FRAMEBUFFER, false}
	}

	entry.active = true
	return handle, true
}

destroy_framebuffer_d3d11 :: proc(handle: bk.Framebuffer_Handle) {
	entry, ok := framebuffer_entry(handle)
	if !ok {return}
	// RTV and DSV are owned by texture entries, not released here
	entry^ = {}
}