// Rendering library built on SDL3 GPU.
//
// ----- Coordinate system -----
// Origin is the top-left corner of the window/layer. X increases rightward, Y increases
// downward. This matches SDL, HTML Canvas, and most 2D UI coordinate conventions. All
// public position parameters (`center`, `origin`, `start_position`, `end_position`, every
// `Vec2`-typed field, every `Rectangle.x/y`, etc.) live in this coordinate system.
//
// ----- Unit-suffix convention -----
// Public CPU-side dimensions are in *logical* pixels by default (CSS-style: a value of 200
// looks the same physical size on a 1× monitor and a 2× Retina display). Suffix rules:
//
//   no suffix    — logical pixels. Default for layout values (positions, sizes, radii,
//                  outline widths, line thicknesses, gradient endpoints, etc.).
//   `_lpx`       — logical pixels, *explicit*. Optional. Use when an identifier would
//                  otherwise be ambiguous about which kind of pixel it carries —
//                  typically standalone constants like `SCANLINE_STRIPE_LPX` where the
//                  context doesn't make the unit obvious from the surrounding code.
//                  Procedure parameters and struct fields named after a layout property
//                  (`width`, `radius`, ...) don't need this suffix.
//   `_ppx`       — physical (device) pixels. Required whenever a value is in physical
//                  pixels, regardless of context. Reserved for quantities whose
//                  right-feeling magnitude is a property of the device pixel grid rather
//                  than of the layout: anti-aliasing band widths, sub-pixel snap targets,
//                  MSDF screen-pixel-range parameters.
//
// Examples:
//
//   width, height, radius, outline_width, thickness   — logical px (no suffix)
//   SCANLINE_STRIPE_LPX, SCANLINE_GAP_LPX              — logical px (explicit `_lpx`)
//   feather_ppx, aa_ppx                                — physical px (`_ppx`)
//
// Layout values scale with DPI; rasterization-grid values do not. The shader handles the
// logical-to-physical conversion at the rasterization boundary; CPU-side `_ppx` inputs that
// need to interact with logical-space data convert via `/ dpi_scaling` at the use site.
//
// ----- Anti-aliasing -----
// MSAA is intentionally NOT supported. SDF text and shapes compute fragment coverage
// analytically via `smoothstep`, so they don't benefit from multisampling. Tessellated
// user geometry submitted via `prepare_shape` is rendered without anti-aliasing — if AA is
// required for tessellated content, the caller must either render it to their own offscreen
// target and submit the result as a texture, or use the AA helpers in the `tess` subpackage
// (e.g. `tess.triangle_aa` extrudes 1-physical-pixel alpha-falloff edge bands). This
// decision aligns with the SBC target (Mali Valhall, where MSAA's per-tile bandwidth
// multiplier is expensive) and matches RAD Debugger's architecture.
//
// ----- Color and blending -----
// `Color` is RGBA8 in memory order (R, G, B, A at indices 0..3). The shader unpacks via
// `unpackUnorm4x8`, which reads bytes in that exact order. Alpha 255 = fully opaque, 0 =
// fully transparent.
//
// All rendering uses *premultiplied-over* blending (blend state ONE, ONE_MINUS_SRC_ALPHA —
// the standard mode used by Skia, Flutter, and GPUI). Three implications:
//
//   - Public shape procs (`rectangle`, `circle`, `line`, etc.) accept straight-alpha
//     `Color` values and the SDF fragment shaders premultiply internally; users of these
//     procs don't need to think about premultiplication.
//   - Vertex colors written to the shared vertex stream (the tessellated path — text and
//     anything submitted via `prepare_shape`, including `tess.*` helpers) MUST be
//     premultiplied at the CPU. The tessellated fragment shader passes vertex color through
//     directly without further modification. The `premultiply_color` helper handles this.
//   - The clear color passed to `end()` is also premultiplied internally before being
//     handed to the GPU; callers pass straight-alpha `Color` here too.
package draw

import "base:runtime"
import "core:c"
import "core:log"
import "core:math"
import sdl "vendor:sdl3"
import sdl_ttf "vendor:sdl3/ttf"

// ---------------------------------------------------------------------------------------------------------------------
// ----- Shader format ------------
// ---------------------------------------------------------------------------------------------------------------------

//INTERNAL (each constant in the when-block below)
when ODIN_OS == .Darwin {
	PLATFORM_SHADER_FORMAT_FLAG :: sdl.GPUShaderFormatFlag.MSL
	SHADER_ENTRY :: cstring("main0")
	BASE_VERT_2D_RAW :: #load("shaders/generated/base_2d.vert.metal")
	BASE_FRAG_2D_RAW :: #load("shaders/generated/base_2d.frag.metal")
	BACKDROP_FULLSCREEN_VERT_RAW :: #load("shaders/generated/backdrop_fullscreen.vert.metal")
	BACKDROP_DOWNSAMPLE_FRAG_RAW :: #load("shaders/generated/backdrop_downsample.frag.metal")
	BACKDROP_BLUR_VERT_RAW :: #load("shaders/generated/backdrop_blur.vert.metal")
	BACKDROP_BLUR_FRAG_RAW :: #load("shaders/generated/backdrop_blur.frag.metal")
} else {
	PLATFORM_SHADER_FORMAT_FLAG :: sdl.GPUShaderFormatFlag.SPIRV
	SHADER_ENTRY :: cstring("main")
	BASE_VERT_2D_RAW :: #load("shaders/generated/base_2d.vert.spv")
	BASE_FRAG_2D_RAW :: #load("shaders/generated/base_2d.frag.spv")
	BACKDROP_FULLSCREEN_VERT_RAW :: #load("shaders/generated/backdrop_fullscreen.vert.spv")
	BACKDROP_DOWNSAMPLE_FRAG_RAW :: #load("shaders/generated/backdrop_downsample.frag.spv")
	BACKDROP_BLUR_VERT_RAW :: #load("shaders/generated/backdrop_blur.vert.spv")
	BACKDROP_BLUR_FRAG_RAW :: #load("shaders/generated/backdrop_blur.frag.spv")
}

PLATFORM_SHADER_FORMAT :: sdl.GPUShaderFormat{PLATFORM_SHADER_FORMAT_FLAG}

// ---------------------------------------------------------------------------------------------------------------------
// ----- Defaults and config ------------
// ---------------------------------------------------------------------------------------------------------------------

//INTERNAL
BUFFER_INIT_SIZE :: 256
//INTERNAL
INITIAL_LAYER_SIZE :: 5
//INTERNAL
INITIAL_SCISSOR_SIZE :: 10

// ----- Default parameter values -----
// Named constants for non-zero default procedure parameters. Centralizes magic numbers
// so they can be tuned in one place and referenced by name in proc signatures.
DFT_FEATHER_PPX :: 1 // Total AA feather width in physical pixels (half on each side of boundary).
DFT_STROKE_THICKNESS :: 1 // Default line/stroke thickness in logical pixels.
DFT_FONT_SIZE :: 44 // Default font size in points for text rendering.
DFT_CIRC_END_ANGLE :: 360 // Full-circle end angle in degrees (ring/arc).
DFT_UV_RECT :: Rectangle{0, 0, 1, 1} // Full-texture UV rect (Texture_Fill default).
DFT_TINT :: WHITE // Default texture tint (Texture_Fill, clay_image).
DFT_TEXT_COLOR :: BLACK // Default text color.
DFT_CLEAR_COLOR :: BLACK // Default clear color for end().
DFT_SAMPLER :: Sampler_Preset.Linear_Clamp // Default texture sampler preset.

// ---------------------------------------------------------------------------------------------------------------------
// ----- Global state ------------
// ---------------------------------------------------------------------------------------------------------------------

//INTERNAL
GLOB: Global

//INTERNAL
Global :: struct {
	// -- Per-frame staging (hottest — touched by every prepare/upload/clear cycle) --
	tmp_shape_verts:              [dynamic]Vertex_2D, // Tessellated shape vertices staged for GPU upload.
	tmp_text_verts:               [dynamic]Vertex_2D, // Text vertices staged for GPU upload.
	tmp_text_indices:             [dynamic]c.int, // Text index buffer staged for GPU upload.
	tmp_text_batches:             [dynamic]Text_Batch, // Text atlas batch metadata for indexed drawing.
	tmp_primitives:               [dynamic]Core_2D_Primitive, // SDF primitives staged for GPU storage buffer upload (core 2D subsystem).
	tmp_sub_batches:              [dynamic]Sub_Batch, // Sub-batch records that drive draw call dispatch.
	tmp_uncached_text:            [dynamic]^sdl_ttf.Text, // Uncached TTF_Text objects destroyed after end() submits.
	tmp_gaussian_blur_primitives: [dynamic]Gaussian_Blur_Primitive, // Gaussian blur primitives staged for GPU storage buffer upload.
	layers:                       [dynamic]Layer, // Draw layers, each with its own scissor stack.
	scissors:                     [dynamic]Scissor, // Scissor rects that clip drawing within each layer.

	// -- Per-frame scalars (accessed during prepare and draw_layer) --
	curr_layer_index:             uint, // Index of the currently active layer.
	dpi_scaling:                  f32, // Window DPI scale factor applied to all pixel coordinates.
	clay_z_index:                 i16, // Tracks z-index for layer splitting during Clay batch processing.
	cleared:                      bool, // Whether the render target has been cleared this frame.

	// Per-frame: which layer (if any) currently has an open begin_backdrop scope.
	// Reset to nil at frame start. end() panics if non-nil at frame end.
	open_backdrop_layer:          ^Layer,

	// -- Subsystems (accessed every draw_layer call) --
	core_2d:                      Core_2D, // The unified 2D GPU pipeline (shaders, buffers, samplers).
	backdrop:                     Backdrop, // Frosted-glass backdrop blur subsystem (downsample + blur PSOs, working textures).
	device:                       ^sdl.GPUDevice, // GPU device handle, stored at init.
	samplers:                     [SAMPLER_PRESET_COUNT]^sdl.GPUSampler, // Lazily-created sampler objects, one per Sampler_Preset.

	// -- Deferred release (processed once per frame at frame boundary) --
	pending_texture_releases:     [dynamic]Texture_Id, // Deferred GPU texture releases, processed next frame.
	pending_text_releases:        [dynamic]^sdl_ttf.Text, // Deferred TTF_Text destroys, processed next frame.

	// -- Textures (registration is occasional, binding is per draw call) --
	texture_slots:                [dynamic]Texture_Slot, // Registered texture slots indexed by Texture_Id.
	texture_free_list:            [dynamic]u32, // Recycled slot indices available for reuse.

	// -- Clay (once per frame in prepare_clay_batch) --
	clay_memory:                  [^]u8, // Raw memory block backing Clay's internal arena.
	clay_merge_open_stack:        [dynamic]Clay_Merge_Candidate, // Pending Rectangle/Image primitives waiting for a matching Border to merge with.

	// -- Text (occasional — font registration and text cache lookups) --
	text_cache:                   Text_Cache, // Font registry, SDL_ttf engine, and cached TTF_Text objects.

	// -- Resize tracking (cold — checked once per frame in resize_global) --
	max_layers:                   int, // High-water marks for dynamic array shrink heuristic.
	max_scissors:                 int,
	max_shape_verts:              int,
	max_text_verts:               int,
	max_text_indices:             int,
	max_text_batches:             int,
	max_primitives:               int,
	max_sub_batches:              int,
	max_gaussian_blur_primitives: int,

	// -- Init-only (coldest — set once at init, never written again) --
	odin_context:                 runtime.Context, // Odin context captured at init for use in callbacks.
}

// ---------------------------------------------------------------------------------------------------------------------
// ----- Core types ------------
// ---------------------------------------------------------------------------------------------------------------------

// A 2D position in world space. Non-distinct alias for [2]f32 — bare literals like {100, 200}
// work at non-ambiguous call sites. See the package doc for coordinate-system and unit
// conventions.
Vec2 :: [2]f32

// An RGBA color with 8 bits per channel. Distinct type over [4]u8 so that proc-group
// overloads can disambiguate Color from other 4-byte structs. See the package doc for the
// memory layout and the premultiplied-over blending contract.
Color :: [4]u8

BLACK :: Color{0, 0, 0, 255}
WHITE :: Color{255, 255, 255, 255}
RED :: Color{255, 0, 0, 255}
GREEN :: Color{0, 255, 0, 255}
BLUE :: Color{0, 0, 255, 255}
BLANK :: Color{0, 0, 0, 0}

Rectangle :: struct {
	x:      f32,
	y:      f32,
	width:  f32,
	height: f32,
}

// Per-corner rounding radii for rectangles, specified clockwise from top-left.
// All values are in logical pixels (pre-DPI-scaling).
Rectangle_Radii :: struct {
	top_left:     f32,
	top_right:    f32,
	bottom_right: f32,
	bottom_left:  f32,
}

// A linear gradient between two colors along an arbitrary angle.
// `angle` is in degrees: 0 = left-to-right, 90 = top-to-bottom.
Linear_Gradient :: struct {
	start_color: Color,
	end_color:   Color,
	angle:       f32,
}

// A radial gradient between two colors from center to edge.
Radial_Gradient :: struct {
	inner_color: Color,
	outer_color: Color,
}

// Sample a registered texture as the shape's fill source.
// `tint` modulates the sampled texels per-pixel (constant multiply); WHITE passes through
// unchanged. Translucent tints fade the texture; non-white tints recolor it.
// Zero-initialized fields are treated as defaults by the shape procs:
//   tint    == Color{}      → WHITE
//   uv_rect == Rectangle{}  → {0, 0, 1, 1}  (full texture)
//   sampler == .Linear_Clamp (enum value 0)
Texture_Fill :: struct {
	id:      Texture_Id,
	tint:    Color,
	uv_rect: Rectangle,
	sampler: Sampler_Preset,
}

// Mutually exclusive fill sources for shape procs. Each shape proc accepts a Brush
// as its third positional parameter. Texture and gradient are mutually exclusive at
// the GPU level (they share the worst-case register path); outline is orthogonal and
// composes with any Brush variant.
Brush :: union {
	Color,
	Linear_Gradient,
	Radial_Gradient,
	Texture_Fill,
}

// Convert Color to [4]f32 in 0.0–1.0 range. Useful for SDL interop (e.g. clear color).
color_to_f32 :: proc(color: Color) -> [4]f32 {
	INV :: 1.0 / 255.0
	return {f32(color[0]) * INV, f32(color[1]) * INV, f32(color[2]) * INV, f32(color[3]) * INV}
}

// Pre-multiply RGB channels by alpha. Required for any vertex written to the tessellated
// vertex stream (text path or `prepare_shape`-style submissions); see the package doc's
// "Color and blending" section for the full contract.
premultiply_color :: #force_inline proc(color: Color) -> Color {
	a := u32(color[3])
	return Color {
		u8((u32(color[0]) * a + 127) / 255),
		u8((u32(color[1]) * a + 127) / 255),
		u8((u32(color[2]) * a + 127) / 255),
		color[3],
	}
}

// ---------------------------------------------------------------------------------------------------------------------
// ----- Frame layout types ------------
// ---------------------------------------------------------------------------------------------------------------------

//INTERNAL
Sub_Batch_Kind :: enum u8 {
	Tessellated, // non-indexed, white texture or user texture, Core_2D_Mode.Tessellated
	Text, // indexed, atlas texture, Core_2D_Mode.Text (vertices already in physical-pixel space)
	SDF, // instanced unit quad, Core_2D_Mode.SDF
	// instanced unit quad, backdrop subsystem V-composite (indexes Gaussian_Blur_Primitive).
	// Bracket-scheduled per layer; see README.md § "Backdrop pipeline" for ordering semantics.
	Backdrop,
}

//INTERNAL
Sub_Batch :: struct {
	kind:           Sub_Batch_Kind,
	offset:         u32, // Tessellated: vertex offset; Text: text_batch index; SDF/Backdrop: primitive index
	count:          u32, // Tessellated: vertex count; Text: always 1; SDF/Backdrop: primitive count
	texture_id:     Texture_Id,
	sampler:        Sampler_Preset,
	// Backdrop only — Gaussian std-dev in logical pixels. Named with the
	// distribution prefix because future kinds may want different sigma
	// shapes (e.g. drop-shadow penumbra) without overloading this field.
	gaussian_sigma: f32,
}

Layer :: struct {
	bounds:          Rectangle,
	sub_batch_start: u32,
	sub_batch_len:   u32,
	scissor_start:   u32,
	scissor_len:     u32,
}

//INTERNAL
Scissor :: struct {
	bounds:          sdl.Rect,
	sub_batch_start: u32,
	sub_batch_len:   u32,
}

// ---------------------------------------------------------------------------------------------------------------------
// ----- Lifecycle ------------
// ---------------------------------------------------------------------------------------------------------------------

// Initialize the renderer. Returns false if GPU pipeline or text engine creation fails.
@(require_results)
init :: proc(
	device: ^sdl.GPUDevice,
	window: ^sdl.Window,
	allocator := context.allocator,
	odin_context := context,
) -> (
	ok: bool,
) {
	core, core_ok := create_core_2d(device, window)
	if !core_ok {
		return false
	}

	backdrop, backdrop_ok := create_backdrop(device, window)
	if !backdrop_ok {
		destroy_core_2d(device, &core)
		return false
	}

	text_cache, text_ok := init_text_cache(device, allocator)
	if !text_ok {
		destroy_backdrop(device, &backdrop)
		destroy_core_2d(device, &core)
		return false
	}

	GLOB = Global {
		layers                       = make([dynamic]Layer, 0, INITIAL_LAYER_SIZE, allocator = allocator),
		scissors                     = make([dynamic]Scissor, 0, INITIAL_SCISSOR_SIZE, allocator = allocator),
		tmp_shape_verts              = make([dynamic]Vertex_2D, 0, BUFFER_INIT_SIZE, allocator = allocator),
		tmp_text_verts               = make([dynamic]Vertex_2D, 0, BUFFER_INIT_SIZE, allocator = allocator),
		tmp_text_indices             = make([dynamic]c.int, 0, BUFFER_INIT_SIZE, allocator = allocator),
		tmp_text_batches             = make([dynamic]Text_Batch, 0, BUFFER_INIT_SIZE, allocator = allocator),
		tmp_primitives               = make(
			[dynamic]Core_2D_Primitive,
			0,
			BUFFER_INIT_SIZE,
			allocator = allocator,
		),
		tmp_sub_batches              = make([dynamic]Sub_Batch, 0, BUFFER_INIT_SIZE, allocator = allocator),
		tmp_uncached_text            = make([dynamic]^sdl_ttf.Text, 0, 16, allocator = allocator),
		tmp_gaussian_blur_primitives = make(
			[dynamic]Gaussian_Blur_Primitive,
			0,
			BUFFER_INIT_SIZE,
			allocator = allocator,
		),
		device                       = device,
		texture_slots                = make([dynamic]Texture_Slot, 0, 16, allocator = allocator),
		texture_free_list            = make([dynamic]u32, 0, 16, allocator = allocator),
		pending_texture_releases     = make([dynamic]Texture_Id, 0, 16, allocator = allocator),
		pending_text_releases        = make([dynamic]^sdl_ttf.Text, 0, 16, allocator = allocator),
		odin_context                 = odin_context,
		dpi_scaling                  = sdl.GetWindowDisplayScale(window),
		core_2d                      = core,
		backdrop                     = backdrop,
		text_cache                   = text_cache,
	}

	// Reserve slot 0 for INVALID_TEXTURE
	append(&GLOB.texture_slots, Texture_Slot{})
	log.debug("Window DPI scaling:", GLOB.dpi_scaling)
	init_clay(window, allocator)
	return true
}

// TODO Either every x frames nuke max values in case of edge cases where max gets set very high
// or leave to application code to decide the right time for resize
resize_global :: proc() {
	if len(GLOB.layers) > GLOB.max_layers do GLOB.max_layers = len(GLOB.layers)
	shrink(&GLOB.layers, GLOB.max_layers)
	if len(GLOB.scissors) > GLOB.max_scissors do GLOB.max_scissors = len(GLOB.scissors)
	shrink(&GLOB.scissors, GLOB.max_scissors)
	if len(GLOB.tmp_shape_verts) > GLOB.max_shape_verts do GLOB.max_shape_verts = len(GLOB.tmp_shape_verts)
	shrink(&GLOB.tmp_shape_verts, GLOB.max_shape_verts)
	if len(GLOB.tmp_text_verts) > GLOB.max_text_verts do GLOB.max_text_verts = len(GLOB.tmp_text_verts)
	shrink(&GLOB.tmp_text_verts, GLOB.max_text_verts)
	if len(GLOB.tmp_text_indices) > GLOB.max_text_indices do GLOB.max_text_indices = len(GLOB.tmp_text_indices)
	shrink(&GLOB.tmp_text_indices, GLOB.max_text_indices)
	if len(GLOB.tmp_text_batches) > GLOB.max_text_batches do GLOB.max_text_batches = len(GLOB.tmp_text_batches)
	shrink(&GLOB.tmp_text_batches, GLOB.max_text_batches)
	if len(GLOB.tmp_primitives) > GLOB.max_primitives do GLOB.max_primitives = len(GLOB.tmp_primitives)
	shrink(&GLOB.tmp_primitives, GLOB.max_primitives)
	if len(GLOB.tmp_sub_batches) > GLOB.max_sub_batches do GLOB.max_sub_batches = len(GLOB.tmp_sub_batches)
	shrink(&GLOB.tmp_sub_batches, GLOB.max_sub_batches)
	if len(GLOB.tmp_gaussian_blur_primitives) > GLOB.max_gaussian_blur_primitives do GLOB.max_gaussian_blur_primitives = len(GLOB.tmp_gaussian_blur_primitives)
	shrink(&GLOB.tmp_gaussian_blur_primitives, GLOB.max_gaussian_blur_primitives)
}

destroy :: proc(device: ^sdl.GPUDevice, allocator := context.allocator) {
	delete(GLOB.layers)
	delete(GLOB.scissors)
	delete(GLOB.tmp_shape_verts)
	delete(GLOB.tmp_text_verts)
	delete(GLOB.tmp_text_indices)
	delete(GLOB.tmp_text_batches)
	delete(GLOB.tmp_primitives)
	delete(GLOB.tmp_sub_batches)
	delete(GLOB.tmp_gaussian_blur_primitives)
	for ttf_text in GLOB.tmp_uncached_text do sdl_ttf.DestroyText(ttf_text)
	delete(GLOB.tmp_uncached_text)
	destroy_clay(allocator)
	process_pending_texture_releases()
	destroy_all_textures()
	destroy_sampler_pool()
	for ttf_text in GLOB.pending_text_releases do sdl_ttf.DestroyText(ttf_text)
	delete(GLOB.pending_text_releases)
	destroy_backdrop(device, &GLOB.backdrop)
	destroy_core_2d(device, &GLOB.core_2d)
	destroy_text_cache()
}

//INTERNAL
clear_global :: proc() {
	// Process deferred texture releases from the previous frame
	process_pending_texture_releases()
	// Process deferred text releases from the previous frame
	for ttf_text in GLOB.pending_text_releases do sdl_ttf.DestroyText(ttf_text)
	clear(&GLOB.pending_text_releases)

	GLOB.curr_layer_index = 0
	GLOB.cleared = false
	GLOB.open_backdrop_layer = nil
	// Destroy uncached TTF_Text objects from the previous frame (after end() has submitted draw data)
	for ttf_text in GLOB.tmp_uncached_text do sdl_ttf.DestroyText(ttf_text)
	clear(&GLOB.tmp_uncached_text)
	clear(&GLOB.layers)
	clear(&GLOB.scissors)
	clear(&GLOB.tmp_shape_verts)
	clear(&GLOB.tmp_text_verts)
	clear(&GLOB.tmp_text_indices)
	clear(&GLOB.tmp_text_batches)
	clear(&GLOB.tmp_primitives)
	clear(&GLOB.tmp_sub_batches)
	clear(&GLOB.tmp_gaussian_blur_primitives)
	clear_clay_per_frame()
}

// ---------------------------------------------------------------------------------------------------------------------
// ----- Frame ------------
// ---------------------------------------------------------------------------------------------------------------------

// Creates a new layer
new_layer :: proc(prev_layer: ^Layer, bounds: Rectangle) -> ^Layer {
	if GLOB.open_backdrop_layer != nil {
		log.panicf("new_layer called while backdrop scope is open on layer %p", GLOB.open_backdrop_layer)
	}

	layer := Layer {
		bounds          = bounds,
		sub_batch_start = prev_layer.sub_batch_start + prev_layer.sub_batch_len,
		scissor_start   = prev_layer.scissor_start + prev_layer.scissor_len,
		scissor_len     = 1,
	}
	append(&GLOB.layers, layer)
	GLOB.curr_layer_index += 1
	log.debug("Added new layer; curr index", GLOB.curr_layer_index)

	scissor := Scissor {
		sub_batch_start = u32(len(GLOB.tmp_sub_batches)),
		bounds = sdl.Rect {
			x = i32(bounds.x * GLOB.dpi_scaling),
			y = i32(bounds.y * GLOB.dpi_scaling),
			w = i32(bounds.width * GLOB.dpi_scaling),
			h = i32(bounds.height * GLOB.dpi_scaling),
		},
	}
	append(&GLOB.scissors, scissor)
	return &GLOB.layers[GLOB.curr_layer_index]
}

// Sets up renderer to begin upload to the GPU. Returns starting `Layer` to begin processing primitives for.
begin :: proc(bounds: Rectangle) -> ^Layer {
	// Cleanup
	clear_global()

	// Begin new layer + start a new scissor
	scissor := Scissor {
		bounds = sdl.Rect {
			x = i32(bounds.x * GLOB.dpi_scaling),
			y = i32(bounds.y * GLOB.dpi_scaling),
			w = i32(bounds.width * GLOB.dpi_scaling),
			h = i32(bounds.height * GLOB.dpi_scaling),
		},
	}
	append(&GLOB.scissors, scissor)

	layer := Layer {
		bounds      = bounds,
		scissor_len = 1,
	}
	append(&GLOB.layers, layer)
	return &GLOB.layers[GLOB.curr_layer_index]
}

// Render primitives. clear_color is the background fill before any layers are drawn.
end :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window, clear_color: Color = DFT_CLEAR_COLOR) {
	cmd_buffer := sdl.AcquireGPUCommandBuffer(device)
	if cmd_buffer == nil {
		log.panicf("Failed to acquire GPU command buffer: %s", sdl.GetError())
	}

	if GLOB.open_backdrop_layer != nil {
		log.panicf(
			"end() called with open backdrop scope on layer %p; missing end_backdrop",
			GLOB.open_backdrop_layer,
		)
	}

	// Pre-scan: if any layer this frame has a backdrop sub-batch, route the entire frame to
	// source_texture so the bracket can sample the pre-bracket framebuffer without a mid-
	// frame texture copy. Frames without any backdrop hit the existing fast path and never
	// touch the backdrop pipeline's working textures.
	has_backdrop := frame_has_backdrop()

	// Upload primitives to GPU (vertices, indices, SDF prims, and backdrop prims share one
	// copy pass so we pay the BeginGPUCopyPass / EndGPUCopyPass cost once per frame).
	copy_pass := sdl.BeginGPUCopyPass(cmd_buffer)
	upload(device, copy_pass)
	if has_backdrop {
		upload_backdrop_primitives(device, copy_pass)
	}
	sdl.EndGPUCopyPass(copy_pass)

	swapchain_texture: ^sdl.GPUTexture
	width, height: u32
	if !sdl.WaitAndAcquireGPUSwapchainTexture(cmd_buffer, window, &swapchain_texture, &width, &height) {
		log.panicf("Failed to acquire swapchain texture: %s", sdl.GetError())
	}

	if swapchain_texture == nil {
		// Window is minimized or not visible — submit and skip this frame
		if !sdl.SubmitGPUCommandBuffer(cmd_buffer) {
			log.panicf("Failed to submit GPU command buffer (minimized window): %s", sdl.GetError())
		}
		return
	}

	render_texture := swapchain_texture
	if has_backdrop {
		ensure_backdrop_textures(device, sdl.GetGPUSwapchainTextureFormat(device, window), width, height)
		render_texture = GLOB.backdrop.source_texture
	}

	// Premultiply clear color: the blend state is ONE, ONE_MINUS_SRC_ALPHA (premultiplied),
	// so the clear color must also be premultiplied for correct background compositing.
	clear_color_straight := color_to_f32(clear_color)
	clear_alpha := clear_color_straight[3]
	clear_color_f32 := [4]f32 {
		clear_color_straight[0] * clear_alpha,
		clear_color_straight[1] * clear_alpha,
		clear_color_straight[2] * clear_alpha,
		clear_alpha,
	}

	// Draw layers. One render pass per layer; sub-batches draw in submission order within each scissor.
	for &layer, index in GLOB.layers {
		draw_layer(device, window, cmd_buffer, render_texture, width, height, clear_color_f32, &layer)
	}

	// When we rendered into source_texture, copy it to the swapchain. Single
	// CopyGPUTextureToTexture call per frame, only when backdrop content was present.
	if has_backdrop {
		copy_pass := sdl.BeginGPUCopyPass(cmd_buffer)
		sdl.CopyGPUTextureToTexture(
			copy_pass,
			sdl.GPUTextureLocation{texture = GLOB.backdrop.source_texture},
			sdl.GPUTextureLocation{texture = swapchain_texture},
			width,
			height,
			1,
			false,
		)
		sdl.EndGPUCopyPass(copy_pass)
	}

	if !sdl.SubmitGPUCommandBuffer(cmd_buffer) {
		log.panicf("Failed to submit GPU command buffer: %s", sdl.GetError())
	}
}

// Open a backdrop scope on `layer`. All subsequent draws on `layer` until the matching
// `end_backdrop` must be backdrop primitives (currently only `backdrop_blur`). Non-backdrop
// draws inside a scope, or backdrop draws outside one, panic.
//
// Bracket scheduling: each scope produces one bracket at render time. Within the scope,
// per-sigma sub-batch coalescing still applies (two contiguous backdrop_blur calls with
// the same sigma share an instanced composite draw and a single H+V blur pass pair).
//
// Multiple begin/end pairs per layer are allowed: each pair is its own bracket, and
// non-backdrop draws between pairs render in their submission position relative to the
// brackets. Use this for layered frost effects.
begin_backdrop :: proc(layer: ^Layer) {
	if GLOB.open_backdrop_layer != nil {
		log.panicf("begin_backdrop called while a scope is already open on layer %p", GLOB.open_backdrop_layer)
	}
	GLOB.open_backdrop_layer = layer
}

// Close the backdrop scope opened by `begin_backdrop`. Must be called on the same layer that
// the scope was opened on; the layer pointer mismatch is a hard error rather than a silent
// recovery to surface integration bugs early.
end_backdrop :: proc(layer: ^Layer) {
	if GLOB.open_backdrop_layer != layer {
		log.panicf("end_backdrop on wrong layer (open=%p, ended=%p)", GLOB.open_backdrop_layer, layer)
	}
	GLOB.open_backdrop_layer = nil
}

// Convenience wrapper for the common case of a backdrop scope tied to a block. Use with
// defer-style block scoping:
//
//	{
//		draw.backdrop_scope(layer)
//		draw.backdrop_blur(layer, ...)
//	}  // end_backdrop fires automatically
@(deferred_in = end_backdrop)
backdrop_scope :: #force_inline proc(layer: ^Layer) {
	begin_backdrop(layer)
}

// ---------------------------------------------------------------------------------------------------------------------
// ----- Sub-batch dispatch ------------
// ---------------------------------------------------------------------------------------------------------------------

// Append a new sub-batch or extend the last one if same kind and contiguous.
//
// `gaussian_sigma` is only consulted for kind == .Backdrop; two .Backdrop sub-batches with
// different sigmas cannot coalesce because they require separate H+V blur passes in the
// bracket scheduler. Float equality is intentional — user-supplied literal sigmas (e.g.
// `sigma = 12`) produce bit-identical floats, and the worst case for two sigmas that differ
// only by a ulp is one extra pass pair (correct, just slightly suboptimal).
//INTERNAL
append_or_extend_sub_batch :: proc(
	scissor: ^Scissor,
	layer: ^Layer,
	kind: Sub_Batch_Kind,
	offset: u32,
	count: u32,
	texture_id: Texture_Id = INVALID_TEXTURE,
	sampler: Sampler_Preset = DFT_SAMPLER,
	gaussian_sigma: f32 = 0,
) {
	// Scope contract: backdrops only inside a scope, non-backdrops only outside.
	in_scope := GLOB.open_backdrop_layer == layer
	if kind == .Backdrop && !in_scope {
		log.panic("backdrop draw outside begin_backdrop / end_backdrop scope")
	}
	if kind != .Backdrop && in_scope {
		log.panicf("non-backdrop draw of kind %v inside backdrop scope on layer %p", kind, layer)
	}

	if scissor.sub_batch_len > 0 {
		last := &GLOB.tmp_sub_batches[scissor.sub_batch_start + scissor.sub_batch_len - 1]
		if last.kind == kind &&
		   kind != .Text &&
		   last.offset + last.count == offset &&
		   last.texture_id == texture_id &&
		   last.sampler == sampler &&
		   (kind != .Backdrop || last.gaussian_sigma == gaussian_sigma) {
			last.count += count
			return
		}
	}
	append(
		&GLOB.tmp_sub_batches,
		Sub_Batch {
			kind = kind,
			offset = offset,
			count = count,
			texture_id = texture_id,
			sampler = sampler,
			gaussian_sigma = gaussian_sigma,
		},
	)
	scissor.sub_batch_len += 1
	layer.sub_batch_len += 1
}

// ---------------------------------------------------------------------------------------------------------------------
// ----- Buffer ------------
// ---------------------------------------------------------------------------------------------------------------------

//INTERNAL
Buffer :: struct {
	gpu:      ^sdl.GPUBuffer,
	transfer: ^sdl.GPUTransferBuffer,
	size:     u32,
}

//INTERNAL
@(require_results)
create_buffer :: proc(
	device: ^sdl.GPUDevice,
	size: u32,
	gpu_usage: sdl.GPUBufferUsageFlags,
) -> (
	buffer: Buffer,
	ok: bool,
) {
	gpu := sdl.CreateGPUBuffer(device, sdl.GPUBufferCreateInfo{usage = gpu_usage, size = size})
	if gpu == nil {
		log.errorf("Failed to create GPU buffer (size=%d): %s", size, sdl.GetError())
		return buffer, false
	}
	transfer := sdl.CreateGPUTransferBuffer(
		device,
		sdl.GPUTransferBufferCreateInfo{usage = .UPLOAD, size = size},
	)
	if transfer == nil {
		sdl.ReleaseGPUBuffer(device, gpu)
		log.errorf("Failed to create GPU transfer buffer (size=%d): %s", size, sdl.GetError())
		return buffer, false
	}
	return Buffer{gpu, transfer, size}, true
}

//INTERNAL
grow_buffer_if_needed :: proc(
	device: ^sdl.GPUDevice,
	buffer: ^Buffer,
	new_size: u32,
	gpu_usage: sdl.GPUBufferUsageFlags,
) {
	if new_size > buffer.size {
		log.debug("Resizing buffer from", buffer.size, "to", new_size)
		destroy_buffer(device, buffer)
		buffer.gpu = sdl.CreateGPUBuffer(device, sdl.GPUBufferCreateInfo{usage = gpu_usage, size = new_size})
		if buffer.gpu == nil {
			log.panicf("Failed to grow GPU buffer (new_size=%d): %s", new_size, sdl.GetError())
		}
		buffer.transfer = sdl.CreateGPUTransferBuffer(
			device,
			sdl.GPUTransferBufferCreateInfo{usage = .UPLOAD, size = new_size},
		)
		if buffer.transfer == nil {
			log.panicf("Failed to grow GPU transfer buffer (new_size=%d): %s", new_size, sdl.GetError())
		}
		buffer.size = new_size
	}
}

//INTERNAL
destroy_buffer :: proc(device: ^sdl.GPUDevice, buffer: ^Buffer) {
	sdl.ReleaseGPUBuffer(device, buffer.gpu)
	sdl.ReleaseGPUTransferBuffer(device, buffer.transfer)
}

// ---------------------------------------------------------------------------------------------------------------------
// ----- Math ------------
// ---------------------------------------------------------------------------------------------------------------------

//INTERNAL
ortho_rh :: proc(left: f32, right: f32, bottom: f32, top: f32, near: f32, far: f32) -> matrix[4, 4]f32 {
	return matrix[4, 4]f32{
		2.0 / (right - left), 0.0, 0.0, -(right + left) / (right - left),
		0.0, 2.0 / (top - bottom), 0.0, -(top + bottom) / (top - bottom),
		0.0, 0.0, -2.0 / (far - near), -(far + near) / (far - near),
		0.0, 0.0, 0.0, 1.0,
	}
}

// 2x3 affine transform for 2D pivot-rotation.
// Used internally by rotation-aware drawing procs.
Transform_2D :: struct {
	m00, m01: f32, // row 0: rotation/scale
	m10, m11: f32, // row 1: rotation/scale
	tx, ty:   f32, // translation
}

// Build a pivot-rotation transform.
//
// Semantics (raylib-style):
//   The point whose local coordinates equal `origin` lands at `pos` in world space.
//   The rest of the shape rotates around that pivot.
//
// Formula:  p_world = pos + R(θ) · (p_local - origin)
//
// Parameters:
//   pos          – world-space position where the pivot lands.
//   origin       – pivot point in local space (measured from the shape's natural reference point).
//   rotation_deg – rotation in degrees, counter-clockwise.
//
build_pivot_rotation :: proc(position: Vec2, origin: Vec2, rotation_deg: f32) -> Transform_2D {
	radians := math.to_radians(rotation_deg)
	cos_angle := math.cos(radians)
	sin_angle := math.sin(radians)
	return build_pivot_rotation_sc(position, origin, cos_angle, sin_angle)
}

// Variant of build_pivot_rotation that accepts pre-computed cos/sin values,
// avoiding redundant trigonometry when the caller has already computed them.
build_pivot_rotation_sc :: #force_inline proc(
	position: Vec2,
	origin: Vec2,
	cos_angle, sin_angle: f32,
) -> Transform_2D {
	return Transform_2D {
		m00 = cos_angle,
		m01 = -sin_angle,
		m10 = sin_angle,
		m11 = cos_angle,
		tx = position.x - (cos_angle * origin.x - sin_angle * origin.y),
		ty = position.y - (sin_angle * origin.x + cos_angle * origin.y),
	}
}

// Apply the transform to a local-space point, producing a world-space point.
apply_transform :: #force_inline proc(transform: Transform_2D, point: Vec2) -> Vec2 {
	return {
		transform.m00 * point.x + transform.m01 * point.y + transform.tx,
		transform.m10 * point.x + transform.m11 * point.y + transform.ty,
	}
}

// Fast-path check callers use BEFORE building a transform.
// Returns true if either the origin is non-zero or rotation is non-zero,
// meaning a transform actually needs to be computed.
needs_transform :: #force_inline proc(origin: Vec2, rotation: f32) -> bool {
	return origin != {0, 0} || rotation != 0
}

// ---------------------------------------------------------------------------------------------------------------------
// ----- Anchors ------------
// ---------------------------------------------------------------------------------------------------------------------

// Translate `local` — a point relative to `bounds`'s top-left — into `bounds`'s own coordinate
// space by offsetting it by the rectangle's origin. Unlike the anchor helpers above (which
// return size-relative offsets and ignore `bounds.x`/`bounds.y`), this uses the rectangle's
// position. Handy for drawing inside a sub-region whose rect you were handed — e.g. the
// `bounds` passed to a `Custom_Draw` callback: `local_to_bounds(bounds, {cx, cy})`.
bound :: #force_inline proc(bounds: Rectangle, local: Vec2) -> Vec2 {
	return {bounds.x + local.x, bounds.y + local.y}
}

// Return Vec2 pixel offsets for use as the `origin` parameter of draw calls.
// Composable with normal vector +/- arithmetic.
//
// Text anchor helpers are in text.odin (they depend on measure_text / SDL_ttf).

// Returns uniform radii (all corners the same) as a fraction of the shorter side.
// `roundness` is clamped to [0, 1]; 0 = sharp corners, 1 = fully rounded (stadium or circle).
uniform_radii :: #force_inline proc(rect: Rectangle, roundness: f32) -> Rectangle_Radii {
	cr := min(rect.width, rect.height) * clamp(roundness, 0, 1) * 0.5
	return {cr, cr, cr, cr}
}

//----- Rectangle anchors (origin measured from rectangle's top-left) ----------------------------------

center_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
	return {rectangle.width * 0.5, rectangle.height * 0.5}
}

top_left_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
	return {0, 0}
}

top_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
	return {rectangle.width * 0.5, 0}
}

top_right_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
	return {rectangle.width, 0}
}

left_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
	return {0, rectangle.height * 0.5}
}

right_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
	return {rectangle.width, rectangle.height * 0.5}
}

bottom_left_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
	return {0, rectangle.height}
}

bottom_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
	return {rectangle.width * 0.5, rectangle.height}
}

bottom_right_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
	return {rectangle.width, rectangle.height}
}

//----- Triangle anchors (origin measured from AABB top-left) ----------------------------------

center_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
	bounds_min := Vec2{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
	return (v1 + v2 + v3) / 3 - bounds_min
}

top_left_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
	return {0, 0}
}

top_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
	min_x := min(v1.x, v2.x, v3.x)
	max_x := max(v1.x, v2.x, v3.x)
	return {(max_x - min_x) * 0.5, 0}
}

top_right_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
	min_x := min(v1.x, v2.x, v3.x)
	max_x := max(v1.x, v2.x, v3.x)
	return {max_x - min_x, 0}
}

left_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
	min_y := min(v1.y, v2.y, v3.y)
	max_y := max(v1.y, v2.y, v3.y)
	return {0, (max_y - min_y) * 0.5}
}

right_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
	bounds_min := Vec2{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
	bounds_max := Vec2{max(v1.x, v2.x, v3.x), max(v1.y, v2.y, v3.y)}
	return {bounds_max.x - bounds_min.x, (bounds_max.y - bounds_min.y) * 0.5}
}

bottom_left_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
	min_y := min(v1.y, v2.y, v3.y)
	max_y := max(v1.y, v2.y, v3.y)
	return {0, max_y - min_y}
}

bottom_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
	bounds_min := Vec2{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
	bounds_max := Vec2{max(v1.x, v2.x, v3.x), max(v1.y, v2.y, v3.y)}
	return {(bounds_max.x - bounds_min.x) * 0.5, bounds_max.y - bounds_min.y}
}

bottom_right_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
	bounds_min := Vec2{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
	bounds_max := Vec2{max(v1.x, v2.x, v3.x), max(v1.y, v2.y, v3.y)}
	return bounds_max - bounds_min
}

//----- Procedure groups ----------------------------------

center_of :: proc {
	center_of_rectangle,
	center_of_triangle,
	center_of_text,
}

top_left_of :: proc {
	top_left_of_rectangle,
	top_left_of_triangle,
	top_left_of_text,
}

top_of :: proc {
	top_of_rectangle,
	top_of_triangle,
	top_of_text,
}

top_right_of :: proc {
	top_right_of_rectangle,
	top_right_of_triangle,
	top_right_of_text,
}

left_of :: proc {
	left_of_rectangle,
	left_of_triangle,
	left_of_text,
}

right_of :: proc {
	right_of_rectangle,
	right_of_triangle,
	right_of_text,
}

bottom_left_of :: proc {
	bottom_left_of_rectangle,
	bottom_left_of_triangle,
	bottom_left_of_text,
}

bottom_of :: proc {
	bottom_of_rectangle,
	bottom_of_triangle,
	bottom_of_text,
}

bottom_right_of :: proc {
	bottom_right_of_rectangle,
	bottom_right_of_triangle,
	bottom_right_of_text,
}
