// Crafter.Graphics UI shader contract — shared by every standard UI compute
// shader and intended to be #included by user-authored shaders that want to
// dispatch alongside them. Layouts here are FROZEN: only additive changes
// (using the reserved `flags` bits or `_pad`).

#extension GL_EXT_shader_image_load_formatted : enable
#extension GL_EXT_shader_explicit_arithmetic_types_int16 : enable
#extension GL_EXT_descriptor_heap : enable
#extension GL_EXT_nonuniform_qualifier : enable
#extension GL_EXT_buffer_reference : enable

// ─── bindless heap declarations ─────────────────────────────────────────
// The same heap slot can be read as either uiImages[] (storage image) or
// uiTextures[] (sampled image) depending on which descriptor was written
// at that slot. Samplers live in a separate sampler heap.
layout(descriptor_heap) uniform image2D    uiImages[];
layout(descriptor_heap) uniform texture2D  uiTextures[];
layout(descriptor_heap) uniform sampler    uiSamplers[];

// ─── push-constant header ───────────────────────────────────────────────
// Every UI dispatch's push-constant struct begins with this. User shaders
// MUST embed it as the first member so UIRenderer::FillHeader works.
struct UIDispatchHeader {
    uint  outImage;       // heap slot of the swapchain image (this frame)
    uint  itemBuffer;     // heap slot of the item SSBO
    uvec2 surfaceSize;    // window pixel size
    vec4  clipRectPx;     // (xy, wh) — every standard shader honors this
    uint  itemCount;
    uint  frameIdx;
    uint  flags;          // feature bits — high bit reserved by the renderer
    uint  _pad;           // reserved — keep zeroed
};

// ─── header flag bits ───────────────────────────────────────────────────
// `flags` is otherwise free for user-defined feature bits, but the renderer
// reserves the high bit: UI_FLAG_CLIP is set by UIRenderer::FillHeader when
// the clip rect does not already cover the whole surface. uiResolveScreenPixel
// gates its four clipRectPx compares on this bit so the overwhelmingly common
// full-surface case (clipRect == {0,0,1e9,1e9}) skips them entirely. The
// branch is push-constant-uniform across the dispatch, so it never diverges.
const uint UI_FLAG_CLIP = 0x80000000u;

// ─── standard item structs ──────────────────────────────────────────────
// These match the C++ Crafter::QuadItem / CircleItem / ImageItem / GlyphItem
// byte-for-byte under std430.
struct QuadItem    { vec4 rect; vec4 color; vec4 corners; vec4 outline; };
// rect    = (x, y, w, h) in pixels
// color   = filled body RGBA (premultiplied alpha not assumed)
// corners = per-corner radius in px (TL, TR, BR, BL); 0 = sharp
// outline = (thickness, R, G, B); thickness > 0 paints an outline of given color

struct CircleItem  { vec4 centerRadius; vec4 color; vec4 outline; };
// centerRadius = (cx, cy, radius, _)
// outline.x = thickness (0 = filled), .yzw = outline RGB

struct ImageItem   { vec4 rect; vec4 uv; vec4 tint; uvec4 slots; };
// rect  = (x, y, w, h)
// uv    = (u0, v0, u1, v1) into the source texture
// tint  = multiplied with the sampled color
// slots = (textureHeapSlot, samplerHeapSlot, _, _)

struct GlyphItem   { vec4 rect; vec4 uv; vec4 color; };
// rect  = (x, y, w, h) on screen
// uv    = (u0, v0, u1, v1) into the SDF font atlas
// color = glyph color (alpha modulated by SDF)

// ─── SSBO heap views ────────────────────────────────────────────────────
// One declaration per item type; each shader uses the one matching its
// dispatch. Indexed by hdr.itemBuffer.
layout(descriptor_heap, std430) readonly buffer UIQuadBuf   { QuadItem   items[]; }   uiQuadHeap[];
layout(descriptor_heap, std430) readonly buffer UICircleBuf { CircleItem items[]; }   uiCircleHeap[];
layout(descriptor_heap, std430) readonly buffer UIImageBuf  { ImageItem  items[]; }   uiImageHeap[];
layout(descriptor_heap, std430) readonly buffer UIGlyphBuf  { GlyphItem  items[]; }   uiGlyphHeap[];


// ──── Driver workaround: per-member SSBO load ────────────────────────────
// `UIItem it = itemHeap[idx].items[i]` emits an OpLoad of a composite type
// from a descriptor-heap'd SSBO, which crashes the GPU on the NVIDIA
// VK_EXT_descriptor_heap path (verified with a 1-float struct repro).
// Reading individual members works (each becomes OpAccessChain + scalar
// OpLoad). LoadItem reassembles the struct member-by-member into a local;
// the rest of the shader then operates on a regular local var.

ImageItem LoadImageItem(uint heap, uint i) {
    ImageItem it;
    it.rect           = uiImageHeap[heap].items[i].rect;
    it.uv           = uiImageHeap[heap].items[i].uv;
    it.tint           = uiImageHeap[heap].items[i].tint;
    it.slots           = uiImageHeap[heap].items[i].slots;
    return it;
}

GlyphItem LoadGlpyhtem(uint heap, uint i) {
    GlyphItem it;
    it.rect           = uiGlyphHeap[heap].items[i].rect;
    it.uv             = uiGlyphHeap[heap].items[i].uv;
    it.color           = uiGlyphHeap[heap].items[i].color;
    return it;
}

CircleItem LoadCircleItem(uint heap, uint i) {
    CircleItem it;
    it.centerRadius           = uiCircleHeap[heap].items[i].centerRadius;
    it.color           = uiCircleHeap[heap].items[i].color;
    it.outline           = uiCircleHeap[heap].items[i].outline;
    return it;
}

QuadItem LoadQuadItem(uint heap, uint i) {
    QuadItem it;
    it.rect           = uiQuadHeap[heap].items[i].rect;
    it.color           = uiQuadHeap[heap].items[i].color;
    it.corners           = uiQuadHeap[heap].items[i].corners;
    it.outline           = uiQuadHeap[heap].items[i].outline;
    return it;
}

// ─── pixel-tile dispatch model ─────────────────────────────────────────
// Standard shaders dispatch one workgroup per 8×8 screen tile. Each thread
// owns ONE pixel and iterates ALL items in order, accumulating the result
// in a local register, then stores once at the end. This guarantees correct
// z-order within a single dispatch (no inter-workgroup race on imageLoad/
// imageStore) and gives the user simple semantics: "items render in array
// order, later items overdraw earlier ones".
//
// Caller dispatches `(ceil(W/8), ceil(H/8), 1)` — no need to know the max
// item size.

// Returns the screen pixel and validates against the surface and clip rect.
bool uiResolveScreenPixel(UIDispatchHeader hdr, out ivec2 screenPx) {
    uvec2 px = gl_GlobalInvocationID.xy;
    if (px.x >= hdr.surfaceSize.x || px.y >= hdr.surfaceSize.y) return false;
    if ((hdr.flags & UI_FLAG_CLIP) != 0u) {
        if (float(px.x) < hdr.clipRectPx.x || float(px.y) < hdr.clipRectPx.y) return false;
        if (float(px.x) >= hdr.clipRectPx.x + hdr.clipRectPx.z) return false;
        if (float(px.y) >= hdr.clipRectPx.y + hdr.clipRectPx.w) return false;
    }
    screenPx = ivec2(px);
    return true;
}

// Per-category clip test for the fused kernel (issue #47). The fused shader
// loads/stores the destination image once for the whole surface but each
// category carries its own clip rect, so the clip can only gate compositing —
// not the shared load/store. This mirrors the integer-pixel comparison
// uiResolveScreenPixel does (compare the pixel's top-left, not its center), so
// a fused category is pixel-identical to its standalone Dispatch* pass.
bool uiPixelInClipRect(uvec2 px, vec4 clip) {
    if (float(px.x) < clip.x || float(px.y) < clip.y) return false;
    if (float(px.x) >= clip.x + clip.z) return false;
    if (float(px.y) >= clip.y + clip.w) return false;
    return true;
}

// Non-premultiplied "src over dst" blend. Both operands and result are
// straight-alpha vec4. Use this when iterating items in a loop with a local
// accumulator.
vec4 uiBlendOver(vec4 dst, vec4 src) {
    float a = clamp(src.a, 0.0, 1.0);
    vec3 outRGB = mix(dst.rgb, src.rgb, a);
    float outA  = a + dst.a * (1.0 - a);
    return vec4(outRGB, outA);
}

// SDF for a rounded rect with per-corner radius. p is the point relative to
// the rect's center; halfSize is the rect half-extents; r is per-corner
// (TL, TR, BR, BL). Returns signed distance (negative inside).
float uiSdRoundRect(vec2 p, vec2 halfSize, vec4 r) {
    // Pick the radius for the quadrant p is in.
    r.xy = (p.x > 0.0) ? r.zy : r.wx;   // pick TR/BR vs TL/BL
    r.x  = (p.y > 0.0) ? r.x  : r.y;
    vec2 q = abs(p) - halfSize + r.x;
    return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r.x;
}

// ─── shared-memory cooperative tile culling ─────────────────────────────
// Each shader still dispatches one 8×8 workgroup per screen tile, but rather
// than have all 64 threads walk the FULL item list — re-reading every item
// from the SSBO 64× and running the per-pixel reject for items that never
// touch the tile — the workgroup streams the list in chunks of 64. Each
// thread loads ONE item, tests its AABB against the whole tile, and the
// survivors are compacted (in original array order) into a shared list that
// every thread then iterates for its own pixel.
//
// This preserves draw order exactly: chunks run in array order and the
// in-chunk compaction is a stable in-order scan, so the per-pixel inner loop
// still sees items in buffer order (later overdraws earlier). It also drops
// SSBO traffic ~64× (each item read once per workgroup, not once per pixel)
// and shrinks the inner loop to only items that actually overlap the tile.
// The inner loop body is left byte-for-byte identical to the unculled path,
// so output is pixel-identical — the cull only decides which items reach it.

const uint UI_TILE_SIZE = 8u;   // matches local_size_x / local_size_y
const uint UI_CHUNK      = 64u; // = workgroup size (8×8); one item per thread

// This workgroup's tile bounds in pixels.
void uiTileBounds(out vec2 tileMin, out vec2 tileMax) {
    tileMin = vec2(gl_WorkGroupID.xy * UI_TILE_SIZE);
    tileMax = tileMin + vec2(float(UI_TILE_SIZE));
}

// Conservative AABB-vs-tile overlap. MUST be a superset of "some pixel in the
// tile passes the in-loop per-pixel reject" so an item a pixel needs is never
// dropped. Pixel centers live in (tileMin, tileMax), so testing the full tile
// box only ever keeps more, never fewer.
bool uiAabbOverlapsTile(vec2 lo, vec2 hi, vec2 tileMin, vec2 tileMax) {
    return lo.x < tileMax.x && hi.x > tileMin.x
        && lo.y < tileMax.y && hi.y > tileMin.y;
}
