Harbor

branch main
showing the latest snapshot on main
executor.odin 85.7 KB · Plain text
gpu/compiler/executor.odin 0644 Raw
package compiler

import bk "../backend"
import ir "../render_ir"
import shader "../shader"
import "core:log"
import "core:mem"
import "core:os"

Prepared_Resource :: struct {
	resource: ir.Resource_Handle,
	kind:     ir.Resource_Kind,
	buffer:   bk.Buffer_Handle,
	texture:  bk.Texture_Handle,
	sampler:  bk.Sampler_Handle,
	owned:    bool,
}

Prepared_Pass :: struct {
	pass:         ir.Pass_Handle,
	render_pass:  bk.Render_Pass_Handle,
	framebuffer:  bk.Framebuffer_Handle,
	begin_desc:   bk.Render_Pass_Begin_Desc,
	has_viewport: bool,
	viewport:     ir.Viewport_Desc,
	has_scissor:  bool,
	scissor:      ir.Scissor_Desc,
	owned:        bool,
}

Prepared_Shader :: struct {
	shader: ir.Shader_Handle,
	stage:  ir.Shader_Stage,
	module: bk.Shader_Handle,
}

Prepared_Descriptor_Layout :: struct {
	layout:     ir.Descriptor_Layout_Handle,
	descriptor: bk.Descriptor_Handle,
}

Prepared_Descriptor_Set :: struct {
	set:        ir.Descriptor_Set_Handle,
	descriptor: bk.Descriptor_Handle,
}

Prepared_Dispatch_Descriptor :: struct {
	command:    ir.Command_Handle,
	layout:     bk.Descriptor_Handle,
	descriptor: bk.Descriptor_Handle,
}

Prepared_Pipeline :: struct {
	pipeline: ir.Pipeline_Handle,
	pass:     ir.Pass_Handle,
	variant:  Pipeline_Variant,
	stencil_ref: u8,
	handle:   bk.Pipeline_Handle,
	is_compute: bool,
	owned:    bool,
}

Pipeline_Variant :: enum {
	Content,
	Stencil_Write,
	Stencil_Test,
}

IMPORTED_PUSH_CONSTANT_CAP :: 256

Imported_Draw_Binding :: struct {
	pipeline:               bk.Pipeline_Handle,
	descriptor_sets:        [ir.MAX_DESCRIPTOR_SETS]bk.Descriptor_Handle,
	descriptor_set_indices: [ir.MAX_DESCRIPTOR_SETS]u32,
	descriptor_set_count:   u8,
	push_constant_stages:   bk.Shader_Stage_Flags,
	push_constant_size:     u32,
	push_constant_bytes:    [IMPORTED_PUSH_CONSTANT_CAP]u8,
	vertex_buffer:          bk.Buffer_Handle,
	index_buffer:           bk.Buffer_Handle,
	vertex_count:           u32,
	index_count:            u32,
	instance_count:         u32,
	first_vertex:           u32,
	first_index:            u32,
	first_instance:         u32,
	indexed:                bool,
}

Imported_Draw_Resolver :: #type proc(
	user_data: rawptr,
	frame: ^ir.Frame_IR,
	command: ir.Command_Handle,
	draw: ^ir.Draw_Command,
	out: ^Imported_Draw_Binding,
) -> bool

Planned_Frame_Begin_Proc :: #type proc(user_data: rawptr, ctx: bk.Frame_Context) -> bool

Render_Target_Cache_Key :: struct {
	frame_index:   u32,
	width, height: u32,
	color_count:   u32,
	color_formats: [ir.MAX_RENDER_TARGETS]ir.Format,
	color_usages:  [ir.MAX_RENDER_TARGETS]ir.Texture_Usage_Flags,
	has_depth:     bool,
	depth_format:  ir.Format,
	depth_usage:   ir.Texture_Usage_Flags,
}

Render_Target_Cache_Entry :: struct {
	key:         Render_Target_Cache_Key,
	colors:      [ir.MAX_RENDER_TARGETS]bk.Texture_Handle,
	depth:       bk.Texture_Handle,
	render_pass: bk.Render_Pass_Handle,
	framebuffer: bk.Framebuffer_Handle,
}

Render_Target_Cache :: struct {
	backend: ^bk.Backend,
	entries: [dynamic]Render_Target_Cache_Entry,
}

Execution_State :: struct {
	backend:                 ^bk.Backend,
	resources:               [dynamic]Prepared_Resource,
	passes:                  [dynamic]Prepared_Pass,
	shaders:                 [dynamic]Prepared_Shader,
	descriptor_layouts:      [dynamic]Prepared_Descriptor_Layout,
	descriptor_sets:         [dynamic]Prepared_Descriptor_Set,
	dispatch_descriptors:    [dynamic]Prepared_Dispatch_Descriptor,
	pipelines:               [dynamic]Prepared_Pipeline,
	descriptor_pool:         bk.Descriptor_Handle,
	imported_draw_resolver:  Imported_Draw_Resolver,
	imported_draw_user_data: rawptr,
	frame_begin_proc:        Planned_Frame_Begin_Proc,
	frame_begin_user_data:   rawptr,
}

init_execution_state :: proc(
	backend: ^bk.Backend,
	resource_cap: int = ir.DEFAULT_RESOURCE_CAP,
	pass_cap: int = ir.DEFAULT_PASS_CAP,
	allocator: mem.Allocator = context.allocator,
) -> Execution_State {
	return {
		backend = backend,
		resources = make([dynamic]Prepared_Resource, 0, resource_cap, allocator),
		passes = make([dynamic]Prepared_Pass, 0, pass_cap, allocator),
		shaders = make([dynamic]Prepared_Shader, 0, ir.DEFAULT_SHADER_CAP, allocator),
		descriptor_layouts = make(
			[dynamic]Prepared_Descriptor_Layout,
			0,
			ir.DEFAULT_DESCRIPTOR_LAYOUT_CAP,
			allocator,
		),
		descriptor_sets = make(
			[dynamic]Prepared_Descriptor_Set,
			0,
			ir.DEFAULT_DESCRIPTOR_SET_CAP,
			allocator,
		),
		dispatch_descriptors = make(
			[dynamic]Prepared_Dispatch_Descriptor,
			0,
			ir.DEFAULT_COMMAND_CAP,
			allocator,
		),
		pipelines = make([dynamic]Prepared_Pipeline, 0, ir.DEFAULT_PIPELINE_CAP, allocator),
	}
}

buffer_desc_to_backend :: proc(desc: ir.Buffer_Desc) -> bk.Buffer_Desc {
	return {
		size = desc.size,
		usage = buffer_usage_to_backend(desc.usage),
		memory = memory_properties_to_backend(desc.memory),
	}
}

set_imported_draw_resolver :: proc(
	state: ^Execution_State,
	resolver: Imported_Draw_Resolver,
	user_data: rawptr,
) {
	if state == nil {
		return
	}
	state.imported_draw_resolver = resolver
	state.imported_draw_user_data = user_data
}

set_planned_frame_begin_proc :: proc(
	state: ^Execution_State,
	callback: Planned_Frame_Begin_Proc,
	user_data: rawptr,
) {
	if state == nil {
		return
	}
	state.frame_begin_proc = callback
	state.frame_begin_user_data = user_data
}

destroy_prepared_shaders :: proc(state: ^Execution_State) {
	if state == nil || state.backend == nil {
		return
	}
	for i := len(state.shaders) - 1; i >= 0; i -= 1 {
		prepared := state.shaders[i]
		if prepared.module != bk.NULL_SHADER {
			state.backend.destroy_shader(prepared.module)
		}
	}
	clear(&state.shaders)
}

init_render_target_cache :: proc(
	backend: ^bk.Backend,
	cap: int = ir.DEFAULT_PASS_CAP,
	allocator: mem.Allocator = context.allocator,
) -> Render_Target_Cache {
	return {
		backend = backend,
		entries = make([dynamic]Render_Target_Cache_Entry, 0, cap, allocator),
	}
}

destroy_render_target_cache :: proc(cache: ^Render_Target_Cache) {
	if cache.backend != nil {
		for i := len(cache.entries) - 1; i >= 0; i -= 1 {
			entry := cache.entries[i]
			if entry.framebuffer != bk.NULL_FRAMEBUFFER {
				cache.backend.destroy_framebuffer(entry.framebuffer)
			}
			if entry.render_pass != bk.NULL_RENDER_PASS {
				cache.backend.destroy_render_pass(entry.render_pass)
			}
			if entry.depth != bk.NULL_TEXTURE {
				cache.backend.destroy_image(entry.depth)
			}
			for color in entry.colors {
				if color != bk.NULL_TEXTURE {
					cache.backend.destroy_image(color)
				}
			}
		}
	}
	delete(cache.entries)
	cache^ = {}
}

clear_execution_state_views :: proc(state: ^Execution_State) {
	if state == nil {
		return
	}
	destroy_prepared_shaders(state)
	destroy_prepared_pipelines(state)
	destroy_prepared_descriptors(state)
	clear(&state.resources)
	clear(&state.passes)
}

destroy_execution_state_views :: proc(state: ^Execution_State) {
	if state == nil {
		return
	}
	destroy_prepared_shaders(state)
	destroy_prepared_pipelines(state)
	destroy_prepared_descriptors(state)
	delete(state.resources)
	delete(state.passes)
	delete(state.shaders)
	delete(state.descriptor_layouts)
	delete(state.descriptor_sets)
	delete(state.dispatch_descriptors)
	delete(state.pipelines)
	state^ = {}
}

destroy_execution_state :: proc(state: ^Execution_State) {
	if state.backend != nil {
		for i := len(state.passes) - 1; i >= 0; i -= 1 {
			pass := state.passes[i]
			if !pass.owned {
				continue
			}
			if pass.framebuffer != bk.NULL_FRAMEBUFFER {
				state.backend.destroy_framebuffer(pass.framebuffer)
			}
			if pass.render_pass != bk.NULL_RENDER_PASS {
				state.backend.destroy_render_pass(pass.render_pass)
			}
		}
		destroy_prepared_pipelines(state)
		destroy_prepared_descriptors(state)
		for i := len(state.resources) - 1; i >= 0; i -= 1 {
			resource := state.resources[i]
			if !resource.owned {
				continue
			}
			if resource.sampler != bk.NULL_SAMPLER {
				state.backend.destroy_sampler(resource.sampler)
			}
			if resource.texture != bk.NULL_TEXTURE {
				state.backend.destroy_image(resource.texture)
			}
			if resource.buffer != bk.NULL_BUFFER {
				state.backend.destroy_buffer(resource.buffer)
			}
		}
		destroy_prepared_shaders(state)
	}
	delete(state.resources)
	delete(state.passes)
	delete(state.shaders)
	delete(state.descriptor_layouts)
	delete(state.descriptor_sets)
	delete(state.dispatch_descriptors)
	delete(state.pipelines)
	state^ = {}
}

destroy_prepared_pipelines :: proc(state: ^Execution_State) {
	if state == nil || state.backend == nil {
		return
	}
	for i := len(state.pipelines) - 1; i >= 0; i -= 1 {
		prepared := state.pipelines[i]
		if prepared.owned && prepared.handle != bk.NULL_PIPELINE {
			if prepared.is_compute {
				state.backend.destroy_compute_pipeline(prepared.handle)
			} else {
				state.backend.destroy_graphics_pipeline(prepared.handle)
			}
		}
	}
	clear(&state.pipelines)
}

destroy_prepared_descriptors :: proc(state: ^Execution_State) {
	if state == nil || state.backend == nil {
		return
	}
	if state.descriptor_pool != bk.NULL_DESCRIPTOR {
		state.backend.destroy_descriptor_pool(state.descriptor_pool)
		state.descriptor_pool = bk.NULL_DESCRIPTOR
	}
	for i := len(state.dispatch_descriptors) - 1; i >= 0; i -= 1 {
		prepared := state.dispatch_descriptors[i]
		if prepared.layout != bk.NULL_DESCRIPTOR {
			state.backend.destroy_descriptor_set_layout(prepared.layout)
		}
	}
	clear(&state.dispatch_descriptors)
	clear(&state.descriptor_sets)
	for i := len(state.descriptor_layouts) - 1; i >= 0; i -= 1 {
		prepared := state.descriptor_layouts[i]
		if prepared.descriptor != bk.NULL_DESCRIPTOR {
			state.backend.destroy_descriptor_set_layout(prepared.descriptor)
		}
	}
	clear(&state.descriptor_layouts)
}

prepare_shaders :: proc(state: ^Execution_State, frame: ^ir.Frame_IR) -> bool {
	if state == nil || frame == nil || state.backend == nil {
		log.error("gpu/compiler: prepare_shaders requires execution state, frame, and backend")
		return false
	}

	destroy_prepared_shaders(state)
	for shader_index in 0 ..< len(frame.shaders) {
		handle := ir.Shader_Handle(shader_index + 1)
		desc := &frame.shaders[shader_index]
		module, ok := prepare_shader(state, desc)
		if !ok {
			log.errorf("gpu/compiler: failed to prepare shader %s", desc.name)
			return false
		}
		append(
			&state.shaders,
			Prepared_Shader{shader = handle, stage = desc.stage, module = module},
		)
	}
	return true
}

prepared_buffer :: proc(
	state: ^Execution_State,
	resource: ir.Resource_Handle,
) -> (
	bk.Buffer_Handle,
	bool,
) {
	prepared, ok := find_prepared_resource(state, resource)
	if !ok || prepared.buffer == bk.NULL_BUFFER {
		return bk.NULL_BUFFER, false
	}
	return prepared.buffer, true
}

prepare_shader :: proc(
	state: ^Execution_State,
	desc: ^ir.Shader_Desc,
) -> (
	bk.Shader_Handle,
	bool,
) {
	if desc == nil {
		return {}, false
	}

	switch desc.source_kind {
	case .Source:
		return prepare_shader_source(state, desc, desc.source, desc.name)
	case .File:
		data, err := os.read_entire_file(desc.path, context.allocator)
		if err != nil {
			log.errorf("gpu/compiler: failed to read shader source %s", desc.path)
			return {}, false
		}
		defer delete(data)
		return prepare_shader_source(state, desc, string(data), desc.path)
	case .Bytecode:
		log.error(
			"gpu/compiler: bytecode shader descriptors need a byte slice field before lowering",
		)
	}
	return {}, false
}

prepare_shader_source :: proc(
	state: ^Execution_State,
	desc: ^ir.Shader_Desc,
	source, file: string,
) -> (
	bk.Shader_Handle,
	bool,
) {
	if desc.language != .Luma {
		log.errorf("gpu/compiler: unsupported shader language for %s", desc.name)
		return {}, false
	}

	result := shader.compile(
		source,
		{
			target = shader_target(),
			stage = shader_stage(desc.stage),
			opt_level = .None,
			hlsl_cbuffer_push_constants = true,
			hlsl_push_constant_slot = 13,
			hlsl_omit_register_spaces = true,
		},
		file,
	)
	defer shader.destroy_compile_result(&result)

	if !result.success {
		for d in result.diagnostics {
			msg := shader.format_diagnostic_with_source(d, source)
			log.errorf("gpu/compiler: shader %s: %s", desc.name, msg)
			delete(msg)
		}
		return {}, false
	}

	module, module_ok := state.backend.create_shader_module(
		{
			stage = backend_shader_stage(desc.stage),
			format = bk.REQUIRED_SHADER_FORMAT,
			name = desc.name,
			entry = "main",
			data = result.output,
		},
	)
	if !module_ok {
		log.errorf("gpu/compiler: backend failed to create shader module %s", desc.name)
	}
	return module, module_ok
}

prepared_shader :: proc(
	state: ^Execution_State,
	shader: ir.Shader_Handle,
) -> (
	bk.Shader_Handle,
	bool,
) {
	if state == nil {
		return {}, false
	}
	for &prepared in state.shaders {
		if prepared.shader == shader {
			return prepared.module, prepared.module != bk.NULL_SHADER
		}
	}
	return {}, false
}

shader_target :: proc() -> shader.Target {
	when bk.REQUIRED_SHADER_FORMAT == .SPIRV {
		return .SPIR_V
	} else when bk.REQUIRED_SHADER_FORMAT == .HLSL {
		return .HLSL_SM6
	} else when bk.REQUIRED_SHADER_FORMAT == .GLSL {
		return .GLSL_450_OPENGL
	} else {
		#assert(false, "unsupported REQUIRED_SHADER_FORMAT")
	}
}

shader_stage :: proc(stage: ir.Shader_Stage) -> shader.Shader_Stage {
	switch stage {
	case .Vertex:
		return .Vertex
	case .Fragment:
		return .Fragment
	case .Compute:
		return .Compute
	}
	return .None
}

backend_shader_stage :: proc(stage: ir.Shader_Stage) -> bk.Shader_Stage {
	switch stage {
	case .Vertex:
		return .Vertex
	case .Fragment:
		return .Fragment
	case .Compute:
		return .Compute
	}
	return .Vertex
}

render_target_cache_key :: proc(
	frame: ^ir.Frame_IR,
	pass: ^ir.Pass_Desc,
	frame_index: u32 = 0,
) -> (
	Render_Target_Cache_Key,
	bool,
) {
	return render_target_cache_key_with_capabilities(
		frame,
		pass,
		bk.implemented_base_capabilities(1, 0),
		frame_index,
	)
}

render_target_cache_key_with_capabilities :: proc(
	frame: ^ir.Frame_IR,
	pass: ^ir.Pass_Desc,
	caps: bk.Capabilities,
	frame_index: u32 = 0,
) -> (
	Render_Target_Cache_Key,
	bool,
) {
	if pass == nil || !bk.supports_color_target_count(caps, u32(pass.color_target_count)) {
		return {}, false
	}
	key: Render_Target_Cache_Key
	key.frame_index = frame_index
	if pass.color_target_count > 0 {
		key.color_count = u32(pass.color_target_count)
		for i in 0..<pass.color_target_count {
			target := pass.color_targets[i]
			resource, ok := ir.get_resource(frame, target.resource)
			if !ok || resource.kind != .Texture {
				return {}, false
			}
			if i == 0 {
				key.width = resource.texture.width
				key.height = resource.texture.height
			} else if key.width != resource.texture.width || key.height != resource.texture.height {
				return {}, false
			}
			key.color_formats[i] = resource.texture.format
			key.color_usages[i] = resource.texture.usage
		}
	}
	if pass.has_depth_target {
		target := pass.depth_target
		resource, ok := ir.get_resource(frame, target.resource)
		if !ok || resource.kind != .Texture {
			return {}, false
		}
		if key.width == 0 {
			key.width = resource.texture.width
			key.height = resource.texture.height
		} else if key.width != resource.texture.width || key.height != resource.texture.height {
			return {}, false
		}
		key.has_depth = true
		key.depth_format = resource.texture.format
		key.depth_usage = resource.texture.usage
	}
	return key, key.width != 0 && key.height != 0 && (key.color_count > 0 || key.has_depth)
}

find_render_target_cache_entry :: proc(
	cache: ^Render_Target_Cache,
	key: Render_Target_Cache_Key,
) -> (
	^Render_Target_Cache_Entry,
	bool,
) {
	if cache == nil {
		return nil, false
	}
	for &entry in cache.entries {
		if entry.key == key {
			return &entry, true
		}
	}
	return nil, false
}

create_cached_render_target_entry :: proc(
	cache: ^Render_Target_Cache,
	frame: ^ir.Frame_IR,
	pass: ^ir.Pass_Desc,
	key: Render_Target_Cache_Key,
) -> (
	^Render_Target_Cache_Entry,
	bool,
) {
	if cache == nil || cache.backend == nil {
		return nil, false
	}
	entry := Render_Target_Cache_Entry {
		key = key,
	}
	render_pass_desc, rp_desc_ok := build_render_pass_desc_with_capabilities(
		frame,
		pass,
		cache.backend.capabilities,
	)
	if !rp_desc_ok {
		return nil, false
	}
	render_pass, rp_ok := cache.backend.create_render_pass(render_pass_desc)
	if !rp_ok {
		log.error("gpu/compiler: failed to create cached offscreen render pass")
		return nil, false
	}
	entry.render_pass = render_pass

	if pass.color_target_count > 0 {
		for i in 0..<pass.color_target_count {
			target := pass.color_targets[i]
			resource, _ := ir.get_resource(frame, target.resource)
			texture, tex_ok := cache.backend.create_image(texture_desc_to_backend(resource.texture))
			if !tex_ok {
				for j in 0..<i {
					if entry.colors[j] != bk.NULL_TEXTURE {cache.backend.destroy_image(entry.colors[j])}
				}
				cache.backend.destroy_render_pass(entry.render_pass)
				return nil, false
			}
			aspect := texture_aspect_to_backend(resource.texture.usage)
			if !cache.backend.create_image_view(
				texture,
				format_to_backend(resource.texture.format),
				aspect,
			) {
				cache.backend.destroy_image(texture)
				for j in 0..<i {
					if entry.colors[j] != bk.NULL_TEXTURE {cache.backend.destroy_image(entry.colors[j])}
				}
				cache.backend.destroy_render_pass(entry.render_pass)
				return nil, false
			}
			entry.colors[i] = texture
		}
	}
	if pass.has_depth_target {
		target := pass.depth_target
		resource, _ := ir.get_resource(frame, target.resource)
		texture, tex_ok := cache.backend.create_image(texture_desc_to_backend(resource.texture))
		if !tex_ok {
			for i in 0..<pass.color_target_count {
				if entry.colors[i] != bk.NULL_TEXTURE {cache.backend.destroy_image(entry.colors[i])}
			}
			cache.backend.destroy_render_pass(entry.render_pass)
			return nil, false
		}
		aspect := texture_aspect_to_backend(resource.texture.usage)
		if !cache.backend.create_image_view(
			texture,
			format_to_backend(resource.texture.format),
			aspect,
		) {
			cache.backend.destroy_image(texture)
			for i in 0..<pass.color_target_count {
				if entry.colors[i] != bk.NULL_TEXTURE {cache.backend.destroy_image(entry.colors[i])}
			}
			cache.backend.destroy_render_pass(entry.render_pass)
			return nil, false
		}
		entry.depth = texture
	}

	temp_state := init_execution_state(cache.backend, allocator = context.allocator)
	defer destroy_execution_state_views(&temp_state)
	if pass.color_target_count > 0 {
		for i in 0..<pass.color_target_count {
			append(
				&temp_state.resources,
				Prepared_Resource {
					resource = pass.color_targets[i].resource,
					kind = .Texture,
					texture = entry.colors[i],
					owned = false,
				},
			)
		}
	}
	if pass.has_depth_target {
		append(
			&temp_state.resources,
			Prepared_Resource {
				resource = pass.depth_target.resource,
				kind = .Texture,
				texture = entry.depth,
				owned = false,
			},
		)
	}
	fb_desc, fb_ok := build_framebuffer_desc(&temp_state, frame, pass, entry.render_pass)
	if !fb_ok {
		if entry.depth != bk.NULL_TEXTURE {cache.backend.destroy_image(entry.depth)}
		for i in 0..<pass.color_target_count {
			if entry.colors[i] != bk.NULL_TEXTURE {cache.backend.destroy_image(entry.colors[i])}
		}
		cache.backend.destroy_render_pass(entry.render_pass)
		return nil, false
	}
	framebuffer, fb_create_ok := cache.backend.create_framebuffer(fb_desc)
	if !fb_create_ok {
		if entry.depth != bk.NULL_TEXTURE {cache.backend.destroy_image(entry.depth)}
		for i in 0..<pass.color_target_count {
			if entry.colors[i] != bk.NULL_TEXTURE {cache.backend.destroy_image(entry.colors[i])}
		}
		cache.backend.destroy_render_pass(entry.render_pass)
		return nil, false
	}
	entry.framebuffer = framebuffer
	append(&cache.entries, entry)
	return &cache.entries[len(cache.entries) - 1], true
}

prepare_render_targets_cached :: proc(
	state: ^Execution_State,
	frame: ^ir.Frame_IR,
	cache: ^Render_Target_Cache,
	frame_index: u32 = 0,
) -> bool {
	if state == nil || frame == nil || cache == nil || cache.backend == nil {
		return false
	}
	state.backend = cache.backend
	clear_execution_state_views(state)
	for pass_index in 0 ..< len(frame.passes) {
		pass_handle := ir.Pass_Handle(pass_index + 1)
		pass := &frame.passes[pass_index]
		if pass.kind != .Render || !pass_has_targets(pass) {
			continue
		}
		if !validate_pass_color_target_count(
			pass,
			cache.backend.capabilities,
			"cached render target pass",
		) {
			return false
		}
		key, key_ok := render_target_cache_key_with_capabilities(
			frame,
			pass,
			cache.backend.capabilities,
			frame_index,
		)
		if !key_ok {
			return false
		}
		entry, entry_ok := find_render_target_cache_entry(cache, key)
		if !entry_ok {
			entry, entry_ok = create_cached_render_target_entry(cache, frame, pass, key)
			if !entry_ok {
				return false
			}
		}
		if pass.color_target_count > 0 {
			for i in 0..<pass.color_target_count {
				append(
					&state.resources,
					Prepared_Resource {
						resource = pass.color_targets[i].resource,
						kind = .Texture,
						texture = entry.colors[i],
					},
				)
			}
		}
		if pass.has_depth_target {
			append(
				&state.resources,
				Prepared_Resource {
					resource = pass.depth_target.resource,
					kind = .Texture,
					texture = entry.depth,
				},
			)
		}
		append(
			&state.passes,
			Prepared_Pass {
				pass = pass_handle,
				render_pass = entry.render_pass,
				framebuffer = entry.framebuffer,
				begin_desc = build_begin_desc(pass, entry.render_pass, entry.framebuffer),
				has_viewport = pass.has_viewport,
				viewport = pass.viewport,
				has_scissor = pass.has_scissor,
				scissor = pass.scissor,
				owned = false,
			},
		)
	}
	return true
}

prepare_render_targets :: proc(state: ^Execution_State, frame: ^ir.Frame_IR) -> bool {
	if state.backend == nil {
		log.error("gpu/compiler: execution state has no backend")
		return false
	}

	for handle_index in 0 ..< len(frame.resources) {
		resource_handle := ir.Resource_Handle(handle_index + 1)
		resource := &frame.resources[handle_index]

		switch resource.kind {
		case .Buffer:
			if resource.lifetime == .Imported {
				if state.imported_draw_resolver != nil {
					continue
				}
				log.errorf(
					"gpu/compiler: imported buffer resource %d is not supported by executor yet",
					resource_handle,
				)
				return false
			}
			if resource.lifetime == .Transient &&
			   resource.buffer.size == 0 &&
			   state.imported_draw_resolver != nil {
				continue
			}
			buffer, ok := state.backend.create_buffer(buffer_desc_to_backend(resource.buffer))
			if !ok {
				log.errorf("gpu/compiler: failed to create buffer resource %d", resource_handle)
				return false
			}
			append(
				&state.resources,
				Prepared_Resource {
					resource = resource_handle,
					kind = .Buffer,
					buffer = buffer,
					owned = true,
				},
			)

		case .Texture:
			if !is_render_target_texture(resource) {
				continue
			}
			if resource.lifetime == .Imported {
				log.errorf(
					"gpu/compiler: imported render target resource %d is not supported by executor yet",
					resource_handle,
				)
				return false
			}

			texture, ok := state.backend.create_image(texture_desc_to_backend(resource.texture))
			if !ok {
				log.errorf("gpu/compiler: failed to create texture resource %d", resource_handle)
				return false
			}

			aspect := texture_aspect_to_backend(resource.texture.usage)
			if !state.backend.create_image_view(
				texture,
				format_to_backend(resource.texture.format),
				aspect,
			) {
				state.backend.destroy_image(texture)
				log.errorf(
					"gpu/compiler: failed to create image view for resource %d",
					resource_handle,
				)
				return false
			}

			append(
				&state.resources,
				Prepared_Resource {
					resource = resource_handle,
					kind = .Texture,
					texture = texture,
					owned = true,
				},
			)

		case .Sampler:
			if resource.lifetime == .Imported {
				log.errorf(
					"gpu/compiler: imported sampler resource %d is not supported by executor yet",
					resource_handle,
				)
				return false
			}
			sampler, ok := state.backend.create_sampler(sampler_desc_to_backend(resource.sampler))
			if !ok {
				log.errorf("gpu/compiler: failed to create sampler resource %d", resource_handle)
				return false
			}
			append(
				&state.resources,
				Prepared_Resource {
					resource = resource_handle,
					kind = .Sampler,
					sampler = sampler,
					owned = true,
				},
			)
		}
	}

	for pass_index in 0 ..< len(frame.passes) {
		pass_handle := ir.Pass_Handle(pass_index + 1)
		pass := &frame.passes[pass_index]
		if pass.kind != .Render || !pass_has_targets(pass) {
			continue
		}
		if !validate_pass_color_target_count(
			pass,
			state.backend.capabilities,
			"render target pass",
		) {
			return false
		}

		render_pass_desc, rp_desc_ok := build_render_pass_desc_with_capabilities(
			frame,
			pass,
			state.backend.capabilities,
		)
		if !rp_desc_ok {
			return false
		}

		render_pass, rp_ok := state.backend.create_render_pass(render_pass_desc)
		if !rp_ok {
			log.errorf("gpu/compiler: failed to create render pass for IR pass %d", pass_handle)
			return false
		}

		framebuffer_desc, fb_desc_ok := build_framebuffer_desc(state, frame, pass, render_pass)
		if !fb_desc_ok {
			state.backend.destroy_render_pass(render_pass)
			return false
		}
		framebuffer, fb_ok := state.backend.create_framebuffer(framebuffer_desc)
		if !fb_ok {
			state.backend.destroy_render_pass(render_pass)
			log.errorf("gpu/compiler: failed to create framebuffer for IR pass %d", pass_handle)
			return false
		}

		append(
			&state.passes,
			Prepared_Pass {
				pass = pass_handle,
				render_pass = render_pass,
				framebuffer = framebuffer,
				begin_desc = build_begin_desc(pass, render_pass, framebuffer),
				has_viewport = pass.has_viewport,
				viewport = pass.viewport,
				has_scissor = pass.has_scissor,
				scissor = pass.scissor,
				owned = true,
			},
		)
	}

	return true
}

begin_prepared_pass :: proc(
	state: ^Execution_State,
	ctx: bk.Frame_Context,
	pass: ir.Pass_Handle,
) -> bool {
	prepared, ok := find_prepared_pass(state, pass)
	if !ok {
		return false
	}
	state.backend.begin_render_pass(ctx, prepared.begin_desc)
	if prepared.has_viewport {
		v := prepared.viewport
		state.backend.set_viewport(ctx, v.x, v.y, v.width, v.height)
	}
	if prepared.has_scissor {
		s := prepared.scissor
		state.backend.set_scissor(ctx, s.x, s.y, s.width, s.height)
	}
	return true
}

end_prepared_pass :: proc(state: ^Execution_State, ctx: bk.Frame_Context) {
	state.backend.end_render_pass(ctx)
}

prepared_texture :: proc(
	state: ^Execution_State,
	resource: ir.Resource_Handle,
) -> (
	bk.Texture_Handle,
	bool,
) {
	prepared, ok := find_prepared_resource(state, resource)
	if !ok || prepared.texture == bk.NULL_TEXTURE {
		return bk.NULL_TEXTURE, false
	}
	return prepared.texture, true
}

prepared_sampler :: proc(
	state: ^Execution_State,
	resource: ir.Resource_Handle,
) -> (
	bk.Sampler_Handle,
	bool,
) {
	prepared, ok := find_prepared_resource(state, resource)
	if !ok || prepared.sampler == bk.NULL_SAMPLER {
		return bk.NULL_SAMPLER, false
	}
	return prepared.sampler, true
}

prepared_render_pass :: proc(
	state: ^Execution_State,
	pass: ir.Pass_Handle,
) -> (
	bk.Render_Pass_Handle,
	bool,
) {
	prepared, ok := find_prepared_pass(state, pass)
	if !ok || prepared.render_pass == bk.NULL_RENDER_PASS {
		return bk.NULL_RENDER_PASS, false
	}
	return prepared.render_pass, true
}

prepared_descriptor_layout :: proc(
	state: ^Execution_State,
	layout: ir.Descriptor_Layout_Handle,
) -> (
	bk.Descriptor_Handle,
	bool,
) {
	if state == nil {
		return bk.NULL_DESCRIPTOR, false
	}
	for &prepared in state.descriptor_layouts {
		if prepared.layout == layout {
			return prepared.descriptor, prepared.descriptor != bk.NULL_DESCRIPTOR
		}
	}
	return bk.NULL_DESCRIPTOR, false
}

prepared_descriptor_set :: proc(
	state: ^Execution_State,
	set: ir.Descriptor_Set_Handle,
) -> (
	bk.Descriptor_Handle,
	bool,
) {
	if state == nil {
		return bk.NULL_DESCRIPTOR, false
	}
	for &prepared in state.descriptor_sets {
		if prepared.set == set {
			return prepared.descriptor, prepared.descriptor != bk.NULL_DESCRIPTOR
		}
	}
	return bk.NULL_DESCRIPTOR, false
}

prepared_graphics_pipeline :: proc(
	state: ^Execution_State,
	pipeline: ir.Pipeline_Handle,
	pass: ir.Pass_Handle,
	variant: Pipeline_Variant = .Content,
	stencil_ref: u8 = 0,
) -> (
	bk.Pipeline_Handle,
	bool,
) {
	if state == nil {
		return bk.NULL_PIPELINE, false
	}
	for &prepared in state.pipelines {
		if prepared.pipeline == pipeline &&
		   prepared.pass == pass &&
		   prepared.variant == variant &&
		   prepared.stencil_ref == stencil_ref {
			return prepared.handle, prepared.handle != bk.NULL_PIPELINE
		}
	}
	return bk.NULL_PIPELINE, false
}

prepared_dispatch_descriptor :: proc(
	state: ^Execution_State,
	command: ir.Command_Handle,
) -> (
	bk.Descriptor_Handle,
	bool,
) {
	if state == nil {
		return bk.NULL_DESCRIPTOR, false
	}
	for &prepared in state.dispatch_descriptors {
		if prepared.command == command {
			return prepared.descriptor, prepared.descriptor != bk.NULL_DESCRIPTOR
		}
	}
	return bk.NULL_DESCRIPTOR, false
}

prepared_compute_pipeline :: proc(
	state: ^Execution_State,
	pipeline: ir.Pipeline_Handle,
) -> (
	bk.Pipeline_Handle,
	bool,
) {
	if state == nil {
		return bk.NULL_PIPELINE, false
	}
	for &prepared in state.pipelines {
		if prepared.is_compute && prepared.pipeline == pipeline {
			return prepared.handle, prepared.handle != bk.NULL_PIPELINE
		}
	}
	return bk.NULL_PIPELINE, false
}

compute_pipeline_binding_count :: proc(
	frame: ^ir.Frame_IR,
	pipeline: ir.Pipeline_Handle,
) -> (
	u32,
	bool,
) {
	count: u32
	seen := false
	if frame == nil {
		return 0, false
	}
	for &dispatch in frame.dispatch_commands {
		if dispatch.pipeline != pipeline {
			continue
		}
		if !dispatch_storage_bindings_are_compute_layout_compatible(&dispatch) {
			return 0, false
		}
		dispatch_count := u32(dispatch.storage_binding_count)
		if !seen {
			count = dispatch_count
			seen = true
		} else if count != dispatch_count {
			return 0, false
		}
	}
	return count, seen
}

prepare_compute_pipeline :: proc(
	state: ^Execution_State,
	frame: ^ir.Frame_IR,
	pipeline: ir.Pipeline_Handle,
) -> (
	bk.Pipeline_Handle,
	bool,
) {
	if handle, ok := prepared_compute_pipeline(state, pipeline); ok {
		return handle, true
	}
	if state == nil || frame == nil || state.backend == nil {
		return bk.NULL_PIPELINE, false
	}
	pipeline_desc, pipeline_ok := ir.get_pipeline(frame, pipeline)
	if !pipeline_ok || pipeline_desc.kind != .Compute {
		log.errorf("gpu/compiler: invalid compute pipeline %d", pipeline)
		return bk.NULL_PIPELINE, false
	}
	shader_handle, shader_ok := prepared_shader(state, pipeline_desc.compute.shader)
	if !shader_ok {
		log.errorf("gpu/compiler: compute pipeline %d references unprepared shader", pipeline)
		return bk.NULL_PIPELINE, false
	}
	binding_count, binding_count_ok := compute_pipeline_binding_count(frame, pipeline)
	if !binding_count_ok {
		log.errorf("gpu/compiler: compute pipeline %d has incompatible dispatch storage bindings", pipeline)
		return bk.NULL_PIPELINE, false
	}
	handle, ok := state.backend.create_compute_pipeline(
		shader_handle,
		binding_count,
		pipeline_desc.compute.push_constant_size,
	)
	if !ok {
		log.errorf("gpu/compiler: failed to create compute pipeline %d", pipeline)
		return bk.NULL_PIPELINE, false
	}
	append(
		&state.pipelines,
		Prepared_Pipeline{pipeline = pipeline, pass = ir.INVALID_PASS, handle = handle, is_compute = true, owned = true},
	)
	return handle, true
}

prepare_descriptor_layouts :: proc(state: ^Execution_State, frame: ^ir.Frame_IR) -> bool {
	if state == nil || frame == nil || state.backend == nil {
		return false
	}
	for i in 0 ..< len(frame.descriptor_layouts) {
		handle := ir.Descriptor_Layout_Handle(i + 1)
		desc := &frame.descriptor_layouts[i]
		bindings: [ir.MAX_DESCRIPTOR_BINDINGS]bk.Descriptor_Set_Layout_Binding
		for j in 0 ..< int(desc.binding_count) {
			b := desc.bindings[j]
			bindings[j] = {
				binding = b.binding,
				type    = descriptor_type_to_backend(b.type),
				count   = b.count,
				stages  = shader_stages_to_backend(b.stages),
			}
		}
		layout, ok := state.backend.create_descriptor_set_layout(
			bindings[:int(desc.binding_count)],
		)
		if !ok {
			log.errorf("gpu/compiler: failed to create descriptor layout %d", handle)
			return false
		}
		append(
			&state.descriptor_layouts,
			Prepared_Descriptor_Layout{layout = handle, descriptor = layout},
		)
	}
	return true
}

descriptor_pool_add_count :: proc(
	types: ^[3]bk.Descriptor_Type,
	counts: ^[3]u32,
	type_count: ^int,
	typ: bk.Descriptor_Type,
	count: u32,
) -> bool {
	found := false
	for j in 0 ..< type_count^ {
		if types[j] == typ {
			counts[j] += count
			found = true
			break
		}
	}
	if !found {
		if type_count^ >= len(types^) {
			log.error("gpu/compiler: descriptor pool type table overflow")
			return false
		}
		types[type_count^] = typ
		counts[type_count^] = count
		type_count^ += 1
	}
	return true
}

dispatch_needs_descriptor_set :: proc(dispatch: ^ir.Dispatch_Command) -> bool {
	return dispatch != nil && dispatch.storage_binding_count > 0
}

dispatch_storage_bindings_are_compute_layout_compatible :: proc(dispatch: ^ir.Dispatch_Command) -> bool {
	if dispatch == nil {
		return false
	}
	for i in 0 ..< int(dispatch.storage_binding_count) {
		b := dispatch.storage_bindings[i]
		if b.type != .Storage_Buffer || b.binding != u32(i) || b.resource == ir.INVALID_RESOURCE {
			return false
		}
	}
	return true
}

dispatch_descriptor_set_count :: proc(frame: ^ir.Frame_IR) -> u32 {
	if frame == nil {
		return 0
	}
	count: u32
	for &dispatch in frame.dispatch_commands {
		if dispatch_needs_descriptor_set(&dispatch) {
			count += 1
		}
	}
	return count
}

prepare_descriptor_pool :: proc(state: ^Execution_State, frame: ^ir.Frame_IR) -> bool {
	if state == nil || frame == nil || state.backend == nil {
		return false
	}
	dispatch_sets := dispatch_descriptor_set_count(frame)
	total_sets := u32(len(frame.descriptor_sets)) + dispatch_sets
	if state.descriptor_pool != bk.NULL_DESCRIPTOR || total_sets == 0 {
		return true
	}
	types: [3]bk.Descriptor_Type
	counts: [3]u32
	type_count := 0
	for &set_desc in frame.descriptor_sets {
		layout_desc, layout_ok := ir.get_descriptor_set_layout(frame, set_desc.layout)
		if !layout_ok {
			log.error("gpu/compiler: descriptor set references invalid layout")
			return false
		}
		for i in 0 ..< int(layout_desc.binding_count) {
			b := layout_desc.bindings[i]
			typ := descriptor_type_to_backend(b.type)
			count := max(b.count, 1)
			if !descriptor_pool_add_count(&types, &counts, &type_count, typ, count) {
				return false
			}
		}
	}
	for &dispatch in frame.dispatch_commands {
		if !dispatch_needs_descriptor_set(&dispatch) {
			continue
		}
		if !dispatch_storage_bindings_are_compute_layout_compatible(&dispatch) {
			log.error("gpu/compiler: dispatch storage bindings must be contiguous 0-based storage buffers")
			return false
		}
		if !descriptor_pool_add_count(
			&types,
			&counts,
			&type_count,
			bk.Descriptor_Type.Storage_Buffer,
			u32(dispatch.storage_binding_count),
		) {
			return false
		}
	}
	pool, ok := state.backend.create_descriptor_pool(
		total_sets,
		types[:type_count],
		counts[:type_count],
	)
	if !ok {
		log.error("gpu/compiler: failed to create descriptor pool")
		return false
	}
	state.descriptor_pool = pool
	return true
}

prepare_descriptor_sets :: proc(state: ^Execution_State, frame: ^ir.Frame_IR) -> bool {
	if !prepare_descriptor_pool(state, frame) {
		return false
	}
	for i in 0 ..< len(frame.descriptor_sets) {
		handle := ir.Descriptor_Set_Handle(i + 1)
		desc := &frame.descriptor_sets[i]
		layout, layout_ok := prepared_descriptor_layout(state, desc.layout)
		if !layout_ok {
			log.errorf("gpu/compiler: descriptor set %d references unprepared layout", handle)
			return false
		}
		set, set_ok := state.backend.allocate_descriptor_set(state.descriptor_pool, layout)
		if !set_ok {
			log.errorf("gpu/compiler: failed to allocate descriptor set %d", handle)
			return false
		}
		for j in 0 ..< int(desc.binding_count) {
			b := desc.bindings[j]
			switch b.type {
			case .Combined_Image_Sampler:
				texture, texture_ok := prepared_texture(state, b.resource)
				sampler, sampler_ok := prepared_sampler(state, b.sampler)
				if !texture_ok || !sampler_ok {
					log.errorf(
						"gpu/compiler: descriptor set %d binding %d references unprepared image/sampler",
						handle,
						b.binding,
					)
					return false
				}
				state.backend.update_descriptor_image(
					set,
					b.binding,
					texture,
					sampler,
					.Shader_Read_Only,
				)
			case .Uniform_Buffer, .Storage_Buffer:
				buffer, buffer_ok := prepared_buffer(state, b.resource)
				if !buffer_ok {
					log.errorf(
						"gpu/compiler: descriptor set %d binding %d references unprepared buffer",
						handle,
						b.binding,
					)
					return false
				}
				state.backend.update_descriptor_buffer(set, b.binding, buffer, b.size)
			}
		}
		append(&state.descriptor_sets, Prepared_Descriptor_Set{set = handle, descriptor = set})
	}
	return true
}

prepare_dispatch_descriptor_sets :: proc(state: ^Execution_State, frame: ^ir.Frame_IR) -> bool {
	if !prepare_descriptor_pool(state, frame) {
		return false
	}
	for command_index in 0 ..< len(frame.commands) {
		record := frame.commands[command_index]
		if record.kind != .Dispatch {
			continue
		}
		dispatch := &frame.dispatch_commands[record.index]
		if !dispatch_needs_descriptor_set(dispatch) {
			continue
		}
		if !dispatch_storage_bindings_are_compute_layout_compatible(dispatch) {
			log.error("gpu/compiler: dispatch storage bindings must match backend compute layout")
			return false
		}
		bindings: [ir.MAX_COMPUTE_BINDINGS]bk.Descriptor_Set_Layout_Binding
		for i in 0 ..< int(dispatch.storage_binding_count) {
			bindings[i] = {
				binding = u32(i),
				type    = .Storage_Buffer,
				count   = 1,
				stages  = {.Compute},
			}
		}
		layout, layout_ok := state.backend.create_descriptor_set_layout(
			bindings[:int(dispatch.storage_binding_count)],
		)
		if !layout_ok {
			log.error("gpu/compiler: failed to create dispatch descriptor layout")
			return false
		}
		set, set_ok := state.backend.allocate_descriptor_set(state.descriptor_pool, layout)
		if !set_ok {
			state.backend.destroy_descriptor_set_layout(layout)
			log.error("gpu/compiler: failed to allocate dispatch descriptor set")
			return false
		}
		for i in 0 ..< int(dispatch.storage_binding_count) {
			b := dispatch.storage_bindings[i]
			buffer, buffer_ok := prepared_buffer(state, b.resource)
			if !buffer_ok {
				state.backend.destroy_descriptor_set_layout(layout)
				log.error("gpu/compiler: dispatch descriptor references unprepared storage buffer")
				return false
			}
			state.backend.update_descriptor_buffer(set, b.binding, buffer, b.size)
		}
		append(
			&state.dispatch_descriptors,
			Prepared_Dispatch_Descriptor {
				command = ir.Command_Handle(command_index + 1),
				layout = layout,
				descriptor = set,
			},
		)
	}
	return true
}

prepare_graphics_pipeline_for_pass :: proc(
	state: ^Execution_State,
	frame: ^ir.Frame_IR,
	pipeline: ir.Pipeline_Handle,
	pass: ir.Pass_Handle,
	variant: Pipeline_Variant = .Content,
	stencil_ref: u8 = 0,
) -> (
	bk.Pipeline_Handle,
	bool,
) {
	if handle, ok := prepared_graphics_pipeline(state, pipeline, pass, variant, stencil_ref); ok {
		return handle, true
	}
	if state == nil || frame == nil || state.backend == nil {
		return bk.NULL_PIPELINE, false
	}
	pipeline_desc, pipeline_ok := ir.get_pipeline(frame, pipeline)
	if !pipeline_ok || pipeline_desc.kind != .Graphics {
		log.errorf("gpu/compiler: invalid graphics pipeline %d", pipeline)
		return bk.NULL_PIPELINE, false
	}
	render_pass, render_pass_ok := prepared_render_pass(state, pass)
	if !render_pass_ok {
		log.errorf("gpu/compiler: pipeline %d references unprepared pass %d", pipeline, pass)
		return bk.NULL_PIPELINE, false
	}
	pass_desc, pass_ok := ir.get_pass(frame, pass)
	if !pass_ok {
		log.errorf("gpu/compiler: pipeline %d references invalid pass %d", pipeline, pass)
		return bk.NULL_PIPELINE, false
	}
	vs, vs_ok := prepared_shader(state, pipeline_desc.graphics.vertex_shader)
	fs, fs_ok := prepared_shader(state, pipeline_desc.graphics.fragment_shader)
	if !vs_ok || !fs_ok {
		log.errorf("gpu/compiler: pipeline %d references unprepared shaders", pipeline)
		return bk.NULL_PIPELINE, false
	}

	descriptor_layouts: [ir.MAX_DESCRIPTOR_SETS]bk.Descriptor_Handle
	for i in 0 ..< int(pipeline_desc.descriptor_layout_count) {
		layout, layout_ok := prepared_descriptor_layout(state, pipeline_desc.descriptor_layouts[i])
		if !layout_ok {
			log.errorf(
				"gpu/compiler: pipeline %d references unprepared descriptor layout",
				pipeline,
			)
			return bk.NULL_PIPELINE, false
		}
		descriptor_layouts[i] = layout
	}

	layout := bk.variant_to_layout(
		layout_variant_to_backend(pipeline_desc.graphics.layout_variant),
	)
	binding, attrs, attr_count := bk.vertex_layout_to_pipeline_attrs(&layout)
	vertex_bindings: [2]bk.Vertex_Binding
	vertex_bindings[0] = binding
	vertex_binding_count := 1
	vertex_attrs: [16]bk.Vertex_Attribute
	for i in 0..<attr_count {
		vertex_attrs[i] = attrs[i]
	}
	vertex_attr_count := attr_count
	if pipeline_desc.graphics.has_instance_layout {
		instance_layout := bk.variant_to_layout(
			layout_variant_to_backend(pipeline_desc.graphics.instance_layout_variant),
		)
		instance_binding, instance_attrs, instance_attr_count :=
			bk.vertex_layout_to_pipeline_attrs_for_binding(
				&instance_layout,
				1,
				.Instance,
				u32(vertex_attr_count),
			)
		vertex_bindings[vertex_binding_count] = instance_binding
		vertex_binding_count += 1
		if vertex_attr_count + instance_attr_count > len(vertex_attrs) {
			log.error("gpu/compiler: combined vertex and instance attributes exceed backend capacity")
			return bk.NULL_PIPELINE, false
		}
		for i in 0..<instance_attr_count {
			vertex_attrs[vertex_attr_count + i] = instance_attrs[i]
		}
		vertex_attr_count += instance_attr_count
	}
	desc := bk.Pipeline_Desc {
		vert_shader          = vs,
		frag_shader          = fs,
		render_pass          = render_pass,
		topology             = topology_to_backend(pipeline_desc.graphics.topology),
		cull_mode            = cull_mode_to_backend(pipeline_desc.graphics.cull_mode),
		front_face           = front_face_to_backend(pipeline_desc.graphics.front_face),
		enable_blending      = pipeline_desc.graphics.enable_blending,
		blend_mode           = blend_mode_to_backend(pipeline_desc.graphics.blend_mode),
		enable_depth_test    = pipeline_desc.graphics.enable_depth_test,
		depth_format         = format_to_backend(pass_desc.depth_target.format),
		depth_only           = pass_desc.color_target_count == 0,
		color_attachment_count = u32(pass_desc.color_target_count),
		push_constant_size   = pipeline_desc.graphics.push_constant_size,
		push_constant_stages = shader_stages_to_backend(
			pipeline_desc.graphics.push_constant_stages,
		),
		descriptor_layouts   = descriptor_layouts[:int(pipeline_desc.descriptor_layout_count)],
		vertex_bindings      = vertex_bindings[:vertex_binding_count],
		vertex_attributes    = vertex_attrs[:vertex_attr_count],
	}
	for i in 0..<pass_desc.color_target_count {
		desc.color_formats[i] = format_to_backend(pass_desc.color_targets[i].format)
		desc.color_write_masks[i] = bk.COLOR_WRITE_MASK_ALL
	}
	apply_pipeline_variant(&desc, variant, stencil_ref)
	handle, ok := state.backend.create_graphics_pipeline(desc)
	if !ok {
		log.errorf("gpu/compiler: failed to create graphics pipeline %d", pipeline)
		return bk.NULL_PIPELINE, false
	}
	append(
		&state.pipelines,
		Prepared_Pipeline{pipeline = pipeline, pass = pass, variant = variant, stencil_ref = stencil_ref, handle = handle, owned = true},
	)
	return handle, true
}

@(private)
apply_pipeline_variant :: proc(desc: ^bk.Pipeline_Desc, variant: Pipeline_Variant, stencil_ref: u8) {
	if desc == nil || variant == .Content {
		return
	}
	desc.stencil = stencil_state_for_variant(variant, stencil_ref)
	if variant == .Stencil_Write {
		for i in 0..<desc.color_attachment_count {
			desc.color_write_masks[i] = 0
		}
	}
}

@(private)
stencil_state_for_variant :: proc(variant: Pipeline_Variant, stencil_ref: u8) -> bk.Stencil_State {
	state := bk.Stencil_State {
		enable     = true,
		read_mask  = 0xFF,
		write_mask = 0xFF,
		reference  = stencil_ref,
	}
	switch variant {
	case .Stencil_Write:
		face := bk.Stencil_Face_State {
			fail_op       = .Replace,
			pass_op       = .Replace,
			depth_fail_op = .Replace,
			compare_op    = .Always,
		}
		state.front = face
		state.back = face
	case .Stencil_Test:
		state.write_mask = 0
		face := bk.Stencil_Face_State {
			fail_op       = .Keep,
			pass_op       = .Keep,
			depth_fail_op = .Keep,
			compare_op    = .Equal,
		}
		state.front = face
		state.back = face
	case .Content:
		state.enable = false
	}
	return state
}

prepare_planned_frame :: proc(state: ^Execution_State, frame: ^ir.Frame_IR) -> bool {
	if state == nil || frame == nil || state.backend == nil {
		log.error("gpu/compiler: prepare_planned_frame requires execution state, frame, and backend")
		return false
	}
	if !prepare_render_targets(state, frame) {
		log.error("gpu/compiler: planned frame render-target preparation failed")
		return false
	}
	if !prepare_shaders(state, frame) {
		log.error("gpu/compiler: planned frame shader preparation failed")
		return false
	}
	if !prepare_descriptor_layouts(state, frame) {
		log.error("gpu/compiler: planned frame descriptor-layout preparation failed")
		return false
	}
	if !prepare_descriptor_sets(state, frame) {
		log.error("gpu/compiler: planned frame descriptor-set preparation failed")
		return false
	}
	if !prepare_dispatch_descriptor_sets(state, frame) {
		log.error("gpu/compiler: planned frame dispatch-descriptor preparation failed")
		return false
	}
	return true
}

execute_prepared_planned_frame :: proc(
	state: ^Execution_State,
	frame: ^ir.Frame_IR,
	clear_color: [4]f32,
	diagnostics: ^ir.Diagnostic_List = nil,
) -> bool {
	if state == nil || frame == nil || state.backend == nil {
		return false
	}
	ctx, ok := state.backend.begin_frame(clear_color)
	if !ok {
		return false
	}
	if state.frame_begin_proc != nil &&
	   !state.frame_begin_proc(state.frame_begin_user_data, ctx) {
		_ = state.backend.end_frame(ctx)
		return false
	}
	success := execute_prepared_planned_commands(state, frame, ctx, clear_color, diagnostics)
	_ = state.backend.end_frame(ctx)
	return success
}

execute_prepared_planned_commands :: proc(
	state: ^Execution_State,
	frame: ^ir.Frame_IR,
	ctx: bk.Frame_Context,
	clear_color: [4]f32,
	diagnostics: ^ir.Diagnostic_List = nil,
) -> bool {
	if state == nil || frame == nil || state.backend == nil {
		return false
	}
	stencil_supported := bk.supports(state.backend.capabilities, .Stencil_Clips)
	if frame_has_mask_clips(frame) && !stencil_supported {
		if diagnostics != nil {
			ir.validate_mask_clips_supported(frame, diagnostics, stencil_supported)
		}
		return false
	}
	if diagnostics != nil && !ir.validate_resource_accesses(frame, diagnostics) {
		return false
	}
	plan := ir.plan_frame_commands(frame, context.temp_allocator)
	defer ir.command_plan_destroy(&plan)

	active_pass := ir.INVALID_PASS
	active_default := false
	default_pass_closed := false
	success := true
	for command in plan {
		if command.culled {
			continue
		}
		if command.record.kind == .Dispatch {
			if active_default || default_pass_closed {
				planned_command_error(
					diagnostics,
					.Invalid_Order,
					command.handle,
					"dispatch after present pass is unsupported",
				)
				success = false
				break
			}
			if active_pass != ir.INVALID_PASS {
				state.backend.end_render_pass(ctx)
				active_pass = ir.INVALID_PASS
			}
			dispatch, dispatch_ok := ir.get_dispatch(frame, command.handle)
			if !dispatch_ok {
				planned_command_error(
					diagnostics,
					.Invalid_Command,
					command.handle,
					"planned dispatch command has no payload",
				)
				success = false
				break
			}
			if !execute_planned_dispatch(state, frame, ctx, command.handle, dispatch, diagnostics) {
				success = false
				break
			}
			continue
		}
		if command.record.kind != .Draw {
			planned_command_error(
				diagnostics,
				.Unsupported,
				command.handle,
				"planned executor only supports draw and dispatch commands",
			)
			success = false
			break
		}
		draw, draw_ok := ir.get_draw(frame, command.handle)
		if !draw_ok {
			planned_command_error(
				diagnostics,
				.Invalid_Command,
				command.handle,
				"planned draw command has no payload",
			)
			success = false
			break
		}
		pass, pass_ok := planned_draw_pass(frame, draw)
		if !pass_ok {
			planned_command_error(
				diagnostics,
				.Invalid_Pass,
				command.handle,
				"planned draw command has no executable pass",
			)
			success = false
			break
		}
		if pass != ir.INVALID_PASS && default_pass_closed {
			planned_command_error(
				diagnostics,
				.Invalid_Order,
				command.handle,
				"offscreen work after present pass is unsupported",
			)
			success = false
			break
		}
		if !ensure_planned_pass(
			state,
			frame,
			ctx,
			pass,
			draw.packet.target,
			clear_color,
			&active_pass,
			&active_default,
			&default_pass_closed,
		) {
			success = false
			break
		}
		if !execute_planned_draw(state, frame, ctx, command.handle, draw, pass, diagnostics) {
			success = false
			break
		}
	}
	if active_pass != ir.INVALID_PASS || active_default {
		state.backend.end_render_pass(ctx)
	}
	return success
}

@(private)
execute_planned_dispatch :: proc(
	state: ^Execution_State,
	frame: ^ir.Frame_IR,
	ctx: bk.Frame_Context,
	handle: ir.Command_Handle,
	dispatch: ^ir.Dispatch_Command,
	diagnostics: ^ir.Diagnostic_List,
) -> bool {
	if dispatch == nil {
		planned_command_error(
			diagnostics,
			.Invalid_Command,
			handle,
			"planned dispatch command has no payload",
		)
		return false
	}
	pipeline_handle, pipeline_ok := prepare_compute_pipeline(state, frame, dispatch.pipeline)
	if !pipeline_ok {
		planned_command_error(
			diagnostics,
			.Invalid_Pipeline,
			handle,
			"planned dispatch references invalid or unprepared compute pipeline",
		)
		return false
	}
	state.backend.bind_compute_pipeline(ctx, pipeline_handle)
	if dispatch_needs_descriptor_set(dispatch) {
		set, set_ok := prepared_dispatch_descriptor(state, handle)
		if !set_ok {
			planned_command_error(
				diagnostics,
				.Invalid_Descriptor,
				handle,
				"planned dispatch references unprepared storage descriptor set",
			)
			return false
		}
		state.backend.bind_descriptor_set(ctx, pipeline_handle, set, 0)
	}
	if len(dispatch.push_constants) > 0 {
		state.backend.push_constants(
			ctx,
			pipeline_handle,
			{.Compute},
			0,
			u32(len(dispatch.push_constants)),
			raw_data(dispatch.push_constants),
		)
	}
	state.backend.dispatch_compute(ctx, dispatch.groups[0], dispatch.groups[1], dispatch.groups[2])
	if dispatch.barrier_after {
		state.backend.compute_barrier(ctx)
	}
	return true
}

@(private)
planned_command_error :: proc(
	diagnostics: ^ir.Diagnostic_List,
	code: ir.Diagnostic_Code,
	command: ir.Command_Handle,
	message: string,
) {
	if diagnostics != nil {
		ir.add_command_error(diagnostics, code, command, message)
		return
	}
	log.errorf("gpu/compiler: command %d: %s", command, message)
}

@(private)
draw_uses_imported_resource :: proc(frame: ^ir.Frame_IR, draw: ^ir.Draw_Command) -> bool {
	if frame == nil || draw == nil || draw.packet.geometry.resource == ir.INVALID_RESOURCE {
		return false
	}
	resource, ok := ir.get_resource(frame, draw.packet.geometry.resource)
	return ok && resource.lifetime == .Imported
}

@(private)
draw_uses_runtime_bound_resource :: proc(
	state: ^Execution_State,
	frame: ^ir.Frame_IR,
	draw: ^ir.Draw_Command,
) -> bool {
	if state == nil || state.imported_draw_resolver == nil ||
	   frame == nil || draw == nil || draw.packet.geometry.resource == ir.INVALID_RESOURCE {
		return false
	}
	resource, ok := ir.get_resource(frame, draw.packet.geometry.resource)
	if !ok || resource.kind != .Buffer {
		return false
	}
	if resource.lifetime == .Imported {
		return true
	}
	return resource.lifetime == .Transient && resource.buffer.size == 0
}

@(private)
planned_draw_pass :: proc(frame: ^ir.Frame_IR, draw: ^ir.Draw_Command) -> (ir.Pass_Handle, bool) {
	if draw.pass != ir.INVALID_PASS {
		return draw.pass, true
	}
	target, target_ok := ir.get_target(frame, draw.packet.target)
	if !target_ok {
		return ir.INVALID_PASS, true
	}
	return target.pass, true
}

@(private)
ensure_planned_pass :: proc(
	state: ^Execution_State,
	frame: ^ir.Frame_IR,
	ctx: bk.Frame_Context,
	pass: ir.Pass_Handle,
	target: ir.Target_Handle,
	clear_color: [4]f32,
	active_pass: ^ir.Pass_Handle,
	active_default: ^bool,
	default_pass_closed: ^bool,
) -> bool {
	if pass == ir.INVALID_PASS {
		if active_default^ {
			return true
		}
		if active_pass^ != ir.INVALID_PASS {
			state.backend.end_render_pass(ctx)
			active_pass^ = ir.INVALID_PASS
		}
		state.backend.begin_default_pass(ctx, clear_color)
		extent := state.backend.get_extent()
		width := extent.width
		height := extent.height
		if target_desc, target_ok := ir.get_target(frame, target);
		   target_ok && target_desc.width > 0 && target_desc.height > 0 {
			width = target_desc.width
			height = target_desc.height
		}
		state.backend.set_viewport(ctx, 0, 0, f32(width), f32(height))
		state.backend.set_scissor(ctx, 0, 0, width, height)
		active_default^ = true
		return true
	}
	if active_pass^ == pass {
		return true
	}
	if active_default^ {
		state.backend.end_render_pass(ctx)
		active_default^ = false
		default_pass_closed^ = true
	}
	if active_pass^ != ir.INVALID_PASS {
		state.backend.end_render_pass(ctx)
		active_pass^ = ir.INVALID_PASS
	}
	if !begin_prepared_pass(state, ctx, pass) {
		log.errorf("gpu/compiler: failed to begin prepared pass %d", pass)
		return false
	}
	active_pass^ = pass
	return true
}

@(private)
execute_planned_draw :: proc(
	state: ^Execution_State,
	frame: ^ir.Frame_IR,
	ctx: bk.Frame_Context,
	handle: ir.Command_Handle,
	draw: ^ir.Draw_Command,
	pass: ir.Pass_Handle,
	diagnostics: ^ir.Diagnostic_List = nil,
) -> bool {
	switch draw.kind {
	case .Raw, .External:
		planned_command_error(
			diagnostics,
			.Unsupported,
			handle,
			"draw kind is not supported by Phase 5a planned executor",
		)
		return false
	case .Mesh,
	     .Indexed_Mesh,
	     .Vertex_Stream,
	     .Quad_Stream,
	     .Line_Stream,
	     .Point_Stream,
	     .Glyph_Quad_Stream,
	     .Indirect:
	// handled below
	}
	pipeline := draw.pipeline
	descriptor_set := ir.INVALID_DESCRIPTOR_SET
	if material, material_ok := ir.get_material(frame, draw.packet.material); material_ok {
		if pipeline == ir.INVALID_PIPELINE {
			pipeline = material.pipeline
		}
		descriptor_set = material.descriptor_set
	}
	if draw_uses_runtime_bound_resource(state, frame, draw) {
		if draw.packet.instances.resource != ir.INVALID_RESOURCE ||
		   draw.packet.instances.indirect != ir.INVALID_RESOURCE {
			planned_command_error(
				diagnostics,
				.Unsupported,
				handle,
				"instance buffers or indirect instances are not supported by Phase 5b imported executor",
			)
			return false
		}
		if !apply_planned_draw_scissor(state, frame, ctx, handle, draw, pass, diagnostics) {
			return false
		}
		return execute_imported_planned_draw(state, frame, ctx, handle, draw, diagnostics)
	}
	if pipeline == ir.INVALID_PIPELINE {
		planned_command_error(
			diagnostics,
			.Invalid_Pipeline,
			handle,
			"planned draw command has no graphics pipeline",
		)
		return false
	}
	pipeline_desc, pipeline_desc_ok := ir.get_pipeline(frame, pipeline)
	if !pipeline_desc_ok {
		return false
	}
	mask_clip, mask_desc, mask_ref, has_mask := planned_draw_mask(frame, draw)
	if has_mask {
		if !validate_mask_draw_for_stencil(state, frame, handle, draw, pass, pipeline_desc, mask_clip, mask_desc, diagnostics) {
			return false
		}
	}
	pipeline_variant := Pipeline_Variant.Stencil_Test if has_mask else .Content
	pipeline_handle, pipeline_ok := prepare_graphics_pipeline_for_pass(
		state,
		frame,
		pipeline,
		pass,
		pipeline_variant,
		mask_ref,
	)
	if !pipeline_ok {
		return false
	}
	prepared_set := bk.NULL_DESCRIPTOR
	needs_descriptors := pipeline_desc.descriptor_layout_count > 0
	if needs_descriptors {
		if descriptor_set == ir.INVALID_DESCRIPTOR_SET {
			planned_command_error(
				diagnostics,
				.Invalid_Descriptor,
				handle,
				"planned draw needs descriptors but has no descriptor set",
			)
			return false
		}
		set, set_ok := prepared_descriptor_set(state, descriptor_set)
		if !set_ok {
			planned_command_error(
				diagnostics,
				.Invalid_Descriptor,
				handle,
				"planned draw references unprepared descriptor set",
			)
			return false
		}
		prepared_set = set
	}
	if has_mask {
		if !execute_mask_write_draw(
			state,
			frame,
			ctx,
			handle,
			draw,
			pass,
			pipeline,
			mask_desc,
			mask_ref,
			prepared_set,
			needs_descriptors,
			diagnostics,
		) {
			return false
		}
	}
	state.backend.bind_graphics_pipeline(ctx, pipeline_handle)
	if needs_descriptors {
		state.backend.bind_descriptor_set(ctx, pipeline_handle, prepared_set, 0)
	}
	if !apply_planned_draw_scissor(state, frame, ctx, handle, draw, pass, diagnostics) {
		return false
	}
	if draw.packet.instances.resource != ir.INVALID_RESOURCE &&
	   !planned_pipeline_accepts_instance_stream(pipeline_desc) {
		planned_command_error(
			diagnostics,
			.Unsupported,
			handle,
			"planned draw has an instance buffer but pipeline has no instance vertex layout",
		)
		return false
	}
	if draw.kind == .Indirect || draw.packet.instances.indirect != ir.INVALID_RESOURCE {
		return execute_planned_indirect_draw(
			state,
			frame,
			ctx,
			handle,
			draw,
			pipeline_desc,
			diagnostics,
		)
	}

	geometry := draw.packet.geometry
	vertex_buffer, vertex_ok := prepared_buffer(state, geometry.resource)
	if !vertex_ok {
		planned_command_error(
			diagnostics,
			.Invalid_Resource,
			handle,
			"planned draw references unprepared geometry buffer",
		)
		return false
	}
	if !bind_planned_vertex_streams(state, ctx, handle, draw, pipeline_desc, vertex_buffer, diagnostics) {
		return false
	}
	instance_count := draw.instance_count
	if instance_count == 0 {
		if draw.packet.instances.count > 0 {
			instance_count = draw.packet.instances.count
		} else {
			instance_count = 1
		}
	}
	if draw.kind == .Indexed_Mesh || geometry.kind == .Indexed_Mesh {
		index_count := geometry.index_count
		if index_count == 0 {
			index_count = draw.vertex_count
		}
		if index_count == 0 {
			planned_command_error(
				diagnostics,
				.Invalid_Command,
				handle,
				"planned indexed draw has no index count",
			)
			return false
		}
		state.backend.bind_index_buffer(ctx, vertex_buffer)
		state.backend.draw_indexed(
			ctx,
			index_count,
			instance_count,
			geometry.first_index,
			i32(geometry.first_vertex),
			0,
		)
		return true
	}
	vertex_count := draw.vertex_count
	if vertex_count == 0 {
		vertex_count = geometry.vertex_count
	}
	if vertex_count == 0 {
		planned_command_error(
			diagnostics,
			.Invalid_Command,
			handle,
			"planned draw has no vertex count",
		)
		return false
	}
	state.backend.draw(ctx, vertex_count, instance_count, geometry.first_vertex, 0)
	return true
}

@(private)
planned_draw_mask :: proc(
	frame: ^ir.Frame_IR,
	draw: ^ir.Draw_Command,
) -> (
	^ir.Clip_Desc,
	^ir.Mask_Desc,
	u8,
	bool,
) {
	if frame == nil || draw == nil || draw.packet.clip == ir.INVALID_CLIP {
		return nil, nil, 0, false
	}
	clip, clip_ok := ir.get_clip(frame, draw.packet.clip)
	if !clip_ok || clip.kind != .Mask {
		return nil, nil, 0, false
	}
	mask, mask_ok := ir.get_mask(frame, clip.mask)
	if !mask_ok {
		return clip, nil, 0, true
	}
	ref := u8(clip.mask)
	return clip, mask, ref, true
}

@(private)
validate_mask_draw_for_stencil :: proc(
	state: ^Execution_State,
	frame: ^ir.Frame_IR,
	handle: ir.Command_Handle,
	draw: ^ir.Draw_Command,
	pass: ir.Pass_Handle,
	pipeline_desc: ^ir.Pipeline_Desc,
	clip: ^ir.Clip_Desc,
	mask: ^ir.Mask_Desc,
	diagnostics: ^ir.Diagnostic_List = nil,
) -> bool {
	if state == nil || frame == nil || draw == nil || pipeline_desc == nil || clip == nil || mask == nil {
		planned_command_error(diagnostics, .Invalid_Mask, handle, "planned mask clip references invalid mask")
		return false
	}
	if !bk.supports(state.backend.capabilities, .Stencil_Clips) {
		planned_command_error(diagnostics, .Stencil_Unsupported, handle, "mask clips require backend Stencil_Clips support")
		return false
	}
	if int(clip.mask) > 255 {
		planned_command_error(diagnostics, .Unsupported, handle, "mask clips require an 8-bit stencil reference")
		return false
	}
	if mask.target != clip.target || clip.target != draw.packet.target {
		planned_command_error(diagnostics, .Invalid_Mask, handle, "mask clip target does not match draw target")
		return false
	}
	if mask.layout_variant != pipeline_desc.graphics.layout_variant {
		planned_command_error(diagnostics, .Invalid_Mask, handle, "mask coverage layout must match clipped pipeline layout")
		return false
	}
	pass_desc, pass_ok := ir.get_pass(frame, pass)
	if !pass_ok || !pass_desc.has_depth_target {
		planned_command_error(diagnostics, .Stencil_Unsupported, handle, "mask clips require a stencil-capable depth target")
		return false
	}
	depth_format := format_to_backend(pass_desc.depth_target.format)
	if !backend_format_has_stencil(depth_format) {
		planned_command_error(diagnostics, .Stencil_Unsupported, handle, "mask clips require a packed depth-stencil target")
		return false
	}
	return true
}

@(private)
execute_mask_write_draw :: proc(
	state: ^Execution_State,
	frame: ^ir.Frame_IR,
	ctx: bk.Frame_Context,
	handle: ir.Command_Handle,
	draw: ^ir.Draw_Command,
	pass: ir.Pass_Handle,
	pipeline: ir.Pipeline_Handle,
	mask: ^ir.Mask_Desc,
	mask_ref: u8,
	prepared_set: bk.Descriptor_Handle,
	needs_descriptors: bool,
	diagnostics: ^ir.Diagnostic_List = nil,
) -> bool {
	mask_pipeline, mask_pipeline_ok := prepare_graphics_pipeline_for_pass(
		state,
		frame,
		pipeline,
		pass,
		.Stencil_Write,
		mask_ref,
	)
	if !mask_pipeline_ok {
		return false
	}
	mask_buffer, mask_buffer_ok := prepared_buffer(state, mask.coverage_resource)
	if !mask_buffer_ok {
		planned_command_error(diagnostics, .Invalid_Resource, handle, "mask coverage references unprepared geometry buffer")
		return false
	}
	state.backend.bind_graphics_pipeline(ctx, mask_pipeline)
	if needs_descriptors {
		state.backend.bind_descriptor_set(ctx, mask_pipeline, prepared_set, 0)
	}
	if !apply_planned_draw_scissor(state, frame, ctx, handle, draw, pass, diagnostics) {
		return false
	}
	state.backend.bind_vertex_buffer(ctx, mask_buffer)
	if mask.index_resource != ir.INVALID_RESOURCE {
		index_buffer, index_ok := prepared_buffer(state, mask.index_resource)
		if !index_ok {
			planned_command_error(diagnostics, .Invalid_Resource, handle, "mask coverage references unprepared index buffer")
			return false
		}
		state.backend.bind_index_buffer(ctx, index_buffer)
		state.backend.draw_indexed(ctx, mask.index_count, 1, 0, 0, 0)
		return true
	}
	state.backend.draw(ctx, mask.vertex_count, 1, 0, 0)
	return true
}

@(private)
execute_planned_indirect_draw :: proc(
	state: ^Execution_State,
	frame: ^ir.Frame_IR,
	ctx: bk.Frame_Context,
	handle: ir.Command_Handle,
	draw: ^ir.Draw_Command,
	pipeline_desc: ^ir.Pipeline_Desc,
	diagnostics: ^ir.Diagnostic_List = nil,
) -> bool {
	argument_resource := draw.packet.instances.indirect
	if argument_resource == ir.INVALID_RESOURCE {
		planned_command_error(
			diagnostics,
			.Invalid_Resource,
			handle,
			"indirect planned draw has no argument buffer",
		)
		return false
	}
	if (draw.packet.instances.offset & 3) != 0 {
		planned_command_error(
			diagnostics,
			.Invalid_Command,
			handle,
			"indirect planned draw argument offset must be 4-byte aligned",
		)
		return false
	}
	draw_count := draw.packet.instances.count
	if draw_count == 0 {
		draw_count = 1
	}
	if draw_count != 1 {
		planned_command_error(
			diagnostics,
			.Unsupported,
			handle,
			"indirect planned draw currently supports one command",
		)
		return false
	}
	geometry := draw.packet.geometry
	vertex_buffer, vertex_ok := prepared_buffer(state, geometry.resource)
	if !vertex_ok {
		planned_command_error(
			diagnostics,
			.Invalid_Resource,
			handle,
			"indirect planned draw references unprepared geometry buffer",
		)
		return false
	}
	argument_buffer, argument_ok := prepared_buffer(state, argument_resource)
	if !argument_ok {
		planned_command_error(
			diagnostics,
			.Invalid_Resource,
			handle,
			"indirect planned draw references unprepared argument buffer",
		)
		return false
	}
	indexed := geometry.kind == .Indexed_Mesh
	min_stride := u32(size_of(bk.Indirect_Draw_Indexed_Args)) if indexed else u32(size_of(bk.Indirect_Draw_Args))
	stride := draw.packet.instances.stride
	if stride == 0 {
		stride = min_stride
	}
	if stride < min_stride || (stride & 3) != 0 {
		planned_command_error(
			diagnostics,
			.Invalid_Command,
			handle,
			"indirect planned draw stride must be 4-byte aligned and at least the argument size",
		)
		return false
	}
	if !bind_planned_vertex_streams(state, ctx, handle, draw, pipeline_desc, vertex_buffer, diagnostics) {
		return false
	}
	if indexed {
		if state.backend.draw_indexed_indirect == nil {
			planned_command_error(
				diagnostics,
				.Unsupported,
				handle,
				"backend does not support indexed indirect draws",
			)
			return false
		}
		state.backend.bind_index_buffer(ctx, vertex_buffer)
		state.backend.draw_indexed_indirect(
			ctx,
			argument_buffer,
			draw.packet.instances.offset,
			draw_count,
			stride,
		)
		return true
	}
	if state.backend.draw_indirect == nil {
		planned_command_error(
			diagnostics,
			.Unsupported,
			handle,
			"backend does not support indirect draws",
		)
		return false
	}
	state.backend.draw_indirect(
		ctx,
		argument_buffer,
		draw.packet.instances.offset,
		draw_count,
		stride,
	)
	return true
}

@(private)
planned_pipeline_accepts_instance_stream :: proc(desc: ^ir.Pipeline_Desc) -> bool {
	return desc != nil && desc.kind == .Graphics && desc.graphics.has_instance_layout
}

@(private)
pipeline_vertex_stride :: proc(desc: ^ir.Pipeline_Desc) -> u32 {
	if desc == nil || desc.kind != .Graphics {
		return 0
	}
	layout := bk.variant_to_layout(layout_variant_to_backend(desc.graphics.layout_variant))
	binding, _, _ := bk.vertex_layout_to_pipeline_attrs(&layout)
	return binding.stride
}

@(private)
bind_planned_vertex_streams :: proc(
	state: ^Execution_State,
	ctx: bk.Frame_Context,
	handle: ir.Command_Handle,
	draw: ^ir.Draw_Command,
	pipeline_desc: ^ir.Pipeline_Desc,
	vertex_buffer: bk.Buffer_Handle,
	diagnostics: ^ir.Diagnostic_List = nil,
) -> bool {
	if draw == nil {
		return false
	}
	if state.backend.bind_vertex_buffer_slot == nil {
		planned_command_error(
			diagnostics,
			.Unsupported,
			handle,
			"backend does not support slot-aware vertex buffer binding",
		)
		return false
	}
	state.backend.bind_vertex_buffer_slot(
		ctx,
		0,
		vertex_buffer,
		0,
		pipeline_vertex_stride(pipeline_desc),
	)
	if draw.packet.instances.resource == ir.INVALID_RESOURCE {
		return true
	}
	if draw.packet.instances.stride == 0 {
		planned_command_error(
			diagnostics,
			.Invalid_Command,
			handle,
			"planned instance-buffer draw requires a nonzero instance stride",
		)
		return false
	}
	instance_buffer, instance_ok := prepared_buffer(state, draw.packet.instances.resource)
	if !instance_ok {
		planned_command_error(
			diagnostics,
			.Invalid_Resource,
			handle,
			"planned instance-buffer draw references unprepared instance buffer",
		)
		return false
	}
	state.backend.bind_vertex_buffer_slot(
		ctx,
		1,
		instance_buffer,
		draw.packet.instances.offset,
		draw.packet.instances.stride,
	)
	return true
}

@(private)
apply_planned_draw_scissor :: proc(
	state: ^Execution_State,
	frame: ^ir.Frame_IR,
	ctx: bk.Frame_Context,
	handle: ir.Command_Handle,
	draw: ^ir.Draw_Command,
	pass: ir.Pass_Handle,
	diagnostics: ^ir.Diagnostic_List = nil,
) -> bool {
	scissor: ir.Scissor_Desc
	if draw.packet.clip != ir.INVALID_CLIP {
		scissor_ok: bool
		scissor, scissor_ok = ir.packet_effective_scissor(frame, draw.packet)
		if !scissor_ok {
			if clip, clip_ok := ir.get_clip(frame, draw.packet.clip); clip_ok && clip.kind == .Mask {
				if !bk.supports(state.backend.capabilities, .Stencil_Clips) {
					planned_command_error(
						diagnostics,
						.Stencil_Unsupported,
						handle,
						"mask clips require backend Stencil_Clips support",
					)
					return false
				}
				scissor, scissor_ok = planned_draw_default_scissor(
					state,
					frame,
					pass,
					draw.packet.target,
				)
				if scissor_ok {
					state.backend.set_scissor(
						ctx,
						scissor.x,
						scissor.y,
						scissor.width,
						scissor.height,
					)
					return true
				}
			}
			planned_command_error(
				diagnostics,
				.Invalid_Command,
				handle,
				"planned draw has invalid clip",
			)
			return false
		}
	} else {
		scissor_ok: bool
		scissor, scissor_ok = planned_draw_default_scissor(state, frame, pass, draw.packet.target)
		if !scissor_ok {
			planned_command_error(
				diagnostics,
				.Invalid_Command,
				handle,
				"planned draw has no valid scissor extent",
			)
			return false
		}
	}
	state.backend.set_scissor(ctx, scissor.x, scissor.y, scissor.width, scissor.height)
	return true
}

@(private)
frame_has_mask_clips :: proc(frame: ^ir.Frame_IR) -> bool {
	if frame == nil {
		return false
	}
	for clip in frame.clips {
		if clip.kind == .Mask {
			return true
		}
	}
	return false
}

@(private)
planned_draw_default_scissor :: proc(
	state: ^Execution_State,
	frame: ^ir.Frame_IR,
	pass: ir.Pass_Handle,
	target: ir.Target_Handle,
) -> (ir.Scissor_Desc, bool) {
	if pass != ir.INVALID_PASS {
		pass_desc, pass_ok := ir.get_pass(frame, pass)
		if !pass_ok {
			return {}, false
		}
		if pass_desc.has_scissor {
			return pass_desc.scissor, true
		}
		if pass_desc.has_viewport {
			return {
				x = i32(pass_desc.viewport.x),
				y = i32(pass_desc.viewport.y),
				width = u32(pass_desc.viewport.width),
				height = u32(pass_desc.viewport.height),
			}, true
		}
		width, height, extent_ok := planned_pass_extent(frame, pass_desc)
		if extent_ok {
			return {x = 0, y = 0, width = width, height = height}, true
		}
		extent := state.backend.get_extent()
		return {
			x = 0,
			y = 0,
			width = extent.width,
			height = extent.height,
		}, extent.width > 0 && extent.height > 0
	}
	extent := state.backend.get_extent()
	width := extent.width
	height := extent.height
	if target_desc, target_ok := ir.get_target(frame, target);
	   target_ok && target_desc.width > 0 && target_desc.height > 0 {
		width = target_desc.width
		height = target_desc.height
	}
	return {x = 0, y = 0, width = width, height = height}, width > 0 && height > 0
}

@(private)
planned_pass_extent :: proc(
	frame: ^ir.Frame_IR,
	pass: ^ir.Pass_Desc,
) -> (u32, u32, bool) {
	if pass.color_target_count > 0 {
		target := pass.color_targets[0]
		resource, ok := ir.get_resource(frame, target.resource)
		if ok && resource.kind == .Texture && resource.texture.width > 0 && resource.texture.height > 0 {
			return resource.texture.width, resource.texture.height, true
		}
	}
	if pass.has_depth_target {
		target := pass.depth_target
		resource, ok := ir.get_resource(frame, target.resource)
		if ok && resource.kind == .Texture && resource.texture.width > 0 && resource.texture.height > 0 {
			return resource.texture.width, resource.texture.height, true
		}
	}
	return 0, 0, false
}

@(private)
execute_imported_planned_draw :: proc(
	state: ^Execution_State,
	frame: ^ir.Frame_IR,
	ctx: bk.Frame_Context,
	handle: ir.Command_Handle,
	draw: ^ir.Draw_Command,
	diagnostics: ^ir.Diagnostic_List = nil,
) -> bool {
	if state.imported_draw_resolver == nil {
		planned_command_error(
			diagnostics,
			.Unsupported,
			handle,
			"planned draw references imported geometry without an imported resource resolver",
		)
		return false
	}
	binding: Imported_Draw_Binding
	if !state.imported_draw_resolver(
		state.imported_draw_user_data,
		frame,
		handle,
		draw,
		&binding,
	) {
		planned_command_error(
			diagnostics,
			.Invalid_Resource,
			handle,
			"imported draw resolver failed",
		)
		return false
	}
	if binding.pipeline == bk.NULL_PIPELINE || binding.vertex_buffer == bk.NULL_BUFFER {
		planned_command_error(
			diagnostics,
			.Invalid_Resource,
			handle,
			"imported draw resolver returned incomplete backend handles",
		)
		return false
	}
	if binding.instance_count == 0 {
		binding.instance_count = 1
	}
	state.backend.bind_graphics_pipeline(ctx, binding.pipeline)
	for i in 0 ..< int(binding.descriptor_set_count) {
		set := binding.descriptor_sets[i]
		if set != bk.NULL_DESCRIPTOR {
			state.backend.bind_descriptor_set(
				ctx,
				binding.pipeline,
				set,
				binding.descriptor_set_indices[i],
			)
		}
	}
	if binding.push_constant_size > 0 {
		if binding.push_constant_size > IMPORTED_PUSH_CONSTANT_CAP {
			planned_command_error(
				diagnostics,
				.Invalid_Command,
				handle,
				"imported draw push constants exceed executor capacity",
			)
			return false
		}
		state.backend.push_constants(
			ctx,
			binding.pipeline,
			binding.push_constant_stages,
			0,
			binding.push_constant_size,
			&binding.push_constant_bytes[0],
		)
	}
	state.backend.bind_vertex_buffer(ctx, binding.vertex_buffer)
	if binding.indexed {
		if binding.index_buffer == bk.NULL_BUFFER || binding.index_count == 0 {
			planned_command_error(
				diagnostics,
				.Invalid_Resource,
				handle,
				"imported indexed draw missing index buffer or count",
			)
			return false
		}
		state.backend.bind_index_buffer(ctx, binding.index_buffer)
		state.backend.draw_indexed(
			ctx,
			binding.index_count,
			binding.instance_count,
			binding.first_index,
			i32(binding.first_vertex),
			binding.first_instance,
		)
		return true
	}
	if binding.vertex_count == 0 {
		planned_command_error(
			diagnostics,
			.Invalid_Command,
			handle,
			"imported draw missing vertex count",
		)
		return false
	}
	state.backend.draw(
		ctx,
		binding.vertex_count,
		binding.instance_count,
		binding.first_vertex,
		binding.first_instance,
	)
	return true
}

materialize_frame_targets :: proc(frame: ^ir.Frame_IR) -> bool {
	if frame == nil {
		return false
	}

	for target_index in 0 ..< len(frame.targets) {
		target := &frame.targets[target_index]
		if target.pass != ir.INVALID_PASS {
			continue
		}
		if target.kind == .Present {
			continue
		}
		if target.kind != .Offscreen {
			return false
		}
		if target.width == 0 || target.height == 0 {
			return false
		}

		pass := ir.Pass_Desc {
			kind = .Render,
			name = target.name,
		}
		if target.color_format != .Undefined {
			if target.color_resource == ir.INVALID_RESOURCE {
				target.color_resource = ir.add_texture(
					frame,
					"target-color",
					target.width,
					target.height,
					target.color_format,
					{.Color_Attachment, .Sampled},
					.Transient,
				)
			}
			if !ir.pass_add_color_target(
				&pass,
				ir.make_color_target(
					target.color_resource,
					target.color_format,
					.Clear,
					.Store,
					target.clear_color,
				),
			) {
				return false
			}
		}

		if target.depth_format != .Undefined {
			if target.depth_resource == ir.INVALID_RESOURCE {
				target.depth_resource = ir.add_texture(
					frame,
					"target-depth",
					target.width,
					target.height,
					target.depth_format,
					{.Depth_Stencil_Attachment},
					.Transient,
				)
			}
			ir.pass_set_depth_target(
				&pass,
				ir.make_depth_target(
					target.depth_resource,
					target.depth_format,
					.Clear,
					.Store,
					target.clear_depth,
				),
			)
		}

		if pass.color_target_count == 0 && !pass.has_depth_target {
			return false
		}
		ir.pass_set_viewport(
			&pass,
			{
				x = 0,
				y = 0,
				width = f32(target.width),
				height = f32(target.height),
				min_depth = 0,
				max_depth = 1,
			},
		)
		ir.pass_set_scissor(&pass, {x = 0, y = 0, width = target.width, height = target.height})
		target.pass = ir.add_pass(frame, pass)
	}

	return true
}

build_render_pass_desc :: proc(
	frame: ^ir.Frame_IR,
	pass: ^ir.Pass_Desc,
) -> (
	bk.Render_Pass_Desc,
	bool,
) {
	return build_render_pass_desc_with_capabilities(
		frame,
		pass,
		bk.implemented_base_capabilities(1, 0),
	)
}

build_render_pass_desc_with_capabilities :: proc(
	frame: ^ir.Frame_IR,
	pass: ^ir.Pass_Desc,
	caps: bk.Capabilities,
) -> (
	bk.Render_Pass_Desc,
	bool,
) {
	if pass == nil || !bk.supports_color_target_count(caps, u32(pass.color_target_count)) {
		return {}, false
	}

	desc: bk.Render_Pass_Desc
	if pass.color_target_count > 0 {
		desc.has_color = true
		desc.color_count = u32(pass.color_target_count)
		for i in 0..<pass.color_target_count {
			target := pass.color_targets[i]
			resource, ok := ir.get_resource(frame, target.resource)
			if !ok || resource.kind != .Texture {
				return {}, false
			}
			desc.color_formats[i] = format_to_backend(target.format)
			desc.color_load_ops[i] = load_op_to_backend(target.load_op)
			desc.color_store_ops[i] = store_op_to_backend(target.store_op)
			desc.color_final_layouts[i] =
				.Shader_Read_Only if .Sampled in resource.texture.usage else .Color_Attachment
		}
		desc.color_format = desc.color_formats[0]
		desc.color_load_op = desc.color_load_ops[0]
		desc.color_store_op = desc.color_store_ops[0]
		desc.color_final_layout = desc.color_final_layouts[0]
	}

	if pass.has_depth_target {
		target := pass.depth_target
		resource, ok := ir.get_resource(frame, target.resource)
		if !ok || resource.kind != .Texture {
			return {}, false
		}
		desc.has_depth = true
		desc.depth_format = format_to_backend(target.format)
		desc.has_stencil = backend_format_has_stencil(desc.depth_format)
		desc.depth_load_op = load_op_to_backend(target.load_op)
		desc.depth_store_op = store_op_to_backend(target.store_op)
		desc.stencil_load_op = desc.depth_load_op if desc.has_stencil else .Dont_Care
		desc.stencil_store_op = desc.depth_store_op if desc.has_stencil else .Dont_Care
		desc.depth_final_layout =
			.Depth_Stencil_Read_Only if .Sampled in resource.texture.usage else .Depth_Stencil_Attachment
		desc.depth_only = !desc.has_color
	}

	return desc, desc.has_color || desc.has_depth
}

@(private)
backend_format_has_stencil :: proc(format: bk.Format) -> bool {
	switch format {
	case .D32_SFLOAT_S8_UINT, .D24_UNORM_S8_UINT:
		return true
	case .Undefined, .R8G8B8A8_SRGB, .R8G8B8A8_UNORM, .B8G8R8A8_SRGB, .D32_SFLOAT:
		return false
	}
	return false
}

build_begin_desc :: proc(
	pass: ^ir.Pass_Desc,
	render_pass: bk.Render_Pass_Handle,
	framebuffer: bk.Framebuffer_Handle,
) -> bk.Render_Pass_Begin_Desc {
	desc := bk.Render_Pass_Begin_Desc {
		pass        = render_pass,
		framebuffer = framebuffer,
		clear_depth = 1,
	}
	if pass.color_target_count > 0 {
		desc.color_count = u32(pass.color_target_count)
		for i in 0..<pass.color_target_count {
			desc.clear_colors[i] = pass.color_targets[i].clear_color
		}
		desc.clear_color = desc.clear_colors[0]
	}
	if pass.has_depth_target {
		desc.clear_depth = pass.depth_target.clear_depth
	}
	if pass.has_viewport {
		desc.width = u32(pass.viewport.width)
		desc.height = u32(pass.viewport.height)
	} else if pass.has_scissor {
		desc.width = pass.scissor.width
		desc.height = pass.scissor.height
	}
	return desc
}

build_framebuffer_desc :: proc(
	state: ^Execution_State,
	frame: ^ir.Frame_IR,
	pass: ^ir.Pass_Desc,
	render_pass: bk.Render_Pass_Handle,
) -> (
	bk.Framebuffer_Desc,
	bool,
) {
	desc := bk.Framebuffer_Desc {
		pass   = render_pass,
		layers = 1,
	}

	if pass.color_target_count > 0 {
		desc.color_count = u32(pass.color_target_count)
		for i in 0..<pass.color_target_count {
			target := pass.color_targets[i]
			texture, ok := prepared_texture(state, target.resource)
			if !ok {
				return {}, false
			}
			resource, _ := ir.get_resource(frame, target.resource)
			desc.color_views[i] = texture
			if i == 0 {
				desc.color_view = texture
				desc.width = resource.texture.width
				desc.height = resource.texture.height
			} else if desc.width != resource.texture.width || desc.height != resource.texture.height {
				log.error("gpu/compiler: render pass attachments must have matching extents")
				return {}, false
			}
		}
	}

	if pass.has_depth_target {
		target := pass.depth_target
		texture, ok := prepared_texture(state, target.resource)
		if !ok {
			return {}, false
		}
		resource, _ := ir.get_resource(frame, target.resource)
		desc.depth_view = texture
		if desc.width == 0 {
			desc.width = resource.texture.width
			desc.height = resource.texture.height
		} else if desc.width != resource.texture.width || desc.height != resource.texture.height {
			log.error("gpu/compiler: render pass attachments must have matching extents")
			return {}, false
		}
	}

	return desc, desc.width != 0 && desc.height != 0
}

format_to_backend :: proc(format: ir.Format) -> bk.Format {
	switch format {
	case .Undefined:
		return .Undefined
	case .R8G8B8A8_SRGB:
		return .R8G8B8A8_SRGB
	case .R8G8B8A8_UNORM:
		return .R8G8B8A8_UNORM
	case .B8G8R8A8_SRGB:
		return .B8G8R8A8_SRGB
	case .D32_SFLOAT:
		return .D32_SFLOAT
	case .D32_SFLOAT_S8_UINT:
		return .D32_SFLOAT_S8_UINT
	case .D24_UNORM_S8_UINT:
		return .D24_UNORM_S8_UINT
	}
	return .Undefined
}

texture_usage_to_backend :: proc(usage: ir.Texture_Usage_Flags) -> bk.Image_Usage_Flags {
	result: bk.Image_Usage_Flags
	if .Sampled in usage {result += {.Sampled}}
	if .Color_Attachment in usage {result += {.Color_Attachment}}
	if .Depth_Stencil_Attachment in usage {result += {.Depth_Stencil_Attachment}}
	if .Transfer_Dst in usage {result += {.Transfer_Dst}}
	if .Transfer_Src in usage {result += {.Transfer_Src}}
	return result
}

buffer_usage_to_backend :: proc(usage: ir.Buffer_Usage_Flags) -> bk.Buffer_Usage_Flags {
	result: bk.Buffer_Usage_Flags
	if .Vertex in usage {result += {.Vertex}}
	if .Index in usage {result += {.Index}}
	if .Uniform in usage {result += {.Uniform}}
	if .Storage in usage {result += {.Storage}}
	if .Indirect_Argument in usage {result += {.Indirect_Argument}}
	if .Transfer_Src in usage {result += {.Transfer_Src}}
	if .Transfer_Dst in usage {result += {.Transfer_Dst}}
	return result
}

memory_properties_to_backend :: proc(
	memory: ir.Memory_Property_Flags,
) -> bk.Memory_Property_Flags {
	result: bk.Memory_Property_Flags
	if .Device_Local in memory {result += {.Device_Local}}
	if .Host_Visible in memory {result += {.Host_Visible}}
	if .Host_Coherent in memory {result += {.Host_Coherent}}
	return result
}

descriptor_type_to_backend :: proc(typ: ir.Descriptor_Type) -> bk.Descriptor_Type {
	switch typ {
	case .Combined_Image_Sampler:
		return .Combined_Image_Sampler
	case .Uniform_Buffer:
		return .Uniform_Buffer
	case .Storage_Buffer:
		return .Storage_Buffer
	}
	return .Uniform_Buffer
}

shader_stages_to_backend :: proc(stages: ir.Shader_Stage_Flags) -> bk.Shader_Stage_Flags {
	result: bk.Shader_Stage_Flags
	if .Vertex in stages {result += {.Vertex}}
	if .Fragment in stages {result += {.Fragment}}
	if .Compute in stages {result += {.Compute}}
	return result
}

layout_variant_to_backend :: proc(variant: ir.Layout_Variant) -> bk.Layout_Variant {
	switch variant {
	case .PNU:
		return .PNU
	case .PN:
		return .PN
	case .PNUC:
		return .PNUC
	case .PNUT:
		return .PNUT
	case .PNUCT:
		return .PNUCT
	}
	return .PNU
}

topology_to_backend :: proc(topology: ir.Topology) -> bk.Topology {
	switch topology {
	case .Triangle_List:
		return .Triangle_List
	case .Triangle_Strip:
		return .Triangle_Strip
	case .Line_List:
		return .Line_List
	case .Point_List:
		return .Point_List
	}
	return .Triangle_List
}

cull_mode_to_backend :: proc(mode: ir.Cull_Mode) -> bk.Cull_Mode {
	switch mode {
	case .None:
		return .None
	case .Front:
		return .Front
	case .Back:
		return .Back
	case .Front_And_Back:
		return .Front_And_Back
	}
	return .None
}

front_face_to_backend :: proc(face: ir.Front_Face) -> bk.Front_Face {
	switch face {
	case .Counter_Clockwise:
		return .Counter_Clockwise
	case .Clockwise:
		return .Clockwise
	}
	return .Counter_Clockwise
}

blend_mode_to_backend :: proc(mode: ir.Blend_Mode) -> bk.Blend_Mode {
	switch mode {
	case .Alpha:
		return .Alpha
	case .Additive:
		return .Additive
	case .Premultiplied_Alpha:
		return .Premultiplied_Alpha
	}
	return .Alpha
}

load_op_to_backend :: proc(op: ir.Attachment_Load_Op) -> bk.Attachment_Load_Op {
	switch op {
	case .Load:
		return .Load
	case .Clear:
		return .Clear
	case .Dont_Care:
		return .Dont_Care
	}
	return .Load
}

store_op_to_backend :: proc(op: ir.Attachment_Store_Op) -> bk.Attachment_Store_Op {
	switch op {
	case .Store:
		return .Store
	case .Dont_Care:
		return .Dont_Care
	}
	return .Store
}

sampler_desc_to_backend :: proc(desc: ir.Sampler_Desc) -> bk.Sampler_Desc {
	return {
		mag_filter = filter_to_backend(desc.mag_filter),
		min_filter = filter_to_backend(desc.min_filter),
		address_mode_u = address_mode_to_backend(desc.address_mode_u),
		address_mode_v = address_mode_to_backend(desc.address_mode_v),
		enable_aniso = desc.enable_aniso,
		enable_compare = desc.enable_compare,
		compare_op = compare_op_to_backend(desc.compare_op),
		mipmap_mode = filter_to_backend(desc.mipmap_mode),
	}
}

texture_desc_to_backend :: proc(desc: ir.Texture_Desc) -> bk.Texture_Desc {
	return {
		width = desc.width,
		height = desc.height,
		format = format_to_backend(desc.format),
		usage = texture_usage_to_backend(desc.usage),
	}
}

@(private)
filter_to_backend :: proc(filter: ir.Filter) -> bk.Filter {
	switch filter {
	case .Nearest:
		return .Nearest
	case .Linear:
		return .Linear
	}
	return .Linear
}

@(private)
address_mode_to_backend :: proc(mode: ir.Address_Mode) -> bk.Address_Mode {
	switch mode {
	case .Repeat:
		return .Repeat
	case .Clamp_To_Edge:
		return .Clamp_To_Edge
	case .Clamp_To_Border:
		return .Clamp_To_Border
	}
	return .Repeat
}

@(private)
compare_op_to_backend :: proc(op: ir.Compare_Op) -> bk.Compare_Op {
	switch op {
	case .Never:
		return .Never
	case .Less:
		return .Less
	case .Less_Or_Equal:
		return .Less_Or_Equal
	case .Always:
		return .Always
	}
	return .Always
}

@(private)
is_render_target_texture :: proc(resource: ^ir.Resource_Desc) -> bool {
	usage := resource.texture.usage
	return .Color_Attachment in usage || .Depth_Stencil_Attachment in usage
}

@(private)
texture_aspect_to_backend :: proc(usage: ir.Texture_Usage_Flags) -> bk.Image_Aspect_Flags {
	if .Depth_Stencil_Attachment in usage {
		return {.Depth}
	}
	return {.Color}
}

@(private)
pass_has_targets :: proc(pass: ^ir.Pass_Desc) -> bool {
	return pass.color_target_count > 0 || pass.has_depth_target
}

@(private)
validate_pass_color_target_count :: proc(
	pass: ^ir.Pass_Desc,
	caps: bk.Capabilities,
	label: string,
) -> bool {
	if pass == nil {
		return false
	}
	if bk.supports_color_target_count(caps, u32(pass.color_target_count)) {
		return true
	}
	log.errorf(
		"gpu/compiler: %s requires %d color targets; backend implemented limit is %d",
		label,
		u32(pass.color_target_count),
		caps.max_color_targets,
	)
	return false
}

@(private)
find_prepared_resource :: proc(
	state: ^Execution_State,
	resource: ir.Resource_Handle,
) -> (
	^Prepared_Resource,
	bool,
) {
	for &prepared in state.resources {
		if prepared.resource == resource {
			return &prepared, true
		}
	}
	return nil, false
}

@(private)
find_prepared_pass :: proc(
	state: ^Execution_State,
	pass: ir.Pass_Handle,
) -> (
	^Prepared_Pass,
	bool,
) {
	for &prepared in state.passes {
		if prepared.pass == pass {
			return &prepared, true
		}
	}
	return nil, false
}