package draw

import "core:c"
import "core:log"
import "core:mem"
import sdl "vendor:sdl3"

// Vertex layout for tessellated and text geometry.
// IMPORTANT: `color` must be premultiplied alpha (RGB channels pre-scaled by alpha).
// The tessellated fragment shader passes vertex color through directly — it does NOT
// premultiply. The blend state is ONE, ONE_MINUS_SRC_ALPHA (premultiplied-over).
// Use `premultiply_color` when constructing vertices manually for `prepare_shape`.
Vertex :: struct {
	position: Vec2,
	uv:       [2]f32,
	color:    Color,
}

TextBatch :: struct {
	atlas_texture: ^sdl.GPUTexture,
	vertex_start:  u32,
	vertex_count:  u32,
	index_start:   u32,
	index_count:   u32,
}

// ----------------------------------------------------------------------------------------------------------------
// ----- SDF primitive types -----------
// ----------------------------------------------------------------------------------------------------------------

// The SDF path evaluates one of four signed distance functions per primitive, dispatched
// by Shape_Kind encoded in the low byte of Primitive.flags:
//
//   RRect    — rounded rectangle with per-corner radii (sdRoundedBox). Also covers circles
//              (uniform radii = half-size), capsule-style line segments (rotated, max rounding),
//              and other RRect-reducible shapes.
//   NGon     — regular polygon with N sides and optional rounding.
//   Ellipse  — approximate ellipse (non-exact SDF, suitable for UI but not for shape merging).
//   Ring_Arc — annular ring with optional angular clipping. Covers full rings, partial arcs,
//              pie slices (inner_radius = 0), and loading spinners.
Shape_Kind :: enum u8 {
	Solid    = 0, // tessellated path (mode marker; not a real SDF kind)
	RRect    = 1,
	NGon     = 2,
	Ellipse  = 3,
	Ring_Arc = 4,
}

Shape_Flag :: enum u8 {
	Textured, // bit 0: sample texture using uv.uv_rect (mutually exclusive with Gradient)
	Gradient, // bit 1: 2-color gradient using uv.effects.gradient_color as end/outer color
	Gradient_Radial, // bit 2: if set with Gradient, radial from center; else linear at angle
	Outline, // bit 3: outer outline band using uv.effects.outline_color; CPU expands bounds by outline_width
	Rotated, // bit 4: shape has non-zero rotation; rotation_sc contains packed sin/cos
	Arc_Narrow, // bit 5: ring arc span ≤ π — intersect half-planes. Neither Arc bit = full ring.
	Arc_Wide, // bit 6: ring arc span > π — union half-planes. Neither Arc bit = full ring.
}

Shape_Flags :: bit_set[Shape_Flag;u8]

RRect_Params :: struct {
	half_size:    [2]f32,
	radii:        [4]f32,
	half_feather: f32, // feather_px * 0.5; shader uses smoothstep(-h, h, d)
	_:            f32,
}

NGon_Params :: struct {
	radius:       f32,
	sides:        f32,
	half_feather: f32, // feather_px * 0.5; shader uses smoothstep(-h, h, d)
	_:            [5]f32,
}

Ellipse_Params :: struct {
	radii:        [2]f32,
	half_feather: f32, // feather_px * 0.5; shader uses smoothstep(-h, h, d)
	_:            [5]f32,
}

Ring_Arc_Params :: struct {
	inner_radius: f32, // inner radius in physical pixels (0 for pie slice)
	outer_radius: f32, // outer radius in physical pixels
	normal_start: [2]f32, // pre-computed outward normal of start edge: (sin(start), -cos(start))
	normal_end:   [2]f32, // pre-computed outward normal of end edge: (-sin(end), cos(end))
	half_feather: f32, // feather_px * 0.5; shader uses smoothstep(-h, h, d)
	_:            f32,
}

Shape_Params :: struct #raw_union {
	rrect:    RRect_Params,
	ngon:     NGon_Params,
	ellipse:  Ellipse_Params,
	ring_arc: Ring_Arc_Params,
	raw:      [8]f32,
}
#assert(size_of(Shape_Params) == 32)

// GPU-side storage for 2-color gradient parameters and/or outline parameters.
// Packed into 16 bytes to alias with uv_rect in the Uv_Or_Effects raw union.
// The shader reads gradient_color and outline_color via unpackUnorm4x8.
// gradient_dir_sc stores the pre-computed gradient direction as (cos, sin) in f16 pair
// via unpackHalf2x16. outline_packed stores outline_width as f16 via unpackHalf2x16.
Gradient_Outline :: struct {
	gradient_color:  Color, //  0: end (linear) or outer (radial) gradient color
	outline_color:   Color, //  4: outline band color
	gradient_dir_sc: u32, //  8: packed f16 pair: low = cos(angle), high = sin(angle) — pre-computed gradient direction
	outline_packed:  u32, // 12: packed f16 pair: low = outline_width (f16, physical pixels), high = reserved
}

#assert(size_of(Gradient_Outline) == 16)

// Uv_Or_Effects aliases the final 16 bytes of a Primitive. When .Textured is set,
// uv_rect holds texture-atlas coordinates. When .Gradient or .Outline is set,
// effects holds 2-color gradient parameters and/or outline parameters.
// Textured and Gradient are mutually exclusive; if both are set, Gradient takes precedence.
Uv_Or_Effects :: struct #raw_union {
	uv_rect: [4]f32, // u_min, v_min, u_max, v_max (default {0,0,1,1})
	effects: Gradient_Outline, // gradient + outline parameters
}

// GPU layout: 80 bytes, std430-compatible. The shader declares this as a storage buffer struct.
// The low byte of `flags` encodes the Shape_Kind (0 = tessellated, 1-4 = SDF kinds).
// Bits 8-15 encode Shape_Flags (Textured, Gradient, Gradient_Radial, Outline, Rotated, Arc_Narrow, Arc_Wide).
// rotation_sc stores pre-computed sin/cos of the rotation angle as a packed f16 pair,
// avoiding per-pixel trigonometry in the fragment shader. Only read when .Rotated is set.
Primitive :: struct {
	bounds:      [4]f32, //  0: min_x, min_y, max_x, max_y (world-space, pre-DPI)
	color:       Color, // 16: u8x4, fill color / gradient start color / texture tint
	flags:       u32, // 20: low byte = Shape_Kind, bits 8+ = Shape_Flags
	rotation_sc: u32, // 24: packed f16 pair: low = sin(angle), high = cos(angle). Requires .Rotated flag.
	_pad:        f32, // 28: reserved for future use
	params:      Shape_Params, // 32: per-kind shape parameters (raw union, 32 bytes)
	uv:          Uv_Or_Effects, // 64: texture coords or gradient/outline parameters
}

#assert(size_of(Primitive) == 80)

// Pack shape kind and flags into the Primitive.flags field. The low byte encodes the Shape_Kind
// (which also serves as the SDF mode marker — kind > 0 means SDF path). The tessellated path
// leaves the field at 0 (Solid kind, set by vertex shader zero-initialization).
pack_kind_flags :: #force_inline proc(kind: Shape_Kind, flags: Shape_Flags) -> u32 {
	return u32(kind) | (u32(transmute(u8)flags) << 8)
}

// Pack two f16 values into a single u32 for GPU consumption via unpackHalf2x16.
// Used to pack gradient_dir_sc (cos/sin) and outline_packed (width/reserved) in Gradient_Outline.
pack_f16_pair :: #force_inline proc(low, high: f16) -> u32 {
	return u32(transmute(u16)low) | (u32(transmute(u16)high) << 16)
}

Pipeline_2D_Base :: struct {
	sdl_pipeline:     ^sdl.GPUGraphicsPipeline,
	vertex_buffer:    Buffer,
	index_buffer:     Buffer,
	unit_quad_buffer: ^sdl.GPUBuffer,
	primitive_buffer: Buffer,
	white_texture:    ^sdl.GPUTexture,
	sampler:          ^sdl.GPUSampler,
}

@(private)
create_pipeline_2d_base :: proc(
	device: ^sdl.GPUDevice,
	window: ^sdl.Window,
	sample_count: sdl.GPUSampleCount,
) -> (
	pipeline: Pipeline_2D_Base,
	ok: bool,
) {
	// On failure, clean up any partially-created resources
	defer if !ok {
		if pipeline.sampler != nil do sdl.ReleaseGPUSampler(device, pipeline.sampler)
		if pipeline.white_texture != nil do sdl.ReleaseGPUTexture(device, pipeline.white_texture)
		if pipeline.unit_quad_buffer != nil do sdl.ReleaseGPUBuffer(device, pipeline.unit_quad_buffer)
		if pipeline.primitive_buffer.gpu != nil do destroy_buffer(device, &pipeline.primitive_buffer)
		if pipeline.index_buffer.gpu != nil do destroy_buffer(device, &pipeline.index_buffer)
		if pipeline.vertex_buffer.gpu != nil do destroy_buffer(device, &pipeline.vertex_buffer)
		if pipeline.sdl_pipeline != nil do sdl.ReleaseGPUGraphicsPipeline(device, pipeline.sdl_pipeline)
	}

	active_shader_formats := sdl.GetGPUShaderFormats(device)
	if PLATFORM_SHADER_FORMAT_FLAG not_in active_shader_formats {
		log.errorf(
			"draw: no embedded shader matches active GPU formats; this build supports %v but device reports %v",
			PLATFORM_SHADER_FORMAT,
			active_shader_formats,
		)
		return pipeline, false
	}

	log.debug("Loaded", len(BASE_VERT_2D_RAW), "vert bytes")
	log.debug("Loaded", len(BASE_FRAG_2D_RAW), "frag bytes")

	vert_info := sdl.GPUShaderCreateInfo {
		code_size           = len(BASE_VERT_2D_RAW),
		code                = raw_data(BASE_VERT_2D_RAW),
		entrypoint          = SHADER_ENTRY,
		format              = {PLATFORM_SHADER_FORMAT_FLAG},
		stage               = .VERTEX,
		num_uniform_buffers = 1,
		num_storage_buffers = 1,
	}

	frag_info := sdl.GPUShaderCreateInfo {
		code_size    = len(BASE_FRAG_2D_RAW),
		code         = raw_data(BASE_FRAG_2D_RAW),
		entrypoint   = SHADER_ENTRY,
		format       = {PLATFORM_SHADER_FORMAT_FLAG},
		stage        = .FRAGMENT,
		num_samplers = 1,
	}

	vert_shader := sdl.CreateGPUShader(device, vert_info)
	if vert_shader == nil {
		log.errorf("Could not create draw vertex shader: %s", sdl.GetError())
		return pipeline, false
	}

	frag_shader := sdl.CreateGPUShader(device, frag_info)
	if frag_shader == nil {
		sdl.ReleaseGPUShader(device, vert_shader)
		log.errorf("Could not create draw fragment shader: %s", sdl.GetError())
		return pipeline, false
	}

	vertex_attributes: [3]sdl.GPUVertexAttribute = {
		// position (GLSL location 0)
		sdl.GPUVertexAttribute{buffer_slot = 0, location = 0, format = .FLOAT2, offset = 0},
		// uv (GLSL location 1)
		sdl.GPUVertexAttribute{buffer_slot = 0, location = 1, format = .FLOAT2, offset = size_of([2]f32)},
		// color (GLSL location 2, u8x4 normalized to float by GPU)
		sdl.GPUVertexAttribute{buffer_slot = 0, location = 2, format = .UBYTE4_NORM, offset = size_of([2]f32) * 2},
	}

	pipeline_info := sdl.GPUGraphicsPipelineCreateInfo {
		vertex_shader = vert_shader,
		fragment_shader = frag_shader,
		primitive_type = .TRIANGLELIST,
		multisample_state = sdl.GPUMultisampleState{sample_count = sample_count},
		target_info = sdl.GPUGraphicsPipelineTargetInfo {
			color_target_descriptions = &sdl.GPUColorTargetDescription {
				format = sdl.GetGPUSwapchainTextureFormat(device, window),
				// Premultiplied-alpha blending: src outputs RGB pre-multiplied by alpha,
				// so src factor is ONE (not SRC_ALPHA). This eliminates the per-pixel
				// divide in the outline path and is the standard blend mode used by
				// Skia, Flutter, and GPUI.
				blend_state = sdl.GPUColorTargetBlendState {
					enable_blend = true,
					enable_color_write_mask = true,
					src_color_blendfactor = .ONE,
					dst_color_blendfactor = .ONE_MINUS_SRC_ALPHA,
					color_blend_op = .ADD,
					src_alpha_blendfactor = .ONE,
					dst_alpha_blendfactor = .ONE_MINUS_SRC_ALPHA,
					alpha_blend_op = .ADD,
					color_write_mask = sdl.GPUColorComponentFlags{.R, .G, .B, .A},
				},
			},
			num_color_targets         = 1,
		},
		vertex_input_state = sdl.GPUVertexInputState {
			vertex_buffer_descriptions = &sdl.GPUVertexBufferDescription {
				slot = 0,
				input_rate = .VERTEX,
				pitch = size_of(Vertex),
			},
			num_vertex_buffers = 1,
			vertex_attributes = raw_data(vertex_attributes[:]),
			num_vertex_attributes = 3,
		},
	}

	pipeline.sdl_pipeline = sdl.CreateGPUGraphicsPipeline(device, pipeline_info)
	// Shaders are no longer needed regardless of pipeline creation success
	sdl.ReleaseGPUShader(device, vert_shader)
	sdl.ReleaseGPUShader(device, frag_shader)
	if pipeline.sdl_pipeline == nil {
		log.errorf("Failed to create draw graphics pipeline: %s", sdl.GetError())
		return pipeline, false
	}

	// Create vertex buffer
	vert_buf_ok: bool
	pipeline.vertex_buffer, vert_buf_ok = create_buffer(
		device,
		size_of(Vertex) * BUFFER_INIT_SIZE,
		sdl.GPUBufferUsageFlags{.VERTEX},
	)
	if !vert_buf_ok do return pipeline, false

	// Create index buffer (used by text)
	idx_buf_ok: bool
	pipeline.index_buffer, idx_buf_ok = create_buffer(
		device,
		size_of(c.int) * BUFFER_INIT_SIZE,
		sdl.GPUBufferUsageFlags{.INDEX},
	)
	if !idx_buf_ok do return pipeline, false

	// Create primitive storage buffer (used by SDF instanced drawing)
	prim_buf_ok: bool
	pipeline.primitive_buffer, prim_buf_ok = create_buffer(
		device,
		size_of(Primitive) * BUFFER_INIT_SIZE,
		sdl.GPUBufferUsageFlags{.GRAPHICS_STORAGE_READ},
	)
	if !prim_buf_ok do return pipeline, false

	// Create static 6-vertex unit quad buffer (two triangles, TRIANGLELIST)
	pipeline.unit_quad_buffer = sdl.CreateGPUBuffer(
		device,
		sdl.GPUBufferCreateInfo{usage = {.VERTEX}, size = 6 * size_of(Vertex)},
	)
	if pipeline.unit_quad_buffer == nil {
		log.errorf("Failed to create unit quad buffer: %s", sdl.GetError())
		return pipeline, false
	}

	// Create 1x1 white pixel texture
	pipeline.white_texture = sdl.CreateGPUTexture(
		device,
		sdl.GPUTextureCreateInfo {
			type = .D2,
			format = .R8G8B8A8_UNORM,
			usage = {.SAMPLER},
			width = 1,
			height = 1,
			layer_count_or_depth = 1,
			num_levels = 1,
			sample_count = ._1,
		},
	)
	if pipeline.white_texture == nil {
		log.errorf("Failed to create white pixel texture: %s", sdl.GetError())
		return pipeline, false
	}

	// Upload white pixel and unit quad data in a single command buffer
	white_pixel := Color{255, 255, 255, 255}
	white_transfer_buf := sdl.CreateGPUTransferBuffer(
		device,
		sdl.GPUTransferBufferCreateInfo{usage = .UPLOAD, size = size_of(white_pixel)},
	)
	if white_transfer_buf == nil {
		log.errorf("Failed to create white pixel transfer buffer: %s", sdl.GetError())
		return pipeline, false
	}
	defer sdl.ReleaseGPUTransferBuffer(device, white_transfer_buf)

	white_ptr := sdl.MapGPUTransferBuffer(device, white_transfer_buf, false)
	if white_ptr == nil {
		log.errorf("Failed to map white pixel transfer buffer: %s", sdl.GetError())
		return pipeline, false
	}
	mem.copy(white_ptr, &white_pixel, size_of(white_pixel))
	sdl.UnmapGPUTransferBuffer(device, white_transfer_buf)

	quad_verts := [6]Vertex {
		{position = {0, 0}},
		{position = {1, 0}},
		{position = {0, 1}},
		{position = {0, 1}},
		{position = {1, 0}},
		{position = {1, 1}},
	}
	quad_transfer_buf := sdl.CreateGPUTransferBuffer(
		device,
		sdl.GPUTransferBufferCreateInfo{usage = .UPLOAD, size = size_of(quad_verts)},
	)
	if quad_transfer_buf == nil {
		log.errorf("Failed to create unit quad transfer buffer: %s", sdl.GetError())
		return pipeline, false
	}
	defer sdl.ReleaseGPUTransferBuffer(device, quad_transfer_buf)

	quad_ptr := sdl.MapGPUTransferBuffer(device, quad_transfer_buf, false)
	if quad_ptr == nil {
		log.errorf("Failed to map unit quad transfer buffer: %s", sdl.GetError())
		return pipeline, false
	}
	mem.copy(quad_ptr, &quad_verts, size_of(quad_verts))
	sdl.UnmapGPUTransferBuffer(device, quad_transfer_buf)

	upload_cmd_buffer := sdl.AcquireGPUCommandBuffer(device)
	if upload_cmd_buffer == nil {
		log.errorf("Failed to acquire command buffer for init upload: %s", sdl.GetError())
		return pipeline, false
	}
	upload_pass := sdl.BeginGPUCopyPass(upload_cmd_buffer)

	sdl.UploadToGPUTexture(
		upload_pass,
		sdl.GPUTextureTransferInfo{transfer_buffer = white_transfer_buf},
		sdl.GPUTextureRegion{texture = pipeline.white_texture, w = 1, h = 1, d = 1},
		false,
	)

	sdl.UploadToGPUBuffer(
		upload_pass,
		sdl.GPUTransferBufferLocation{transfer_buffer = quad_transfer_buf},
		sdl.GPUBufferRegion{buffer = pipeline.unit_quad_buffer, offset = 0, size = size_of(quad_verts)},
		false,
	)

	sdl.EndGPUCopyPass(upload_pass)
	if !sdl.SubmitGPUCommandBuffer(upload_cmd_buffer) {
		log.errorf("Failed to submit init upload command buffer: %s", sdl.GetError())
		return pipeline, false
	}

	log.debug("White pixel texture and unit quad buffer created and uploaded")

	// Create sampler (shared by shapes and text)
	pipeline.sampler = sdl.CreateGPUSampler(
		device,
		sdl.GPUSamplerCreateInfo {
			min_filter = .LINEAR,
			mag_filter = .LINEAR,
			mipmap_mode = .LINEAR,
			address_mode_u = .CLAMP_TO_EDGE,
			address_mode_v = .CLAMP_TO_EDGE,
			address_mode_w = .CLAMP_TO_EDGE,
		},
	)
	if pipeline.sampler == nil {
		log.errorf("Could not create GPU sampler: %s", sdl.GetError())
		return pipeline, false
	}

	log.debug("Done creating unified draw pipeline")
	return pipeline, true
}

@(private)
upload :: proc(device: ^sdl.GPUDevice, pass: ^sdl.GPUCopyPass) {
	// Upload vertices (shapes then text into one buffer)
	shape_vert_count := u32(len(GLOB.tmp_shape_verts))
	text_vert_count := u32(len(GLOB.tmp_text_verts))
	total_vert_count := shape_vert_count + text_vert_count

	if total_vert_count > 0 {
		total_vert_size := total_vert_count * size_of(Vertex)
		shape_vert_size := shape_vert_count * size_of(Vertex)
		text_vert_size := text_vert_count * size_of(Vertex)

		grow_buffer_if_needed(
			device,
			&GLOB.pipeline_2d_base.vertex_buffer,
			total_vert_size,
			sdl.GPUBufferUsageFlags{.VERTEX},
		)

		vert_array := sdl.MapGPUTransferBuffer(device, GLOB.pipeline_2d_base.vertex_buffer.transfer, false)
		if vert_array == nil {
			log.panicf("Failed to map vertex transfer buffer: %s", sdl.GetError())
		}
		if shape_vert_size > 0 {
			mem.copy(vert_array, raw_data(GLOB.tmp_shape_verts), int(shape_vert_size))
		}
		if text_vert_size > 0 {
			mem.copy(
				rawptr(uintptr(vert_array) + uintptr(shape_vert_size)),
				raw_data(GLOB.tmp_text_verts),
				int(text_vert_size),
			)
		}
		sdl.UnmapGPUTransferBuffer(device, GLOB.pipeline_2d_base.vertex_buffer.transfer)

		sdl.UploadToGPUBuffer(
			pass,
			sdl.GPUTransferBufferLocation{transfer_buffer = GLOB.pipeline_2d_base.vertex_buffer.transfer},
			sdl.GPUBufferRegion{buffer = GLOB.pipeline_2d_base.vertex_buffer.gpu, offset = 0, size = total_vert_size},
			false,
		)
	}

	// Upload text indices
	index_count := u32(len(GLOB.tmp_text_indices))
	if index_count > 0 {
		index_size := index_count * size_of(c.int)

		grow_buffer_if_needed(
			device,
			&GLOB.pipeline_2d_base.index_buffer,
			index_size,
			sdl.GPUBufferUsageFlags{.INDEX},
		)

		idx_array := sdl.MapGPUTransferBuffer(device, GLOB.pipeline_2d_base.index_buffer.transfer, false)
		if idx_array == nil {
			log.panicf("Failed to map index transfer buffer: %s", sdl.GetError())
		}
		mem.copy(idx_array, raw_data(GLOB.tmp_text_indices), int(index_size))
		sdl.UnmapGPUTransferBuffer(device, GLOB.pipeline_2d_base.index_buffer.transfer)

		sdl.UploadToGPUBuffer(
			pass,
			sdl.GPUTransferBufferLocation{transfer_buffer = GLOB.pipeline_2d_base.index_buffer.transfer},
			sdl.GPUBufferRegion{buffer = GLOB.pipeline_2d_base.index_buffer.gpu, offset = 0, size = index_size},
			false,
		)
	}

	// Upload SDF primitives
	prim_count := u32(len(GLOB.tmp_primitives))
	if prim_count > 0 {
		prim_size := prim_count * size_of(Primitive)

		grow_buffer_if_needed(
			device,
			&GLOB.pipeline_2d_base.primitive_buffer,
			prim_size,
			sdl.GPUBufferUsageFlags{.GRAPHICS_STORAGE_READ},
		)

		prim_array := sdl.MapGPUTransferBuffer(device, GLOB.pipeline_2d_base.primitive_buffer.transfer, false)
		if prim_array == nil {
			log.panicf("Failed to map primitive transfer buffer: %s", sdl.GetError())
		}
		mem.copy(prim_array, raw_data(GLOB.tmp_primitives), int(prim_size))
		sdl.UnmapGPUTransferBuffer(device, GLOB.pipeline_2d_base.primitive_buffer.transfer)

		sdl.UploadToGPUBuffer(
			pass,
			sdl.GPUTransferBufferLocation{transfer_buffer = GLOB.pipeline_2d_base.primitive_buffer.transfer},
			sdl.GPUBufferRegion{buffer = GLOB.pipeline_2d_base.primitive_buffer.gpu, offset = 0, size = prim_size},
			false,
		)
	}
}

@(private)
draw_layer :: proc(
	device: ^sdl.GPUDevice,
	window: ^sdl.Window,
	cmd_buffer: ^sdl.GPUCommandBuffer,
	render_texture: ^sdl.GPUTexture,
	swapchain_width: u32,
	swapchain_height: u32,
	clear_color: [4]f32,
	layer: ^Layer,
) {
	if layer.sub_batch_len == 0 {
		if !GLOB.cleared {
			pass := sdl.BeginGPURenderPass(
				cmd_buffer,
				&sdl.GPUColorTargetInfo {
					texture = render_texture,
					clear_color = sdl.FColor{clear_color[0], clear_color[1], clear_color[2], clear_color[3]},
					load_op = .CLEAR,
					store_op = .STORE,
				},
				1,
				nil,
			)
			sdl.EndGPURenderPass(pass)
			GLOB.cleared = true
		}
		return
	}

	render_pass := sdl.BeginGPURenderPass(
		cmd_buffer,
		&sdl.GPUColorTargetInfo {
			texture = render_texture,
			clear_color = sdl.FColor{clear_color[0], clear_color[1], clear_color[2], clear_color[3]},
			load_op = GLOB.cleared ? .LOAD : .CLEAR,
			store_op = .STORE,
		},
		1,
		nil,
	)
	GLOB.cleared = true

	sdl.BindGPUGraphicsPipeline(render_pass, GLOB.pipeline_2d_base.sdl_pipeline)

	// Bind storage buffer (read by vertex shader in SDF mode)
	sdl.BindGPUVertexStorageBuffers(
		render_pass,
		0,
		([^]^sdl.GPUBuffer)(&GLOB.pipeline_2d_base.primitive_buffer.gpu),
		1,
	)

	// Always bind index buffer — harmless if no indexed draws are issued
	sdl.BindGPUIndexBuffer(
		render_pass,
		sdl.GPUBufferBinding{buffer = GLOB.pipeline_2d_base.index_buffer.gpu, offset = 0},
		._32BIT,
	)

	// Shorthand aliases for frequently-used pipeline resources
	main_vert_buf := GLOB.pipeline_2d_base.vertex_buffer.gpu
	unit_quad := GLOB.pipeline_2d_base.unit_quad_buffer
	white_texture := GLOB.pipeline_2d_base.white_texture
	sampler := GLOB.pipeline_2d_base.sampler
	width := f32(swapchain_width)
	height := f32(swapchain_height)

	// Initial GPU state: tessellated mode, main vertex buffer, no atlas bound yet
	push_globals(cmd_buffer, width, height, .Tessellated)
	sdl.BindGPUVertexBuffers(render_pass, 0, &sdl.GPUBufferBinding{buffer = main_vert_buf, offset = 0}, 1)

	current_mode: Draw_Mode = .Tessellated
	current_vert_buf := main_vert_buf
	current_atlas: ^sdl.GPUTexture
	current_sampler := sampler

	// Text vertices live after shape vertices in the GPU vertex buffer
	text_vertex_gpu_base := u32(len(GLOB.tmp_shape_verts))

	for &scissor in GLOB.scissors[layer.scissor_start:][:layer.scissor_len] {
		sdl.SetGPUScissor(render_pass, scissor.bounds)

		for &batch in GLOB.tmp_sub_batches[scissor.sub_batch_start:][:scissor.sub_batch_len] {
			switch batch.kind {
			case .Tessellated:
				if current_mode != .Tessellated {
					push_globals(cmd_buffer, width, height, .Tessellated)
					current_mode = .Tessellated
				}
				if current_vert_buf != main_vert_buf {
					sdl.BindGPUVertexBuffers(render_pass, 0, &sdl.GPUBufferBinding{buffer = main_vert_buf, offset = 0}, 1)
					current_vert_buf = main_vert_buf
				}
				// Determine texture and sampler for this batch
				batch_texture: ^sdl.GPUTexture = white_texture
				batch_sampler: ^sdl.GPUSampler = sampler
				if batch.texture_id != INVALID_TEXTURE {
					if bound_texture := texture_gpu_handle(batch.texture_id); bound_texture != nil {
						batch_texture = bound_texture
					}
					batch_sampler = get_sampler(batch.sampler)
				}
				if current_atlas != batch_texture || current_sampler != batch_sampler {
					sdl.BindGPUFragmentSamplers(
						render_pass,
						0,
						&sdl.GPUTextureSamplerBinding{texture = batch_texture, sampler = batch_sampler},
						1,
					)
					current_atlas = batch_texture
					current_sampler = batch_sampler
				}
				sdl.DrawGPUPrimitives(render_pass, batch.count, 1, batch.offset, 0)

			case .Text:
				if current_mode != .Tessellated {
					push_globals(cmd_buffer, width, height, .Tessellated)
					current_mode = .Tessellated
				}
				if current_vert_buf != main_vert_buf {
					sdl.BindGPUVertexBuffers(render_pass, 0, &sdl.GPUBufferBinding{buffer = main_vert_buf, offset = 0}, 1)
					current_vert_buf = main_vert_buf
				}
				text_batch := &GLOB.tmp_text_batches[batch.offset]
				if current_atlas != text_batch.atlas_texture {
					sdl.BindGPUFragmentSamplers(
						render_pass,
						0,
						&sdl.GPUTextureSamplerBinding{texture = text_batch.atlas_texture, sampler = sampler},
						1,
					)
					current_atlas = text_batch.atlas_texture
				}
				sdl.DrawGPUIndexedPrimitives(
					render_pass,
					text_batch.index_count,
					1,
					text_batch.index_start,
					i32(text_vertex_gpu_base + text_batch.vertex_start),
					0,
				)

			case .SDF:
				if current_mode != .SDF {
					push_globals(cmd_buffer, width, height, .SDF)
					current_mode = .SDF
				}
				if current_vert_buf != unit_quad {
					sdl.BindGPUVertexBuffers(render_pass, 0, &sdl.GPUBufferBinding{buffer = unit_quad, offset = 0}, 1)
					current_vert_buf = unit_quad
				}
				// Determine texture and sampler for this batch
				batch_texture: ^sdl.GPUTexture = white_texture
				batch_sampler: ^sdl.GPUSampler = sampler
				if batch.texture_id != INVALID_TEXTURE {
					if bound_texture := texture_gpu_handle(batch.texture_id); bound_texture != nil {
						batch_texture = bound_texture
					}
					batch_sampler = get_sampler(batch.sampler)
				}
				if current_atlas != batch_texture || current_sampler != batch_sampler {
					sdl.BindGPUFragmentSamplers(
						render_pass,
						0,
						&sdl.GPUTextureSamplerBinding{texture = batch_texture, sampler = batch_sampler},
						1,
					)
					current_atlas = batch_texture
					current_sampler = batch_sampler
				}
				sdl.DrawGPUPrimitives(render_pass, 6, batch.count, 0, batch.offset)
			}
		}
	}

	sdl.EndGPURenderPass(render_pass)
}

destroy_pipeline_2d_base :: proc(device: ^sdl.GPUDevice, pipeline: ^Pipeline_2D_Base) {
	destroy_buffer(device, &pipeline.vertex_buffer)
	destroy_buffer(device, &pipeline.index_buffer)
	destroy_buffer(device, &pipeline.primitive_buffer)
	if pipeline.unit_quad_buffer != nil {
		sdl.ReleaseGPUBuffer(device, pipeline.unit_quad_buffer)
	}
	sdl.ReleaseGPUTexture(device, pipeline.white_texture)
	sdl.ReleaseGPUSampler(device, pipeline.sampler)
	sdl.ReleaseGPUGraphicsPipeline(device, pipeline.sdl_pipeline)
}
