Harbor

branch main
showing the latest snapshot on main
frame_api_test.odin 46.4 KB · Plain text
gpu/tests/frame_api_test.odin 0644 Raw
package gpu_tests

import gpu ".."
import bk "../backend"
import particles "../particles"
import ir "../render_ir"
import renderer "../renderer"
import "core:testing"

test_camera_3d :: proc() -> gpu.Camera3D {
	return {
		position = {0, 0, 5},
		target = {0, 0, 0},
		up = {0, 1, 0},
		fovy = 60,
		near = 0.1,
		far = 100,
	}
}

@(test)
test_frame_api_builds_target_layer_and_packets :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)
	testing.expect(t, !gpu.context_is_runtime_backed(ctx))
	frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&frame)
	testing.expect(t, !gpu.frame_is_runtime_backed(&frame))

	target := gpu.begin_target(&frame, gpu.present_target(ctx))
	testing.expect(t, target != gpu.INVALID_TARGET)

	layer := gpu.push_layer(&frame, {name = "ui", sort_base = 10_000, order = .Strict})
	testing.expect(t, layer != gpu.INVALID_LAYER)

	rect := gpu.draw_rect(
		&frame,
		{x = 10, y = 20, width = 30, height = 40, color = gpu.WHITE, sort_key = 7},
	)
	text := gpu.draw_text_run(
		&frame,
		{
			glyphs = []gpu.Glyph_Quad {
				{
					x = 10,
					y = 20,
					width = 6,
					height = 8,
					u0 = 0,
					v0 = 0,
					u1 = 1,
					v1 = 1,
					color = gpu.WHITE,
				},
				{
					x = 16,
					y = 20,
					width = 6,
					height = 8,
					u0 = 0,
					v0 = 0,
					u1 = 1,
					v1 = 1,
					color = gpu.WHITE,
				},
			},
			sort_key = 8,
		},
	)
	testing.expect(t, rect != gpu.INVALID_COMMAND)
	testing.expect(t, text != gpu.INVALID_COMMAND)

	fir := gpu.frame_ir(&frame)
	testing.expect(t, fir != nil)
	testing.expect_value(t, len(fir.targets), 1)
	testing.expect_value(t, len(fir.layers), 1)
	testing.expect_value(t, len(fir.resources), 2)
	testing.expect_value(t, len(fir.draw_commands), 2)
	testing.expect(t, fir.draw_commands[0].kind == .Quad_Stream)
	testing.expect(t, fir.draw_commands[0].packet.target == target)
	testing.expect(t, fir.draw_commands[0].packet.layer == layer)
	testing.expect(t, fir.draw_commands[0].packet.geometry.resource != gpu.INVALID_RESOURCE)
	testing.expect(t, fir.resources[0].kind == .Buffer)
	testing.expect(t, fir.resources[0].lifetime == .Transient)
	testing.expect(t, .Vertex in fir.resources[0].buffer.usage)
	testing.expect_value(t, fir.draw_commands[0].packet.sequence, u64(1))
	testing.expect(t, fir.draw_commands[1].kind == .Glyph_Quad_Stream)
	testing.expect(t, fir.draw_commands[1].packet.geometry.resource != gpu.INVALID_RESOURCE)
	testing.expect_value(t, fir.draw_commands[1].packet.geometry.vertex_count, u32(12))
	testing.expect(t, fir.resources[1].kind == .Buffer)
	testing.expect(t, fir.resources[1].lifetime == .Transient)
	testing.expect_value(t, fir.draw_commands[1].packet.sequence, u64(2))
	testing.expect(t, gpu.submit(&frame))
}

@(test)
test_frame_api_draw_line_emits_generic_line_stream :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)
	frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&frame)

	_ = gpu.begin_target(&frame, gpu.present_target(ctx))
	line := gpu.draw_line_frame(
		&frame,
		{x0 = 10, y0 = 20, x1 = 100, y1 = 120, thickness = 3, color = gpu.YELLOW, sort_key = 5},
	)

	fir := gpu.frame_ir(&frame)
	testing.expect(t, line != gpu.INVALID_COMMAND)
	testing.expect_value(t, len(fir.resources), 1)
	testing.expect_value(t, len(fir.draw_commands), 1)
	testing.expect(t, fir.draw_commands[0].kind == .Line_Stream)
	testing.expect(t, fir.draw_commands[0].packet.geometry.resource != gpu.INVALID_RESOURCE)
	testing.expect_value(t, fir.draw_commands[0].packet.geometry.vertex_count, u32(6))
	testing.expect_value(t, fir.draw_commands[0].packet.sequence, u64(1))
	testing.expect(t, fir.resources[0].kind == .Buffer)
	testing.expect(t, fir.resources[0].lifetime == .Transient)
	testing.expect(t, .Vertex in fir.resources[0].buffer.usage)
}

@(test)
test_frame_api_draw_circle_emits_generic_vertex_stream :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)
	frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&frame)

	_ = gpu.begin_target(&frame, gpu.present_target(ctx))
	circle := gpu.draw_circle_frame(
		&frame,
		{cx = 100, cy = 120, radius = 40, color = gpu.YELLOW, sort_key = 5},
	)

	fir := gpu.frame_ir(&frame)
	testing.expect(t, circle != gpu.INVALID_COMMAND)
	testing.expect_value(t, len(fir.resources), 1)
	testing.expect_value(t, len(fir.draw_commands), 1)
	testing.expect(t, fir.draw_commands[0].kind == .Vertex_Stream)
	testing.expect(t, fir.draw_commands[0].packet.geometry.kind == .Vertex_Stream)
	testing.expect(t, fir.draw_commands[0].packet.geometry.resource != gpu.INVALID_RESOURCE)
	testing.expect_value(
		t,
		fir.draw_commands[0].packet.geometry.vertex_count,
		u32(1 + renderer.CIRCLE_SEGMENTS),
	)
	testing.expect_value(t, fir.draw_commands[0].packet.sequence, u64(1))
	testing.expect(t, fir.resources[0].kind == .Buffer)
	testing.expect(t, fir.resources[0].lifetime == .Transient)
	testing.expect(t, .Vertex in fir.resources[0].buffer.usage)
}

@(test)
test_frame_api_draw_circle_rejects_invalid_radius_without_resource :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)
	frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&frame)

	circle := gpu.draw_circle_frame(&frame, {cx = 100, cy = 120, radius = 0, color = gpu.YELLOW})

	fir := gpu.frame_ir(&frame)
	testing.expect(t, circle == gpu.INVALID_COMMAND)
	testing.expect_value(t, len(fir.resources), 0)
	testing.expect_value(t, len(fir.draw_commands), 0)
}

@(test)
test_frame_api_draw_billboard_emits_generic_point_stream :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)
	frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&frame)

	_ = gpu.begin_target(&frame, gpu.present_target(ctx))
	billboard := gpu.draw_billboard_frame(
		&frame,
		{position = {1, 2, 3}, size = 2, color = gpu.YELLOW, sort_key = 5},
	)

	fir := gpu.frame_ir(&frame)
	testing.expect(t, billboard != gpu.INVALID_COMMAND)
	testing.expect_value(t, len(fir.resources), 1)
	testing.expect_value(t, len(fir.draw_commands), 1)
	testing.expect(t, fir.draw_commands[0].kind == .Point_Stream)
	testing.expect(t, fir.draw_commands[0].packet.geometry.kind == .Point_Stream)
	testing.expect_value(t, fir.draw_commands[0].packet.geometry.vertex_count, u32(1))
}

@(test)
test_frame_api_billboard_requires_camera_for_runtime :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)
	frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&frame)

	_ = gpu.begin_target(&frame, gpu.present_target(ctx))
	_ = gpu.draw_billboard_frame(&frame, {position = {0, 1, 0}, size = 1, color = gpu.WHITE})

	_, ok := gpu.frame_runtime_mode(&frame)
	testing.expect(t, !ok)
}

@(test)
test_frame_runtime_mode_accepts_billboard_only_with_camera :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)
	frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&frame)

	_ = gpu.begin_target(&frame, gpu.present_target(ctx))
	gpu.set_camera_3d(&frame, test_camera_3d())
	_ = gpu.draw_billboard_frame(&frame, {position = {0, 1, 0}, size = 1, color = gpu.WHITE})

	mode, ok := gpu.frame_runtime_mode(&frame)
	testing.expect(t, ok)
	testing.expect(t, mode == .Billboards)
}

@(test)
test_frame_api_draw_quad_3d_emits_generic_vertex_stream :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)
	frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&frame)

	_ = gpu.begin_target(&frame, gpu.present_target(ctx))
	quad := gpu.draw_quad_3d_frame(
		&frame,
		{
			corners = {{-1, 0, -1}, {1, 0, -1}, {1, 0, 1}, {-1, 0, 1}},
			normal = {0, 1, 0},
			color = {0, 0, 0, 0.35},
			sort_key = 5,
		},
	)

	fir := gpu.frame_ir(&frame)
	testing.expect(t, quad != gpu.INVALID_COMMAND)
	testing.expect_value(t, len(fir.resources), 1)
	testing.expect_value(t, len(fir.draw_commands), 1)
	testing.expect(t, fir.draw_commands[0].kind == .Vertex_Stream)
	testing.expect(t, fir.draw_commands[0].packet.geometry.kind == .Vertex_Stream)
	testing.expect_value(t, fir.draw_commands[0].packet.geometry.vertex_count, u32(4))
	testing.expect_value(t, fir.draw_commands[0].packet.geometry.index_count, u32(6))
}

@(test)
test_frame_api_quad_3d_requires_camera_for_runtime :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)
	frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&frame)

	_ = gpu.begin_target(&frame, gpu.present_target(ctx))
	_ = gpu.draw_quad_3d_frame(
		&frame,
		{corners = {{-1, 0, -1}, {1, 0, -1}, {1, 0, 1}, {-1, 0, 1}}, color = {0, 0, 0, 0.35}},
	)

	_, ok := gpu.frame_runtime_mode(&frame)
	testing.expect(t, !ok)
}

@(test)
test_frame_runtime_mode_accepts_quad_3d_with_camera :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)
	frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&frame)

	_ = gpu.begin_target(&frame, gpu.present_target(ctx))
	gpu.set_camera_3d(&frame, test_camera_3d())
	_ = gpu.draw_quad_3d_frame(
		&frame,
		{corners = {{-1, 0, -1}, {1, 0, -1}, {1, 0, 1}, {-1, 0, 1}}, color = {0, 0, 0, 0.35}},
	)

	mode, ok := gpu.frame_runtime_mode(&frame)
	testing.expect(t, ok)
	testing.expect(t, mode == .Billboards)
}

@(test)
test_particles_3d_producer_draws_frame_billboards :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)
	frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&frame)

	config := particles.Config_3D {
		max_particles = 4,
		lifetime      = {1, 1},
		speed         = {0, 0},
		direction     = {0, 1, 0},
		spread        = 0,
		start_color   = {1, 0, 0, 1},
		end_color     = {1, 0, 0, 1},
		start_size    = 2,
		end_size      = 2,
		gravity       = {0, 0, 0},
	}
	system, ok := particles.init_3d(config)
	testing.expect(t, ok)
	defer particles.destroy_3d(&system)
	particles.emit_3d(&system, {10, 20, 30}, 1)
	particles.draw_3d(&frame, &system)

	fir := gpu.frame_ir(&frame)
	testing.expect_value(t, len(fir.draw_commands), 1)
	testing.expect(t, fir.draw_commands[0].kind == .Point_Stream)
}

@(test)
test_frame_api_draw_texture_quad_emits_generic_quad_stream :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)
	frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&frame)

	_ = gpu.begin_target(&frame, gpu.present_target(ctx))
	texture := gpu.Texture {
		id     = 11,
		width  = 64,
		height = 32,
	}
	cmd := gpu.draw_texture_quad(
		&frame,
		{
			texture = texture,
			x = 12,
			y = 24,
			u0 = 0.25,
			v0 = 0.75,
			u1 = 0.5,
			v1 = 0.125,
			rotation = 15,
			color = gpu.WHITE,
			sort_key = 3,
		},
	)

	fir := gpu.frame_ir(&frame)
	testing.expect(t, cmd != gpu.INVALID_COMMAND)
	testing.expect_value(t, len(fir.resources), 1)
	testing.expect_value(t, len(fir.draw_commands), 1)
	testing.expect(t, fir.draw_commands[0].kind == .Quad_Stream)
	testing.expect(t, fir.draw_commands[0].packet.geometry.kind == .Quad_Stream)
	testing.expect(t, fir.draw_commands[0].packet.geometry.resource != gpu.INVALID_RESOURCE)
	testing.expect_value(t, fir.draw_commands[0].packet.geometry.vertex_count, u32(6))

	quad, quad_ok := gpu.find_generated_textured_quad_geometry(
		&frame,
		fir.draw_commands[0].packet.geometry.resource,
	)
	testing.expect(t, quad_ok)
	testing.expect_value(t, quad.texture_id, u32(11))
	testing.expect_value(t, quad.width, f32(64))
	testing.expect_value(t, quad.height, f32(32))
	testing.expect_value(t, quad.u0, f32(0.25))
	testing.expect_value(t, quad.v0, f32(0.75))
	testing.expect_value(t, quad.u1, f32(0.5))
	testing.expect_value(t, quad.v1, f32(0.125))
	testing.expect_value(t, quad.rotation, f32(15))
	testing.expect(t, quad.color == gpu.WHITE)
}

@(test)
test_frame_api_draw_texture_quad_rejects_invalid_texture_without_resource :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)
	frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&frame)

	cmd := gpu.draw_texture_quad(
		&frame,
		{texture = {}, x = 12, y = 24, width = 64, height = 32, color = gpu.WHITE},
	)

	fir := gpu.frame_ir(&frame)
	testing.expect(t, cmd == gpu.INVALID_COMMAND)
	testing.expect_value(t, len(fir.resources), 0)
	testing.expect_value(t, len(fir.draw_commands), 0)
}

@(test)
test_frame_api_texture_material_rejects_invalid_texture_without_material :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)
	frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&frame)

	material := gpu.add_texture_material(&frame, {}, {name = "invalid-atlas"})

	fir := gpu.frame_ir(&frame)
	testing.expect(t, material == gpu.INVALID_MATERIAL)
	testing.expect_value(t, len(fir.materials), 0)
}

@(test)
test_frame_runtime_mode_accepts_glyphs_with_texture_material :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)
	frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&frame)

	_ = gpu.begin_target(&frame, gpu.present_target(ctx))
	material := gpu.add_texture_material(
		&frame,
		{id = 22, width = 128, height = 128},
		{name = "glyph-atlas", blend_mode = .Alpha},
	)
	text := gpu.draw_text_run(
		&frame,
		{
			glyphs = []gpu.Glyph_Quad {
				{
					x = 10,
					y = 20,
					width = 8,
					height = 12,
					u0 = 0.125,
					v0 = 0.25,
					u1 = 0.1875,
					v1 = 0.34375,
					color = gpu.WHITE,
				},
				{
					x = 18,
					y = 20,
					width = 8,
					height = 12,
					u0 = 0.1875,
					v0 = 0.25,
					u1 = 0.25,
					v1 = 0.34375,
					color = gpu.YELLOW,
				},
			},
			material = material,
		},
	)

	fir := gpu.frame_ir(&frame)
	testing.expect(t, material != gpu.INVALID_MATERIAL)
	testing.expect(t, text != gpu.INVALID_COMMAND)
	testing.expect_value(t, len(fir.materials), 1)
	testing.expect_value(t, fir.materials[0].texture_id, u32(22))
	testing.expect(t, fir.draw_commands[0].packet.material == material)
	testing.expect_value(t, fir.draw_commands[0].packet.geometry.vertex_count, u32(12))

	mode, ok := gpu.frame_runtime_mode(&frame)
	testing.expect(t, ok)
	testing.expect(t, mode == .Generated_2D)
}

@(test)
test_frame_runtime_mode_accepts_generated_textured_quads_with_shapes :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)
	frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&frame)

	_ = gpu.begin_target(&frame, gpu.present_target(ctx))
	_ = gpu.draw_texture_quad(
		&frame,
		{texture = {id = 11, width = 64, height = 32}, x = 10, y = 20, color = gpu.WHITE},
	)
	_ = gpu.draw_rect(&frame, {x = 100, y = 20, width = 30, height = 40, color = gpu.RED})
	_ = gpu.draw_circle_frame(&frame, {cx = 200, cy = 120, radius = 30, color = gpu.YELLOW})

	mode, ok := gpu.frame_runtime_mode(&frame)
	testing.expect(t, ok)
	testing.expect(t, mode == .Generated_2D)
}

@(test)
test_frame_runtime_mode_accepts_generated_2d_rects_and_lines :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)
	frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&frame)

	_ = gpu.begin_target(&frame, gpu.present_target(ctx))
	_ = gpu.draw_rect(&frame, {x = 10, y = 20, width = 30, height = 40, color = gpu.WHITE})
	_ = gpu.draw_line_frame(&frame, {x0 = 0, y0 = 0, x1 = 100, y1 = 100, color = gpu.RED})
	_ = gpu.draw_circle_frame(&frame, {cx = 200, cy = 120, radius = 30, color = gpu.YELLOW})

	mode, ok := gpu.frame_runtime_mode(&frame)
	testing.expect(t, ok)
	testing.expect(t, mode == .Generated_2D)
}

@(test)
test_frame_planned_coverage_marks_generated_shape_slice_planned :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)
	frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&frame)

	_ = gpu.draw_rect(&frame, {x = 10, y = 20, width = 30, height = 40, color = gpu.WHITE})
	_ = gpu.draw_line_frame(&frame, {x0 = 0, y0 = 0, x1 = 100, y1 = 100, color = gpu.RED})
	_ = gpu.draw_circle_frame(&frame, {cx = 200, cy = 120, radius = 30, color = gpu.YELLOW})

	testing.expect(t, gpu.frame_planned_coverage_path(&frame) == .Planned)
}

@(test)
test_frame_planned_coverage_marks_generated_texture_and_glyph_slice_planned :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)

	textured_frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&textured_frame)
	_ = gpu.draw_texture_quad(
		&textured_frame,
		{texture = {id = 11, width = 64, height = 32}, x = 10, y = 20, color = gpu.WHITE},
	)
	testing.expect(t, gpu.frame_planned_coverage_path(&textured_frame) == .Planned)

	glyph_frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&glyph_frame)
	material := gpu.add_texture_material(
		&glyph_frame,
		{id = 12, width = 64, height = 32},
	)
	_ = gpu.draw_text_run(
		&glyph_frame,
		{
			material = material,
			glyphs = []gpu.Glyph_Quad {
				{x = 10, y = 20, width = 6, height = 8, u0 = 0, v0 = 0, u1 = 1, v1 = 1, color = gpu.WHITE},
			},
		},
	)
	testing.expect(t, gpu.frame_planned_coverage_path(&glyph_frame) == .Planned)
}

@(test)
test_frame_planned_coverage_keeps_unsupported_generated_forms_on_migration :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)

	offscreen_frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&offscreen_frame)
	target := gpu.begin_target(
		&offscreen_frame,
		gpu.offscreen_target(ctx, {name = "offscreen", width = 64, height = 64}),
	)
	testing.expect(t, target != gpu.INVALID_TARGET)
	_ = gpu.draw_rect(&offscreen_frame, {x = 1, y = 2, width = 3, height = 4, color = gpu.WHITE})
	testing.expect(t, gpu.frame_planned_coverage_path(&offscreen_frame) == .Migration)
}

@(test)
test_frame_planned_coverage_marks_indirect_draw_planned :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)
	frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&frame)

	geometry := ir.add_buffer(&frame.ir, "geometry", 256, {.Vertex, .Index}, {.Device_Local}, .Persistent)
	args := ir.add_buffer(
		&frame.ir,
		"args",
		u64(size_of(bk.Indirect_Draw_Indexed_Args)),
		{.Indirect_Argument},
		{.Device_Local},
		.Persistent,
	)
	pass := ir.add_pass(&frame.ir, {kind = .Render, name = "main"})
	vertex_shader := ir.add_shader(&frame.ir, {stage = .Vertex, name = "vs"})
	fragment_shader := ir.add_shader(&frame.ir, {stage = .Fragment, name = "fs"})
	pipeline_state := ir.default_graphics_pipeline_state()
	pipeline_state.vertex_shader = vertex_shader
	pipeline_state.fragment_shader = fragment_shader
	pipeline := ir.add_graphics_pipeline(&frame.ir, "pipeline", pipeline_state)

	_ = ir.add_draw_packet(
		&frame.ir,
		pass,
		pipeline,
		.Indirect,
		{
			geometry = {kind = .Indexed_Mesh, resource = geometry},
			instances = {
				indirect = args,
				count = 1,
				stride = u32(size_of(bk.Indirect_Draw_Indexed_Args)),
			},
			order = .Strict,
		},
	)

	testing.expect(t, gpu.frame_planned_coverage_path(&frame) == .Planned)
}

@(test)
test_frame_runtime_mode_accepts_generated_2d_without_explicit_target :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)
	frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&frame)

	_ = gpu.draw_rect(&frame, {x = 10, y = 20, width = 30, height = 40, color = gpu.WHITE})

	mode, ok := gpu.frame_runtime_mode(&frame)
	testing.expect(t, ok)
	testing.expect(t, mode == .Generated_2D)
}

@(test)
test_frame_runtime_mode_accepts_offscreen_generated_2d :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)
	frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&frame)

	offscreen := gpu.begin_target(
		&frame,
		{
			kind = .Offscreen,
			name = "offscreen",
			width = 128,
			height = 128,
			color_format = .R8G8B8A8_UNORM,
			depth_format = .D32_SFLOAT,
			clear_color = gpu.BLACK,
			clear_depth = 1,
		},
	)
	_ = gpu.push_layer(&frame, {name = "offscreen-layer"})
	rect := gpu.draw_rect(&frame, {x = 10, y = 20, width = 30, height = 40, color = gpu.WHITE})

	testing.expect(t, offscreen != gpu.INVALID_TARGET)
	testing.expect(t, rect != gpu.INVALID_COMMAND)
	mode, ok := gpu.frame_runtime_mode(&frame)
	testing.expect(t, ok)
	testing.expect(t, mode == .Generated_2D)
	testing.expect(t, gpu.submit(&frame))
}

@(test)
test_frame_runtime_mode_rejects_offscreen_meshes :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)
	frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&frame)

	offscreen := gpu.begin_target(
		&frame,
		gpu.offscreen_target(
			ctx,
			{
				name = "mesh-offscreen",
				width = 128,
				height = 128,
				color_format = .R8G8B8A8_UNORM,
				depth_format = .D32_SFLOAT,
				clear_color = gpu.BLACK,
				clear_depth = 1,
			},
		),
	)
	_ = gpu.push_layer(&frame, {name = "mesh-offscreen-layer"})
	gpu.set_camera_3d(&frame, test_camera_3d())
	mesh_geometry := gpu.import_mesh_geometry(&frame, {id = 7, vertex_count = 3, index_count = 3})
	mesh := gpu.draw_instances(&frame, {geometry = mesh_geometry, instances = {count = 1}})

	testing.expect(t, offscreen != gpu.INVALID_TARGET)
	testing.expect(t, mesh != gpu.INVALID_COMMAND)
	_, ok := gpu.frame_runtime_mode(&frame)
	testing.expect(t, !ok)
}

@(test)
test_frame_api_offscreen_target_materializes_resources_and_pass :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)
	frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&frame)

	target := gpu.begin_target(
		&frame,
		gpu.offscreen_target(
			ctx,
			{
				name = "offscreen",
				width = 128,
				height = 64,
				color_format = .R8G8B8A8_UNORM,
				depth_format = .D32_SFLOAT,
				clear_color = gpu.BLACK,
				clear_depth = 1,
			},
		),
	)

	testing.expect(t, target != gpu.INVALID_TARGET)
	testing.expect(t, gpu.submit(&frame))

	fir := gpu.frame_ir(&frame)
	testing.expect_value(t, len(fir.resources), 2)
	testing.expect_value(t, len(fir.passes), 1)
	testing.expect(t, fir.targets[0].kind == .Offscreen)
	testing.expect(t, fir.targets[0].pass != gpu.INVALID_PASS)
	testing.expect(t, fir.resources[0].kind == .Texture)
	testing.expect(t, .Color_Attachment in fir.resources[0].texture.usage)
	testing.expect(t, .Sampled in fir.resources[0].texture.usage)
	testing.expect(t, fir.resources[1].kind == .Texture)
	testing.expect(t, .Depth_Stencil_Attachment in fir.resources[1].texture.usage)
	testing.expect(t, fir.passes[0].kind == .Render)
	testing.expect_value(t, fir.passes[0].color_target_count, u8(1))
	testing.expect(t, fir.passes[0].has_depth_target)
}

@(test)
test_frame_api_offscreen_target_rejects_invalid_dimensions :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)
	frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&frame)

	_ = gpu.begin_target(
		&frame,
		gpu.offscreen_target(
			ctx,
			{name = "bad-offscreen", width = 0, height = 64, color_format = .R8G8B8A8_UNORM},
		),
	)

	testing.expect(t, !gpu.submit(&frame))
}

@(test)
test_frame_api_samples_offscreen_target_as_resource_quad :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)
	frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&frame)

	offscreen := gpu.begin_target(
		&frame,
		gpu.offscreen_target(
			ctx,
			{
				name = "sampled-offscreen",
				width = 128,
				height = 64,
				color_format = .R8G8B8A8_UNORM,
				depth_format = .D32_SFLOAT,
				clear_color = gpu.BLACK,
				clear_depth = 1,
			},
		),
	)
	_ = gpu.push_layer(&frame, {name = "offscreen-layer"})
	_ = gpu.draw_rect(&frame, {x = 8, y = 8, width = 32, height = 24, color = gpu.RED})

	present := gpu.begin_target(&frame, gpu.present_target(ctx))
	_ = gpu.push_layer(&frame, {name = "present-layer"})
	sampled := gpu.draw_offscreen_texture_quad(
		&frame,
		{target = offscreen, x = 16, y = 16, width = 256, height = 128, color = gpu.WHITE},
	)

	testing.expect(t, offscreen != gpu.INVALID_TARGET)
	testing.expect(t, present != gpu.INVALID_TARGET)
	testing.expect(t, sampled != gpu.INVALID_COMMAND)

	fir := gpu.frame_ir(&frame)
	offscreen_desc, target_ok := ir.get_target(fir, offscreen)
	testing.expect(t, target_ok)
	testing.expect(t, offscreen_desc.color_resource != gpu.INVALID_RESOURCE)
	color_desc, color_ok := ir.get_resource(fir, offscreen_desc.color_resource)
	testing.expect(t, color_ok)
	testing.expect(t, .Sampled in color_desc.texture.usage)

	draw, draw_ok := ir.get_draw(fir, sampled)
	testing.expect(t, draw_ok)
	testing.expect(t, draw.packet.geometry.kind == .Quad_Stream)
	testing.expect(t, draw.packet.geometry.resource != offscreen_desc.color_resource)

	mode, mode_ok := gpu.frame_runtime_mode(&frame)
	testing.expect(t, mode_ok)
	testing.expect(t, mode == .Generated_2D)
	testing.expect(t, gpu.submit(&frame))
}

@(test)
test_frame_api_rejects_invalid_offscreen_sampling_targets :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)

	present_frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&present_frame)
	present := gpu.begin_target(&present_frame, gpu.present_target(ctx))
	present_sample := gpu.draw_offscreen_texture_quad(
		&present_frame,
		{target = present, x = 0, y = 0, color = gpu.WHITE},
	)
	testing.expect(t, present_sample == gpu.INVALID_COMMAND)

	self_frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&self_frame)
	offscreen := gpu.begin_target(
		&self_frame,
		gpu.offscreen_target(
			ctx,
			{
				name = "self-sample",
				width = 64,
				height = 64,
				color_format = .R8G8B8A8_UNORM,
				depth_format = .D32_SFLOAT,
				clear_color = gpu.BLACK,
				clear_depth = 1,
			},
		),
	)
	self_sample := gpu.draw_offscreen_texture_quad(
		&self_frame,
		{target = offscreen, x = 0, y = 0, color = gpu.WHITE},
	)
	testing.expect(t, self_sample == gpu.INVALID_COMMAND)
}

@(test)
test_frame_runtime_plan_orders_sortable_draws :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)
	frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&frame)

	_ = gpu.begin_target(&frame, gpu.present_target(ctx))
	_ = gpu.push_layer(&frame, {name = "world", sort_base = 0, order = .Sortable})
	first := gpu.draw_rect(
		&frame,
		{
			x = 10,
			y = 10,
			width = 10,
			height = 10,
			color = gpu.RED,
			sort_key = 30,
			order = .Sortable,
		},
	)
	second := gpu.draw_rect(
		&frame,
		{
			x = 20,
			y = 10,
			width = 10,
			height = 10,
			color = gpu.GREEN,
			sort_key = 10,
			order = .Sortable,
		},
	)
	third := gpu.draw_line_frame(
		&frame,
		{x0 = 0, y0 = 0, x1 = 100, y1 = 100, color = gpu.BLUE, sort_key = 20, order = .Sortable},
	)

	plan := gpu.frame_runtime_plan(&frame)
	defer gpu.destroy_frame_runtime_plan(&plan)

	testing.expect_value(t, len(plan), 3)
	testing.expect(t, plan[0].handle == second)
	testing.expect(t, plan[1].handle == third)
	testing.expect(t, plan[2].handle == first)
}

@(test)
test_frame_runtime_plan_keeps_mixed_order :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)
	frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&frame)

	_ = gpu.begin_target(&frame, gpu.present_target(ctx))
	_ = gpu.push_layer(&frame, {name = "world", sort_base = 0, order = .Sortable})
	gpu.set_camera_3d(&frame, test_camera_3d())

	first_2d := gpu.draw_rect(
		&frame,
		{
			x = 10,
			y = 10,
			width = 10,
			height = 10,
			color = gpu.RED,
			sort_key = 10,
			order = .Sortable,
		},
	)
	mesh_geometry := gpu.import_mesh_geometry(&frame, {id = 7, vertex_count = 3, index_count = 3})
	mesh := gpu.draw_instances(
		&frame,
		{geometry = mesh_geometry, instances = {count = 1}, sort_key = 20, order = .Sortable},
	)
	second_2d := gpu.draw_texture_quad(
		&frame,
		{
			texture = {id = 11, width = 16, height = 16},
			x = 30,
			y = 30,
			color = gpu.WHITE,
			sort_key = 30,
			order = .Sortable,
		},
	)

	plan := gpu.frame_runtime_plan(&frame)
	defer gpu.destroy_frame_runtime_plan(&plan)

	testing.expect_value(t, len(plan), 3)
	testing.expect(t, plan[0].handle == first_2d)
	testing.expect(t, plan[1].handle == mesh)
	testing.expect(t, plan[2].handle == second_2d)
}

@(test)
test_frame_runtime_plan_keeps_glyph_mesh_order :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)
	frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&frame)

	_ = gpu.begin_target(&frame, gpu.present_target(ctx))
	_ = gpu.push_layer(&frame, {name = "world", sort_base = 0, order = .Sortable})
	gpu.set_camera_3d(&frame, test_camera_3d())

	material := gpu.add_texture_material(
		&frame,
		{id = 22, width = 128, height = 128},
		{name = "glyph-atlas"},
	)
	glyphs := gpu.draw_text_run(
		&frame,
		{
			glyphs = []gpu.Glyph_Quad {
				{
					x = 10,
					y = 20,
					width = 8,
					height = 12,
					u0 = 0,
					v0 = 0,
					u1 = 1,
					v1 = 1,
					color = gpu.WHITE,
				},
			},
			material = material,
			sort_key = 10,
			order = .Sortable,
		},
	)
	mesh_geometry := gpu.import_mesh_geometry(&frame, {id = 7, vertex_count = 3, index_count = 3})
	mesh := gpu.draw_instances(
		&frame,
		{geometry = mesh_geometry, instances = {count = 1}, sort_key = 20, order = .Sortable},
	)

	plan := gpu.frame_runtime_plan(&frame)
	defer gpu.destroy_frame_runtime_plan(&plan)

	testing.expect_value(t, len(plan), 2)
	testing.expect(t, plan[0].handle == glyphs)
	testing.expect(t, plan[1].handle == mesh)

	mode, ok := gpu.frame_runtime_mode(&frame)
	testing.expect(t, ok)
	testing.expect(t, mode == .Mixed)
}

@(test)
test_frame_runtime_mode_accepts_mixed_generated_2d_and_meshes :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)

	rect_mesh_frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&rect_mesh_frame)
	_ = gpu.begin_target(&rect_mesh_frame, gpu.present_target(ctx))
	gpu.set_camera_3d(&rect_mesh_frame, test_camera_3d())
	_ = gpu.draw_rect(
		&rect_mesh_frame,
		{x = 10, y = 20, width = 30, height = 40, color = gpu.WHITE},
	)
	rect_mesh_geometry := gpu.import_mesh_geometry(
		&rect_mesh_frame,
		{id = 7, vertex_count = 3, index_count = 3},
	)
	_ = gpu.draw_instances(
		&rect_mesh_frame,
		{geometry = rect_mesh_geometry, instances = {count = 1}},
	)
	rect_mesh_mode, rect_mesh_ok := gpu.frame_runtime_mode(&rect_mesh_frame)
	testing.expect(t, rect_mesh_ok)
	testing.expect(t, rect_mesh_mode == .Mixed)

	circle_mesh_frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&circle_mesh_frame)
	_ = gpu.begin_target(&circle_mesh_frame, gpu.present_target(ctx))
	gpu.set_camera_3d(&circle_mesh_frame, test_camera_3d())
	_ = gpu.draw_circle_frame(
		&circle_mesh_frame,
		{cx = 100, cy = 120, radius = 40, color = gpu.YELLOW},
	)
	circle_mesh_geometry := gpu.import_mesh_geometry(
		&circle_mesh_frame,
		{id = 7, vertex_count = 3, index_count = 3},
	)
	_ = gpu.draw_instances(
		&circle_mesh_frame,
		{geometry = circle_mesh_geometry, instances = {count = 1}},
	)
	circle_mesh_mode, circle_mesh_ok := gpu.frame_runtime_mode(&circle_mesh_frame)
	testing.expect(t, circle_mesh_ok)
	testing.expect(t, circle_mesh_mode == .Mixed)
}

@(test)
test_frame_runtime_mode_rejects_unsupported_mixes :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)

	glyph_frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&glyph_frame)
	_ = gpu.begin_target(&glyph_frame, gpu.present_target(ctx))
	_ = gpu.draw_rect(&glyph_frame, {x = 10, y = 20, width = 30, height = 40, color = gpu.WHITE})
	_ = gpu.draw_text_run(
		&glyph_frame,
		{
			glyphs = []gpu.Glyph_Quad {
				{
					x = 10,
					y = 20,
					width = 6,
					height = 8,
					u0 = 0,
					v0 = 0,
					u1 = 1,
					v1 = 1,
					color = gpu.WHITE,
				},
			},
		},
	)
	_, glyph_ok := gpu.frame_runtime_mode(&glyph_frame)
	testing.expect(t, !glyph_ok)

	circle_glyph_frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&circle_glyph_frame)
	_ = gpu.begin_target(&circle_glyph_frame, gpu.present_target(ctx))
	_ = gpu.draw_circle_frame(
		&circle_glyph_frame,
		{cx = 100, cy = 120, radius = 40, color = gpu.YELLOW},
	)
	_ = gpu.draw_text_run(
		&circle_glyph_frame,
		{
			glyphs = []gpu.Glyph_Quad {
				{
					x = 10,
					y = 20,
					width = 6,
					height = 8,
					u0 = 0,
					v0 = 0,
					u1 = 1,
					v1 = 1,
					color = gpu.WHITE,
				},
			},
		},
	)
	_, circle_glyph_ok := gpu.frame_runtime_mode(&circle_glyph_frame)
	testing.expect(t, !circle_glyph_ok)

	glyph_mesh_frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&glyph_mesh_frame)
	_ = gpu.begin_target(&glyph_mesh_frame, gpu.present_target(ctx))
	gpu.set_camera_3d(&glyph_mesh_frame, test_camera_3d())
	_ = gpu.draw_text_run(
		&glyph_mesh_frame,
		{
			glyphs = []gpu.Glyph_Quad {
				{
					x = 10,
					y = 20,
					width = 6,
					height = 8,
					u0 = 0,
					v0 = 0,
					u1 = 1,
					v1 = 1,
					color = gpu.WHITE,
				},
			},
		},
	)
	glyph_mesh_geometry := gpu.import_mesh_geometry(
		&glyph_mesh_frame,
		{id = 7, vertex_count = 3, index_count = 3},
	)
	_ = gpu.draw_instances(
		&glyph_mesh_frame,
		{geometry = glyph_mesh_geometry, instances = {count = 1}},
	)
	_, glyph_mesh_ok := gpu.frame_runtime_mode(&glyph_mesh_frame)
	testing.expect(t, !glyph_mesh_ok)

	dispatch_frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&dispatch_frame)
	_ = gpu.begin_target(&dispatch_frame, gpu.present_target(ctx))
	_ = gpu.draw_rect(
		&dispatch_frame,
		{x = 10, y = 20, width = 30, height = 40, color = gpu.WHITE},
	)
	_ = gpu.dispatch(&dispatch_frame, {shader = {id = 3}, groups = {1, 1, 1}})
	_, dispatch_ok := gpu.frame_runtime_mode(&dispatch_frame)
	testing.expect(t, !dispatch_ok)
}

@(test)
test_frame_runtime_mode_accepts_dispatch_before_draw :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)
	frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&frame)

	_ = gpu.begin_target(&frame, gpu.present_target(ctx))
	push_constants := [4]u8{1, 2, 3, 4}
	buffer_view := gpu.import_storage_buffer(&frame, {id = 7, size = 64})
	dispatch := gpu.dispatch(
		&frame,
		{
			shader = {id = 3},
			groups = {4, 2, 1},
			storage_bindings = []gpu.Storage_Buffer_Binding{{binding = 0, buffer = buffer_view}},
			push_constants = raw_data(push_constants[:]),
			push_constants_size = u32(len(push_constants)),
			barrier_after = true,
			sort_key = 1,
		},
	)
	rect := gpu.draw_rect(
		&frame,
		{x = 10, y = 20, width = 30, height = 40, color = gpu.WHITE, sort_key = 2},
	)

	fir := gpu.frame_ir(&frame)
	testing.expect(t, dispatch != gpu.INVALID_COMMAND)
	testing.expect(t, rect != gpu.INVALID_COMMAND)
	testing.expect_value(t, len(fir.dispatch_commands), 1)
	testing.expect_value(t, fir.dispatch_commands[0].shader_id, u32(3))
	testing.expect_value(t, fir.dispatch_commands[0].storage_binding_count, u8(1))
	testing.expect_value(t, fir.dispatch_commands[0].storage_bindings[0].binding, u32(0))
	testing.expect_value(
		t,
		fir.dispatch_commands[0].storage_bindings[0].resource,
		buffer_view.resource,
	)
	testing.expect_value(t, len(fir.dispatch_commands[0].push_constants), 4)
	testing.expect_value(t, fir.dispatch_commands[0].push_constants[0], u8(1))
	testing.expect_value(t, fir.dispatch_commands[0].barrier_after, true)

	mode, ok := gpu.frame_runtime_mode(&frame)
	testing.expect(t, ok)
	testing.expect(t, mode == .Mixed)
}

@(test)
test_frame_dispatch_rejects_invalid_storage_bindings :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)

	duplicate_frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&duplicate_frame)
	_ = gpu.begin_target(&duplicate_frame, gpu.present_target(ctx))
	buffer_view := gpu.import_storage_buffer(&duplicate_frame, {id = 7, size = 64})
	duplicate := gpu.dispatch(
		&duplicate_frame,
		{
			shader = {id = 3},
			groups = {1, 1, 1},
			storage_bindings = []gpu.Storage_Buffer_Binding {
				{binding = 0, buffer = buffer_view},
				{binding = 0, buffer = buffer_view},
			},
		},
	)
	testing.expect(t, duplicate == gpu.INVALID_COMMAND)

	non_storage_frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&non_storage_frame)
	_ = gpu.begin_target(&non_storage_frame, gpu.present_target(ctx))
	resource := ir.add_buffer(
		gpu.frame_ir(&non_storage_frame),
		"vertex",
		64,
		{.Vertex},
		{},
		.Imported,
	)
	non_storage := gpu.dispatch(
		&non_storage_frame,
		{
			shader = {id = 3},
			groups = {1, 1, 1},
			storage_bindings = []gpu.Storage_Buffer_Binding {
				{binding = 0, buffer = {resource = resource, size = 64}},
			},
		},
	)
	testing.expect(t, non_storage == gpu.INVALID_COMMAND)

	missing_side_table_frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&missing_side_table_frame)
	_ = gpu.begin_target(&missing_side_table_frame, gpu.present_target(ctx))
	missing_resource := ir.add_buffer(
		gpu.frame_ir(&missing_side_table_frame),
		"storage",
		64,
		{.Storage},
		{},
		.Imported,
	)
	missing := gpu.dispatch(
		&missing_side_table_frame,
		{
			shader = {id = 3},
			groups = {1, 1, 1},
			storage_bindings = []gpu.Storage_Buffer_Binding {
				{binding = 0, buffer = {resource = missing_resource, size = 64}},
			},
		},
	)
	testing.expect(t, missing != gpu.INVALID_COMMAND)
	_, mode_ok := gpu.frame_runtime_mode(&missing_side_table_frame)
	testing.expect(t, !mode_ok)
}

@(test)
test_frame_runtime_mode_accepts_dispatch_only :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)
	frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&frame)

	_ = gpu.begin_target(&frame, gpu.present_target(ctx))
	_ = gpu.dispatch(&frame, {shader = {id = 3}, groups = {4, 2, 1}})

	mode, ok := gpu.frame_runtime_mode(&frame)
	testing.expect(t, ok)
	testing.expect(t, mode == .Dispatches)
}

@(test)
test_frame_runtime_mode_records_unsupported_instance_diagnostic :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)
	frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&frame)

	_ = gpu.begin_target(&frame, gpu.present_target(ctx))
	geometry := gpu.import_mesh_geometry(&frame, {id = 7, vertex_count = 3, index_count = 3})
	command := gpu.draw_instances(&frame, {geometry = geometry, instances = {count = 4}})

	_, ok := gpu.frame_runtime_mode(&frame)
	diagnostics := gpu.frame_diagnostics(&frame)

	testing.expect(t, command != gpu.INVALID_COMMAND)
	testing.expect(t, !ok)
	testing.expect_value(t, len(diagnostics), 1)
	testing.expect(t, diagnostics[0].code == .Unsupported)
	testing.expect(t, diagnostics[0].command == command)
}

@(test)
test_frame_api_dispatch_rejects_invalid_shader_without_command :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)
	frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&frame)

	dispatch := gpu.dispatch(&frame, {groups = {4, 2, 1}})

	fir := gpu.frame_ir(&frame)
	testing.expect(t, dispatch == gpu.INVALID_COMMAND)
	testing.expect_value(t, len(fir.dispatch_commands), 0)
}

@(test)
test_frame_runtime_mode_accepts_imported_meshes :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)
	frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&frame)

	_ = gpu.begin_target(&frame, gpu.present_target(ctx))
	gpu.set_camera_3d(&frame, test_camera_3d())
	geometry := gpu.import_mesh_geometry(&frame, {id = 7, vertex_count = 3, index_count = 3})
	_ = gpu.draw_instances(&frame, {geometry = geometry, instances = {count = 1}})

	mode, ok := gpu.frame_runtime_mode(&frame)
	testing.expect(t, ok)
	testing.expect(t, mode == .Meshes)
}

@(test)
test_frame_runtime_mode_accepts_mesh_instance_wrapper :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)
	frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&frame)

	_ = gpu.begin_target(&frame, gpu.present_target(ctx))
	gpu.set_camera_3d(&frame, test_camera_3d())
	mesh := gpu.draw_mesh_instance_frame(
		&frame,
		{
			mesh = {id = 7, vertex_count = 24, index_count = 36},
			transform = gpu.mat4_translate({1, 2, 3}),
		},
	)

	fir := gpu.frame_ir(&frame)
	testing.expect(t, mesh != gpu.INVALID_COMMAND)
	testing.expect_value(t, len(fir.resources), 1)
	testing.expect(t, fir.resources[0].lifetime == .Imported)
	testing.expect(t, fir.draw_commands[0].kind == .Indexed_Mesh)

	mode, ok := gpu.frame_runtime_mode(&frame)
	testing.expect(t, ok)
	testing.expect(t, mode == .Meshes)
}

@(test)
test_frame_api_light_intent_is_frame_owned :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)
	frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&frame)

	testing.expect(t, !frame.lighting_3d_enabled)
	gpu.set_ambient_light_frame(&frame, {0.02, 0.03, 0.04, 1})
	testing.expect(t, frame.lighting_3d_enabled)
	testing.expect_value(t, frame.ambient_light_3d, [4]f32{0.02, 0.03, 0.04, 1})

	light := gpu.create_point_light({1, 2, 3}, {1, 0.5, 0.25, 1}, 2, 8)
	testing.expect(t, gpu.add_light_frame(&frame, light))
	testing.expect_value(t, len(frame.lights_3d), 1)
	testing.expect_value(t, frame.lights_3d[0].position, [3]f32{1, 2, 3})
	testing.expect_value(t, frame.lights_3d[0].radius, f32(8))
}

@(test)
test_frame_api_light_intent_rejects_overflow :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)
	frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&frame)

	light := gpu.create_point_light({0, 1, 0})
	for i in 0 ..< renderer.MAX_LIGHTS {
		testing.expect(t, gpu.add_light_frame(&frame, light))
	}
	testing.expect(t, !gpu.add_light_frame(&frame, light))
	testing.expect_value(t, len(frame.lights_3d), renderer.MAX_LIGHTS)
}

@(test)
test_frame_api_add_mesh_material_maps_3d_material_fields :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)
	frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&frame)

	material := gpu.add_mesh_material(
		&frame,
		{
			diffuse = {id = 11, width = 64, height = 64},
			normal_map = {id = 13, width = 64, height = 64},
			color = {1, 0.5, 0.25, 1},
			double_sided = true,
			metallic = 0.2,
			roughness = 0.7,
			reflectance = 0.4,
			emissive = {0.1, 0.2, 0.3, 1},
		},
	)

	fir := gpu.frame_ir(&frame)
	testing.expect(t, material != gpu.INVALID_MATERIAL)
	testing.expect_value(t, len(fir.materials), 1)
	testing.expect_value(t, fir.materials[0].texture_id, u32(11))
	testing.expect_value(t, fir.materials[0].normal_map_texture_id, u32(13))
	testing.expect_value(t, fir.materials[0].base_color, [4]f32{1, 0.5, 0.25, 1})
	testing.expect(t, fir.materials[0].double_sided)
	testing.expect_value(t, fir.materials[0].metallic, f32(0.2))
	testing.expect_value(t, fir.materials[0].roughness, f32(0.7))
	testing.expect_value(t, fir.materials[0].reflectance, f32(0.4))
	testing.expect_value(t, fir.materials[0].emissive, [4]f32{0.1, 0.2, 0.3, 1})
}

@(test)
test_frame_api_draw_model_frame_emits_materialized_mesh_packets :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)
	frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&frame)

	_ = gpu.begin_target(&frame, gpu.present_target(ctx))
	gpu.set_camera_3d(&frame, test_camera_3d())
	model := gpu.Model {
		meshes    = []gpu.Mesh {
			{id = 7, vertex_count = 24, index_count = 36},
			{id = 8, vertex_count = 12, index_count = 18},
		},
		materials = []gpu.Material {
			{
				diffuse = {id = 21, width = 32, height = 32},
				color = {1, 1, 1, 1},
				double_sided = true,
			},
			{
				diffuse = {id = 22, width = 16, height = 16},
				color = {0.5, 1, 0.5, 1},
				roughness = 0.8,
			},
		},
		transform = gpu.matrix_identity(),
	}
	count := gpu.draw_model_frame(
		&frame,
		{model = model, position = {1, 2, 3}, scale = 2, tint = {0.5, 1, 1, 1}, sort_key = 10},
	)

	fir := gpu.frame_ir(&frame)
	testing.expect_value(t, count, u32(2))
	testing.expect_value(t, len(fir.resources), 2)
	testing.expect_value(t, len(fir.materials), 2)
	testing.expect_value(t, len(fir.draw_commands), 2)
	testing.expect(t, fir.draw_commands[0].kind == .Indexed_Mesh)
	testing.expect(t, fir.draw_commands[0].packet.material == gpu.Material_Handle(1))
	testing.expect(t, fir.draw_commands[1].packet.material == gpu.Material_Handle(2))
	testing.expect_value(t, fir.materials[0].texture_id, u32(21))
	testing.expect(t, fir.materials[0].double_sided)
	testing.expect_value(t, fir.materials[0].base_color, [4]f32{0.5, 1, 1, 1})
	testing.expect_value(t, fir.materials[1].texture_id, u32(22))
	testing.expect_value(t, fir.materials[1].base_color, [4]f32{0.25, 1, 0.5, 1})

	mode, ok := gpu.frame_runtime_mode(&frame)
	testing.expect(t, ok)
	testing.expect(t, mode == .Meshes)
}

@(test)
test_frame_3d_builtin_producers_reject_headless_without_resources :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 640, height = 480)
	frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&frame)

	testing.expect(
		t,
		gpu.draw_cube_frame(&frame, {center = {0, 0, 0}, size = {1, 1, 1}}) == gpu.INVALID_COMMAND,
	)
	testing.expect(
		t,
		gpu.draw_sphere_frame(&frame, {center = {0, 0, 0}, radius = 1}) == gpu.INVALID_COMMAND,
	)
	testing.expect(
		t,
		gpu.draw_plane_frame(&frame, {center = {0, 0, 0}, size = {1, 1}}) == gpu.INVALID_COMMAND,
	)
	testing.expect(
		t,
		gpu.draw_grid_frame(&frame, {slices = 10, spacing = 1}) == gpu.INVALID_COMMAND,
	)

	fir := gpu.frame_ir(&frame)
	testing.expect_value(t, len(fir.resources), 0)
	testing.expect_value(t, len(fir.draw_commands), 0)
}

@(test)
test_headless_context_destroy_is_safe :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 64, height = 64)
	testing.expect(t, !gpu.context_is_runtime_backed(ctx))

	gpu.destroy_context(&ctx)

	testing.expect(t, !gpu.context_is_runtime_backed(ctx))
	testing.expect_value(t, ctx.default_width, u32(0))
}

@(test)
test_frame_api_mesh_instances_and_dispatch :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 320, height = 240)
	frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&frame)

	_ = gpu.begin_target(&frame, gpu.present_target(ctx))
	layer := gpu.push_layer(&frame, {name = "world", order = .Depth})
	material := gpu.add_material(&frame, {name = "mat"})

	mesh := gpu.draw_mesh_frame(
		&frame,
		{
			geometry = {kind = .Mesh, vertex_count = 36},
			instances = {count = 1},
			material = material,
			order = .Depth,
		},
	)
	instances := gpu.draw_instances(
		&frame,
		{
			geometry = {kind = .Indexed_Mesh, index_count = 6},
			instances = {count = 4},
			material = material,
			order = .Sortable,
		},
	)
	dispatch := gpu.dispatch(&frame, {shader = {id = 3}, groups = {8, 1, 1}, sort_key = 9})

	fir := gpu.frame_ir(&frame)
	testing.expect(t, mesh != gpu.INVALID_COMMAND)
	testing.expect(t, instances != gpu.INVALID_COMMAND)
	testing.expect(t, dispatch != gpu.INVALID_COMMAND)
	testing.expect_value(t, len(fir.draw_commands), 2)
	testing.expect_value(t, len(fir.dispatch_commands), 1)
	testing.expect(t, fir.draw_commands[0].packet.layer == layer)
	testing.expect(t, fir.draw_commands[0].packet.material == material)
	testing.expect_value(t, fir.draw_commands[1].packet.instances.count, u32(4))
	testing.expect_value(t, fir.dispatch_commands[0].sequence, u64(3))
	testing.expect(t, gpu.submit(&frame))
}

@(test)
test_import_mesh_geometry_creates_imported_indexed_resource :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 320, height = 240)
	frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&frame)

	geometry := gpu.import_mesh_geometry(&frame, {id = 7, vertex_count = 24, index_count = 36})

	fir := gpu.frame_ir(&frame)
	testing.expect(t, geometry.kind == .Indexed_Mesh)
	testing.expect(t, geometry.resource != gpu.INVALID_RESOURCE)
	testing.expect_value(t, geometry.vertex_count, u32(24))
	testing.expect_value(t, geometry.index_count, u32(36))
	testing.expect_value(t, len(fir.resources), 1)
	testing.expect(t, fir.resources[0].kind == .Buffer)
	testing.expect(t, fir.resources[0].lifetime == .Imported)
	testing.expect(t, .Vertex in fir.resources[0].buffer.usage)
	testing.expect(t, .Index in fir.resources[0].buffer.usage)
}

@(test)
test_import_mesh_geometry_rejects_empty_mesh :: proc(t: ^testing.T) {
	ctx := gpu.init_context(width = 320, height = 240)
	frame := gpu.begin_frame(ctx)
	defer gpu.destroy_frame(&frame)

	geometry := gpu.import_mesh_geometry(&frame, {id = 7, vertex_count = 0, index_count = 36})

	fir := gpu.frame_ir(&frame)
	testing.expect(t, geometry.resource == gpu.INVALID_RESOURCE)
	testing.expect_value(t, len(fir.resources), 0)
}