Compare commits
10 Commits
e8ffa28de3
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 2acbf51637 | |||
| 61d94265dd | |||
| 962a814b84 | |||
| 08f8a9d0b5 | |||
| f2da356580 | |||
| 6ac41b22f8 | |||
| a69b1e8199 | |||
| 6a0a984310 | |||
| 0ecd93a334 | |||
| 43f08ed30c |
@@ -75,6 +75,11 @@
|
||||
"command": "odin run draw/examples -debug -out=out/debug/draw-examples -- textures",
|
||||
"cwd": "$ZED_WORKTREE_ROOT",
|
||||
},
|
||||
{
|
||||
"label": "Run draw clay-borders example",
|
||||
"command": "odin run draw/examples -debug -out=out/debug/draw-examples -- clay-borders",
|
||||
"cwd": "$ZED_WORKTREE_ROOT",
|
||||
},
|
||||
{
|
||||
"label": "Run draw gaussian-blur example",
|
||||
"command": "odin run draw/examples -debug -out=out/debug/draw-examples -- gaussian-blur",
|
||||
|
||||
+49
-26
@@ -5,14 +5,27 @@ Clay UI integration.
|
||||
|
||||
## Current state
|
||||
|
||||
The renderer uses a single unified `Core_2D` (`TRIANGLELIST` pipeline) with two submission
|
||||
modes dispatched by a push constant:
|
||||
The renderer uses a single unified `Core_2D` (`TRIANGLELIST` pipeline) with three submission
|
||||
modes dispatched by a push constant. The split is by **vertex coordinate space**, not by what the
|
||||
fragment shader does — modes 0 and 2 share the same fragment-shader path (kind 0) and differ only
|
||||
in whether the vertex shader applies `dpi_scale` to incoming positions:
|
||||
|
||||
- **Mode 0 (Tessellated):** Vertex buffer contains real geometry. Used for text (indexed draws into
|
||||
SDL_ttf atlas textures), single-pixel points (`tess.pixel`), arbitrary user geometry
|
||||
(`tess.triangle`, `tess.triangle_aa`, `tess.triangle_lines`, `tess.triangle_fan`,
|
||||
`tess.triangle_strip`), and any raw vertex geometry submitted via `prepare_shape`. The fragment
|
||||
shader premultiplies the texture sample (`t.rgb *= t.a`) and computes `out = color * t`.
|
||||
- **Mode 0 (Tessellated):** Vertex buffer contains real geometry in _logical_ pixels. The vertex
|
||||
shader scales by `dpi_scale` before projecting. Used for single-pixel points (`tess.pixel`),
|
||||
arbitrary user geometry (`tess.triangle`, `tess.triangle_aa`, `tess.triangle_lines`,
|
||||
`tess.triangle_fan`, `tess.triangle_strip`), and any raw vertex geometry submitted via
|
||||
`prepare_shape`. The fragment shader premultiplies the texture sample (`t.rgb *= t.a`) and
|
||||
computes `out = color * t`.
|
||||
|
||||
- **Mode 2 (Text):** Vertex buffer contains real geometry in _physical_ pixels. SDL_ttf's GPU text
|
||||
engine lays out glyphs in physical pixels (`TTF_SetFontSizeDPI` is called with `72 * dpi_scale`),
|
||||
so `prepare_text` adds an anchor offset that is itself snapped to integer physical pixels for
|
||||
atlas-aligned bilinear sampling, then writes vertices straight to the buffer. The vertex shader
|
||||
must NOT rescale these vertices. Same fragment-shader kind as Tessellated; same indexed draws
|
||||
into SDL_ttf atlas textures; the only difference is the coordinate space of the input. Mode 2
|
||||
exists because integer-physical-pixel snapping is the load-bearing property of crisp glyph
|
||||
rendering and CPU is the only place that snap can happen once-per-text-element instead of
|
||||
per-vertex.
|
||||
|
||||
- **Mode 1 (SDF):** A static 6-vertex unit-quad buffer is drawn instanced, with per-primitive
|
||||
`Core_2D_Primitive` structs (96 bytes each) uploaded each frame to a GPU storage buffer. The vertex
|
||||
@@ -43,8 +56,8 @@ in the pipeline plan below for the full cliff/margin analysis and SBC architectu
|
||||
The fragment shader's estimated peak footprint is ~22–26 fp32 VGPRs (~16–22 fp16 VGPRs on architectures
|
||||
with native mediump) via manual live-range analysis. The dominant peak is the Ring_Arc kind path
|
||||
(wedge normals + inner/outer radii + dot-product temporaries live simultaneously with carried state
|
||||
like `f_color`, `f_uv_rect`/`f_effects`, and `half_size`). RRect is 1–2 regs lower (`corner_radii` vec4
|
||||
replaces the separate inner/outer + normal pairs). NGon and Ellipse are lighter still. Real compilers
|
||||
like `f_color`, `f_uv_rect`/`f_effects`, and `half_size_ppx`). RRect is 1–2 regs lower
|
||||
(`corner_radii_ppx` vec4 replaces the separate inner/outer + normal pairs). NGon and Ellipse are lighter still. Real compilers
|
||||
apply live-range coalescing, mediump-to-fp16 promotion, and rematerialization that typically shave
|
||||
2–4 regs from hand-counted estimates — the conservative 26-reg upper bound is expected to compile
|
||||
down to within the 24-register budget, but this must be verified with `malioc` (see "Verifying
|
||||
@@ -432,22 +445,32 @@ our design:
|
||||
|
||||
### Main pipeline: SDF + tessellated (unified)
|
||||
|
||||
The main pipeline serves two submission modes through a single `TRIANGLELIST` pipeline and a single
|
||||
vertex input layout, distinguished by a `mode` field in the `Vertex_Uniforms_2D` push constant
|
||||
(`Core_2D_Mode.Tessellated = 0`, `Core_2D_Mode.SDF = 1`), pushed per draw call via `push_globals`. The
|
||||
vertex shader branches on this uniform to select the tessellated or SDF code path.
|
||||
The main pipeline serves three submission modes through a single `TRIANGLELIST` pipeline and a
|
||||
single vertex input layout, distinguished by a `mode` field in the `Vertex_Uniforms_2D` push
|
||||
constant (`Core_2D_Mode.Tessellated = 0`, `Core_2D_Mode.SDF = 1`, `Core_2D_Mode.Text = 2`), pushed
|
||||
per draw call via `push_globals`. The vertex shader branches on this uniform to select the
|
||||
appropriate code path.
|
||||
|
||||
- **Tessellated mode** (`mode = 0`): direct vertex buffer with explicit geometry. Used for text
|
||||
(SDL_ttf atlas sampling), triangles, triangle fans/strips, single-pixel points, and any
|
||||
user-provided raw vertex geometry.
|
||||
- **Tessellated mode** (`mode = 0`): direct vertex buffer with explicit geometry in _logical_
|
||||
pixels. Vertex shader scales positions by `dpi_scale`. Used for triangles, triangle fans/strips,
|
||||
single-pixel points, and any user-provided raw vertex geometry.
|
||||
- **SDF mode** (`mode = 1`): shared unit-quad vertex buffer + GPU storage buffer of
|
||||
`Core_2D_Primitive` structs, drawn instanced. Used for all shapes with closed-form signed distance
|
||||
functions.
|
||||
functions. `Core_2D_Primitive.bounds` is in logical pixels; the vertex shader scales by
|
||||
`dpi_scale`.
|
||||
- **Text mode** (`mode = 2`): direct vertex buffer with explicit geometry in _physical_ pixels.
|
||||
Vertex shader does NOT scale. Used for SDL_ttf atlas sampling. The CPU-side anchor snap to
|
||||
integer physical pixels (`prepare_text`/`prepare_text_transformed`) is what produces crisp glyphs
|
||||
— sub-pixel anchors blur via the bilinear sampler. Mode 2 shares the fragment-shader path with
|
||||
Tessellated (kind 0), so the only divergence between text and shape rasterization is the vertex
|
||||
shader's `* dpi_scale` step.
|
||||
|
||||
Both modes use the same fragment shader. The fragment shader checks `Shape_Kind` (low byte of
|
||||
`Core_2D_Primitive.flags`): kind 0 (`Solid`) is the tessellated path, which premultiplies the texture
|
||||
sample and computes `out = color * t`; kinds 1–4 dispatch to one of four SDF functions (RRect, NGon,
|
||||
Ellipse, Ring_Arc) and apply gradient/texture/outline/solid color based on `Shape_Flags` bits.
|
||||
All three modes use the same fragment shader. Modes 0 (Tessellated) and 2 (Text) take the same
|
||||
fragment-shader path (kind 0), which premultiplies the texture sample and computes `out = color * t`;
|
||||
they differ only in the vertex shader (whether positions are pre-scaled to physical pixels). Mode 1
|
||||
(SDF) checks `Shape_Kind` (low byte of `Core_2D_Primitive.flags`): kinds 1–4 dispatch to one of four
|
||||
SDF functions (RRect, NGon, Ellipse, Ring_Arc) and apply gradient/texture/outline/solid color based
|
||||
on `Shape_Flags` bits.
|
||||
|
||||
#### Why SDF for shapes
|
||||
|
||||
@@ -495,9 +518,9 @@ Compared to encoding per-primitive data in vertex attributes (the "fat vertex" a
|
||||
buffer instancing eliminates the 4–6× data duplication across quad corners. A rounded rectangle costs
|
||||
96 bytes instead of 4 vertices × 60+ bytes = 240+ bytes.
|
||||
|
||||
The tessellated path retains the existing direct vertex buffer layout (20 bytes/vertex, no storage
|
||||
buffer access). The vertex shader branch on `mode` (push constant) is warp-uniform — every invocation
|
||||
in a draw call has the same mode — so it is effectively free on all modern GPUs.
|
||||
The tessellated and text paths retain the existing direct vertex buffer layout (20 bytes/vertex, no
|
||||
storage buffer access). The vertex shader branch on `mode` (push constant) is warp-uniform — every
|
||||
invocation in a draw call has the same mode — so it is effectively free on all modern GPUs.
|
||||
|
||||
#### Shape kinds and SDF dispatch
|
||||
|
||||
@@ -719,7 +742,7 @@ Backdrop_Marker :: struct {
|
||||
sigma: f32,
|
||||
tint: Color,
|
||||
radii: Rectangle_Radii,
|
||||
feather_px: f32,
|
||||
feather_ppx: f32,
|
||||
}
|
||||
```
|
||||
|
||||
@@ -762,7 +785,7 @@ Core_2D_Primitive :: struct {
|
||||
flags: u32, // 20: low byte = Shape_Kind, bits 8+ = Shape_Flags
|
||||
rotation_sc: u32, // 24: packed f16 pair (sin, cos). Requires .Rotated flag.
|
||||
_pad: f32, // 28: reserved for future use
|
||||
params: Shape_Params, // 32: per-kind params union (half_feather, radii, etc.) (32 bytes)
|
||||
params: Shape_Params, // 32: per-kind params union (half_feather_ppx, radii_ppx, etc.) (32 bytes)
|
||||
uv_rect: [4]f32, // 64: texture UV coordinates. Read when .Textured.
|
||||
effects: Gradient_Outline, // 80: gradient and/or outline parameters (16 bytes).
|
||||
}
|
||||
|
||||
+39
-24
@@ -487,10 +487,10 @@ MAX_GAUSSIAN_BLUR_KERNEL_PAIRS :: 32
|
||||
// pipeline rather than tacked onto this one as a flag bit.
|
||||
//INTERNAL
|
||||
Gaussian_Blur_Primitive :: struct {
|
||||
bounds: [4]f32, // 0: 16 — world-space quad (min_xy, max_xy)
|
||||
radii: [4]f32, // 16: 16 — per-corner radii in physical pixels (BR, TR, BL, TL)
|
||||
half_size: [2]f32, // 32: 8 — RRect half extents (physical px)
|
||||
half_feather: f32, // 40: 4 — feather_px * 0.5 (SDF anti-aliasing)
|
||||
bounds: [4]f32, // 0: 16 — world-space quad (min_xy, max_xy) in logical px
|
||||
radii_ppx: [4]f32, // 16: 16 — per-corner radii (BR, TR, BL, TL)
|
||||
half_size_ppx: [2]f32, // 32: 8 — RRect half extents
|
||||
half_feather_ppx: f32, // 40: 4 — feather_ppx * 0.5 (SDF anti-aliasing)
|
||||
color: Color, // 44: 4 — tint, packed RGBA u8x4
|
||||
}
|
||||
#assert(size_of(Gaussian_Blur_Primitive) == 48)
|
||||
@@ -684,7 +684,13 @@ upload_backdrop_primitives :: proc(device: ^sdl.GPUDevice, pass: ^sdl.GPUCopyPas
|
||||
sdl.GPUBufferUsageFlags{.GRAPHICS_STORAGE_READ},
|
||||
)
|
||||
|
||||
prim_array := sdl.MapGPUTransferBuffer(device, GLOB.backdrop.primitive_buffer.transfer, false)
|
||||
// cycle=true: this is a persistent per-frame streaming transfer buffer. The previous
|
||||
// frame's UploadToGPUBuffer is almost certainly still in flight when we map here
|
||||
// (allowedFramesInFlight defaults to 2 on Metal). Without cycling, the CPU memcpy below
|
||||
// races the GPU's blit read on the same MTLBuffer.contents. Cycling rebinds the
|
||||
// container's active internal buffer to an unbound one (or allocates a new one) — O(1)
|
||||
// in steady state, no fence wait. See SDL_gpu_metal.m's METAL_INTERNAL_PrepareBufferForWrite.
|
||||
prim_array := sdl.MapGPUTransferBuffer(device, GLOB.backdrop.primitive_buffer.transfer, true)
|
||||
if prim_array == nil {
|
||||
log.panicf("Failed to map backdrop primitive transfer buffer: %s", sdl.GetError())
|
||||
}
|
||||
@@ -759,11 +765,14 @@ compute_backdrop_group_work_region :: proc(
|
||||
max_x += halo_logical
|
||||
max_y += halo_logical
|
||||
|
||||
// Convert to physical pixels and clamp to swapchain bounds.
|
||||
phys_min_x := math.max(min_x * dpi, 0)
|
||||
phys_min_y := math.max(min_y * dpi, 0)
|
||||
phys_max_x := math.min(max_x * dpi, f32(swapchain_width))
|
||||
phys_max_y := math.min(max_y * dpi, f32(swapchain_height))
|
||||
// Clamp the min corner to 0, but let the max corner's 6σ halo extend past the swapchain edge
|
||||
// into the working texture's unused area (at factor > 1), capped to the texture extent. Keeps
|
||||
// the composite's bilinear upsample off the unwritten texels just past a clamped edge.
|
||||
downsample_factor := compute_backdrop_downsample_factor(sigma_logical)
|
||||
phys_min_x := max(min_x * dpi, 0)
|
||||
phys_min_y := max(min_y * dpi, 0)
|
||||
phys_max_x := min(max_x * dpi, f32(swapchain_width * downsample_factor))
|
||||
phys_max_y := min(max_y * dpi, f32(swapchain_height * downsample_factor))
|
||||
|
||||
if phys_max_x <= phys_min_x || phys_max_y <= phys_min_y do return 0, 0, 0, 0
|
||||
|
||||
@@ -862,12 +871,18 @@ run_backdrop_bracket :: proc(
|
||||
working_w := (region_w + downsample_factor - 1) / downsample_factor
|
||||
working_h := (region_h + downsample_factor - 1) / downsample_factor
|
||||
|
||||
// Working textures are sized at min factor (2). At factor=4 we have only half the texture
|
||||
// area available in each axis. Clamp to the texture extent for either case.
|
||||
wt_w := pipeline.cached_width / downsample_factor
|
||||
wt_h := pipeline.cached_height / downsample_factor
|
||||
if working_x + working_w > wt_w do working_w = wt_w - working_x
|
||||
if working_y + working_h > wt_h do working_h = wt_h - working_y
|
||||
// Clamp to the full texture extent (not cached/factor): the working textures are full
|
||||
// swapchain res, so a factor-N group's halo can spill into the unused remainder. Writing it
|
||||
// keeps the composite's bilinear upsample off unwritten texels at the right/bottom edge.
|
||||
texture_width := pipeline.cached_width
|
||||
texture_height := pipeline.cached_height
|
||||
// Skip fully off-screen groups; also guards the unsigned clamps below from underflow.
|
||||
if working_x >= texture_width || working_y >= texture_height {
|
||||
i = group_end
|
||||
continue
|
||||
}
|
||||
if working_x + working_w > texture_width do working_w = texture_width - working_x
|
||||
if working_y + working_h > texture_height do working_h = texture_height - working_y
|
||||
if working_w == 0 || working_h == 0 {
|
||||
i = group_end
|
||||
continue
|
||||
@@ -1070,7 +1085,7 @@ run_backdrop_bracket :: proc(
|
||||
build_backdrop_primitive :: proc(
|
||||
rect: Rectangle,
|
||||
radii: Rectangle_Radii,
|
||||
feather_px: f32,
|
||||
feather_ppx: f32,
|
||||
) -> Gaussian_Blur_Primitive {
|
||||
max_radius := min(rect.width, rect.height) * 0.5
|
||||
clamped_top_left := clamp(radii.top_left, 0, max_radius)
|
||||
@@ -1078,8 +1093,8 @@ build_backdrop_primitive :: proc(
|
||||
clamped_bottom_right := clamp(radii.bottom_right, 0, max_radius)
|
||||
clamped_bottom_left := clamp(radii.bottom_left, 0, max_radius)
|
||||
|
||||
half_feather := feather_px * 0.5
|
||||
padding := half_feather / GLOB.dpi_scaling
|
||||
half_feather_ppx := feather_ppx * 0.5
|
||||
padding := half_feather_ppx / GLOB.dpi_scaling
|
||||
dpi_scale := GLOB.dpi_scaling
|
||||
|
||||
half_width := rect.width * 0.5
|
||||
@@ -1098,14 +1113,14 @@ build_backdrop_primitive :: proc(
|
||||
// (p.x > 0) ? r.xy : r.zw picks right-vs-left half
|
||||
// then (p.y > 0) ? rxy.x : rxy.y picks bottom-vs-top within that half
|
||||
// So slot 0 = bottom-right, slot 1 = top-right, slot 2 = bottom-left, slot 3 = top-left.
|
||||
radii = {
|
||||
radii_ppx = {
|
||||
clamped_bottom_right * dpi_scale,
|
||||
clamped_top_right * dpi_scale,
|
||||
clamped_bottom_left * dpi_scale,
|
||||
clamped_top_left * dpi_scale,
|
||||
},
|
||||
half_size = {half_width * dpi_scale, half_height * dpi_scale},
|
||||
half_feather = half_feather,
|
||||
half_size_ppx = {half_width * dpi_scale, half_height * dpi_scale},
|
||||
half_feather_ppx = half_feather_ppx,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1162,9 +1177,9 @@ backdrop_blur :: proc(
|
||||
gaussian_sigma: f32,
|
||||
tint: Color = DFT_TINT,
|
||||
radii: Rectangle_Radii = {},
|
||||
feather_px: f32 = DFT_FEATHER_PX,
|
||||
feather_ppx: f32 = DFT_FEATHER_PPX,
|
||||
) {
|
||||
prim := build_backdrop_primitive(rect, radii, feather_px)
|
||||
prim := build_backdrop_primitive(rect, radii, feather_ppx)
|
||||
prim.color = tint
|
||||
prepare_backdrop_primitive(layer, prim, gaussian_sigma)
|
||||
}
|
||||
|
||||
+794
@@ -0,0 +1,794 @@
|
||||
// Clay UI integration for the `draw` package.
|
||||
//
|
||||
// All code in this file is dedicated to bridging Clay's render command stream into `draw`'s
|
||||
// primitive/sub-batch pipeline. Nothing outside this file should reference the `clay` package
|
||||
// directly; everything Clay-related (types, lifecycle helpers, render-command dispatch, the
|
||||
// border-merge stack, the Clay backdrop bracket walker, the text measure/error callbacks,
|
||||
// and the `Clay_Image_Data` user-facing helper) lives here. `draw.odin`'s lifecycle procs
|
||||
// call `init_clay`, `destroy_clay`, and `clear_clay_per_frame` to drive the bits of state
|
||||
// that necessarily live on the shared `Global` struct.
|
||||
package draw
|
||||
|
||||
import "base:runtime"
|
||||
import "core:c"
|
||||
import "core:log"
|
||||
import "core:strings"
|
||||
import sdl "vendor:sdl3"
|
||||
import sdl_ttf "vendor:sdl3/ttf"
|
||||
|
||||
import clay "../vendor/clay"
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// ----- Lifecycle ------------
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// Allocate the Clay arena, build the merge-candidate stack, hand the arena to Clay, and
|
||||
// register the text-measurement and error callbacks. Called by `init` once `GLOB` has been
|
||||
// populated with the device/window state Clay's callbacks read from.
|
||||
//INTERNAL
|
||||
init_clay :: proc(window: ^sdl.Window, allocator: runtime.Allocator) {
|
||||
min_memory_size: c.size_t = cast(c.size_t)clay.MinMemorySize()
|
||||
GLOB.clay_merge_open_stack = make([dynamic]Clay_Merge_Candidate, 0, 16, allocator = allocator)
|
||||
GLOB.clay_memory = make([^]u8, min_memory_size, allocator = allocator)
|
||||
arena := clay.CreateArenaWithCapacityAndMemory(min_memory_size, GLOB.clay_memory)
|
||||
window_width, window_height: c.int
|
||||
sdl.GetWindowSize(window, &window_width, &window_height)
|
||||
clay.Initialize(arena, {f32(window_width), f32(window_height)}, {handler = clay_error_handler})
|
||||
clay.SetMeasureTextFunction(measure_text_clay, nil)
|
||||
}
|
||||
|
||||
// Free the Clay arena memory allocated in `init_clay`. Called by `destroy`. The merge stack
|
||||
// is left to the package allocator's normal teardown to preserve historical behavior.
|
||||
//INTERNAL
|
||||
destroy_clay :: proc(allocator: runtime.Allocator) {
|
||||
free(GLOB.clay_memory, allocator)
|
||||
}
|
||||
|
||||
// Reset Clay per-frame state: the z-index high-water mark and the border-merge stack.
|
||||
// Called by `clear_global` at the start of every frame.
|
||||
//INTERNAL
|
||||
clear_clay_per_frame :: proc() {
|
||||
GLOB.clay_z_index = 0
|
||||
clear(&GLOB.clay_merge_open_stack)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// ----- Image data (Clay RenderCommandType.Image payload) ------------
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
Clay_Image_Data :: struct {
|
||||
texture_id: Texture_Id,
|
||||
fit: Fit_Mode,
|
||||
tint: Color,
|
||||
}
|
||||
|
||||
clay_image_data :: proc(id: Texture_Id, fit: Fit_Mode = .Stretch, tint: Color = WHITE) -> Clay_Image_Data {
|
||||
return {texture_id = id, fit = fit, tint = tint}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// ----- Callbacks (clay -> draw) ------------
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
@(private = "file")
|
||||
clay_error_handler :: proc "c" (errorData: clay.ErrorData) {
|
||||
context = GLOB.odin_context
|
||||
log.error("Clay error:", errorData.errorType, errorData.errorText)
|
||||
}
|
||||
|
||||
@(private = "file")
|
||||
measure_text_clay :: proc "c" (
|
||||
text: clay.StringSlice,
|
||||
config: ^clay.TextElementConfig,
|
||||
user_data: rawptr,
|
||||
) -> clay.Dimensions {
|
||||
context = GLOB.odin_context
|
||||
text := string(text.chars[:text.length])
|
||||
c_text := strings.clone_to_cstring(text, context.temp_allocator)
|
||||
defer delete(c_text, context.temp_allocator)
|
||||
width, height: c.int
|
||||
if !sdl_ttf.GetStringSize(get_font(config.fontId, config.fontSize), c_text, 0, &width, &height) {
|
||||
log.panicf("Failed to measure text: %s", sdl.GetError())
|
||||
}
|
||||
|
||||
return clay.Dimensions{width = f32(width) / GLOB.dpi_scaling, height = f32(height) / GLOB.dpi_scaling}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// ----- Custom draw + customData envelope ------------
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// Called for each Clay `RenderCommandType.Custom` render command that
|
||||
// `prepare_clay_batch` encounters and which is NOT a levlib-managed variant
|
||||
// (e.g. `Backdrop_Marker`).
|
||||
//
|
||||
// - `layer` is the layer the command belongs to (post-z-index promotion).
|
||||
// - `bounds` is already translated into the active layer's coordinate system
|
||||
// and pre-DPI, matching what the built-in shape procs expect.
|
||||
// - `render_data` is Clay's `CustomRenderData` for the element, exposing
|
||||
// `backgroundColor` and `cornerRadius`. Its `customData` field has been
|
||||
// unwrapped from the `Clay_Custom` envelope: it points at the user's own
|
||||
// data (the value the user wrote into the `rawptr` variant), not at the
|
||||
// `Clay_Custom` itself. If the union was zero-init (no variant set) or
|
||||
// `customData` was originally nil, the callback receives nil.
|
||||
//
|
||||
// The callback must not call `new_layer` or `prepare_clay_batch`.
|
||||
Custom_Draw :: #type proc(layer: ^Layer, bounds: Rectangle, render_data: clay.CustomRenderData)
|
||||
|
||||
ClayBatch :: struct {
|
||||
bounds: Rectangle,
|
||||
cmds: clay.ClayArray(clay.RenderCommand),
|
||||
}
|
||||
|
||||
// Discriminated sum of everything `clay.CustomElementConfig.customData` is allowed to point
|
||||
// at. levlib-defined variants (currently just `Backdrop_Marker`) are recognized by
|
||||
// `prepare_clay_batch` and routed to the appropriate internal path; the `rawptr` variant is
|
||||
// the escape hatch for user-defined custom drawing — `prepare_clay_batch` unwraps it before
|
||||
// invoking `custom_draw` so the callback sees the user's pointer in `render_data.customData`
|
||||
// exactly as if no wrapper were involved.
|
||||
//
|
||||
// Contract: `customData`, when non-nil, MUST point at storage holding a `Clay_Custom`
|
||||
// value. The user owns that storage; its lifetime must span the Clay layout call and the
|
||||
// matching `prepare_clay_batch` call. Pointing `customData` at a bare user struct violates
|
||||
// the contract — the dispatcher will read its first bytes as a union tag and either route
|
||||
// the draw incorrectly or panic on type assertion. There is no recovery path; this is a
|
||||
// strict-discipline API by design.
|
||||
//
|
||||
// Construction notes (Odin implicit-conversion rules):
|
||||
// - Backdrop variant: `bd: Clay_Custom = Backdrop_Marker{...}` works directly.
|
||||
// Variant-to-union conversion is implicit.
|
||||
// - User pointer: `up: Clay_Custom = rawptr(&my_struct)` — the explicit `rawptr(...)` is
|
||||
// required because Odin does not chain `^T -> rawptr -> Clay_Custom` implicitly. A bare
|
||||
// `up: Clay_Custom = &my_struct` is a compile error.
|
||||
Clay_Custom :: union {
|
||||
Backdrop_Marker,
|
||||
rawptr,
|
||||
}
|
||||
|
||||
// Per-primitive parameters for a backdrop blur dispatched through the Clay integration.
|
||||
// Embedded as a `Clay_Custom` variant; `prepare_clay_batch` walks the command stream,
|
||||
// opens/closes a backdrop scope around contiguous backdrop runs, and feeds these to
|
||||
// `backdrop_blur` via `dispatch_clay_backdrop`. The discriminant is the union tag — no
|
||||
// in-band magic field needed (compiler-enforced).
|
||||
Backdrop_Marker :: struct {
|
||||
sigma: f32,
|
||||
tint: Color,
|
||||
radii: Rectangle_Radii,
|
||||
feather_ppx: f32,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// ----- Border-merge stack ------------
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// One entry on the Clay merge stack. Pushed by `dispatch_clay_command` when emitting a
|
||||
// Rectangle or an Image primitive, then popped by a matching Border to retroactively add
|
||||
// the outline. See `try_dispatch_clay_border_merge` for the matching semantics.
|
||||
//INTERNAL
|
||||
Clay_Merge_Candidate :: struct {
|
||||
primitive_index: u32, // Index into `GLOB.tmp_primitives` of the candidate primitive.
|
||||
outer_bounds: Rectangle, // Clay's bounding box — keyed on for the bounds match check.
|
||||
corner_radii: clay.CornerRadius, // Clay's corner radii — also keyed on for the match check.
|
||||
image_data: Clay_Image_Data, // Only read when kind == .Fill_Texture (needed to refit UVs to inner_bounds).
|
||||
kind: Clay_Merge_Candidate_Kind,
|
||||
}
|
||||
|
||||
//INTERNAL
|
||||
Clay_Merge_Candidate_Kind :: enum u8 {
|
||||
// Solid Color brush. Used for Rectangle commands and for the bg primitive of an Image
|
||||
// command that has `backgroundColor.a > 0`. Merge mutation: shrink shape + add outline.
|
||||
Fill_Color,
|
||||
// Texture_Fill brush. Used for the image primitive of an Image command with no bg, where
|
||||
// `fit_params` returned `fit_rect == outer_bounds` (the image fully covers Clay's bounds).
|
||||
// Merge mutation: shrink shape + add outline + refit UV against inner_bounds.
|
||||
Fill_Texture,
|
||||
}
|
||||
|
||||
// Returns true if this Clay render command represents a backdrop primitive — i.e. its
|
||||
// `customData` points at a `Clay_Custom` whose active variant is `Backdrop_Marker`.
|
||||
is_clay_backdrop :: proc(cmd: ^clay.RenderCommand) -> bool {
|
||||
if cmd.commandType != .Custom do return false
|
||||
p := cmd.renderData.custom.customData
|
||||
if p == nil do return false
|
||||
_, ok := (^Clay_Custom)(p).(Backdrop_Marker)
|
||||
return ok
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// ----- Border emission ------------
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// Emit a Clay border drawn INSIDE `bounds` — the outer edge of each side aligns with
|
||||
// `bounds`, the inner edge is `border_width.*` pixels inset. Matches Clay's layout model
|
||||
// (CSS border-box) so the visible element occupies exactly Clay's allocated space.
|
||||
//
|
||||
// The fast path (uniform widths) uses `rectangle()` with the built-in SDF outline, which
|
||||
// always extends outward from the shape it's given — we pre-shrink the shape by
|
||||
// `border_width` so the outline lands precisely at Clay's bounds. The slow path (non-uniform
|
||||
// widths) emits per-side rectangles and per-corner arcs directly, all positioned inside
|
||||
// `bounds`. All-zero widths is a no-op.
|
||||
//
|
||||
// A corner is rounded iff its radius is positive AND both adjacent sides have positive
|
||||
// width. Top corners take their thickness from `border_width.top`, bottom corners from
|
||||
// `border_width.bottom`. When the two widths meeting at a corner differ there is a step at
|
||||
// the side/corner junction (acceptable for the rare mixed-width case).
|
||||
//
|
||||
// When `border_width > corner_radius`, the inner corner clamps to zero (sharp inside, still
|
||||
// rounded outside) — matches CSS-standard behavior.
|
||||
//INTERNAL
|
||||
clay_emit_partial_border :: proc(
|
||||
layer: ^Layer,
|
||||
bounds: Rectangle,
|
||||
border_color: Color,
|
||||
border_width: clay.BorderWidth,
|
||||
corner_radii: clay.CornerRadius,
|
||||
) {
|
||||
// All-zero: nothing to draw.
|
||||
if border_width.top == 0 && border_width.right == 0 && border_width.bottom == 0 && border_width.left == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Convert side widths once (u16 -> f32) and cache for reuse.
|
||||
width_top := f32(border_width.top)
|
||||
width_right := f32(border_width.right)
|
||||
width_bottom := f32(border_width.bottom)
|
||||
width_left := f32(border_width.left)
|
||||
|
||||
// Fast path: all four sides have the same nonzero width. Pre-shrink the shape by the
|
||||
// uniform width so the SDF outline (which always extends outward from the shape) lands
|
||||
// exactly at Clay's `bounds` — the visible border ends up INSIDE Clay's allocation while
|
||||
// the SDF mechanism keeps doing outward outlining. Single SDF primitive, exact curves,
|
||||
// analytical AA.
|
||||
if border_width.left == border_width.top &&
|
||||
border_width.top == border_width.right &&
|
||||
border_width.right == border_width.bottom {
|
||||
uniform_width := width_top
|
||||
inner_bounds := Rectangle {
|
||||
x = bounds.x + uniform_width,
|
||||
y = bounds.y + uniform_width,
|
||||
width = bounds.width - 2 * uniform_width,
|
||||
height = bounds.height - 2 * uniform_width,
|
||||
}
|
||||
inner_radii := Rectangle_Radii {
|
||||
top_left = max(0, corner_radii.topLeft - uniform_width),
|
||||
top_right = max(0, corner_radii.topRight - uniform_width),
|
||||
bottom_right = max(0, corner_radii.bottomRight - uniform_width),
|
||||
bottom_left = max(0, corner_radii.bottomLeft - uniform_width),
|
||||
}
|
||||
rectangle(
|
||||
layer,
|
||||
inner_bounds,
|
||||
BLANK,
|
||||
outline_color = border_color,
|
||||
outline_width = uniform_width,
|
||||
radii = inner_radii,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// A corner is drawn rounded only if its radius is positive AND both adjacent sides are present.
|
||||
top_left_rounded := corner_radii.topLeft > 0 && border_width.top > 0 && border_width.left > 0
|
||||
top_right_rounded := corner_radii.topRight > 0 && border_width.top > 0 && border_width.right > 0
|
||||
bottom_left_rounded := corner_radii.bottomLeft > 0 && border_width.bottom > 0 && border_width.left > 0
|
||||
bottom_right_rounded := corner_radii.bottomRight > 0 && border_width.bottom > 0 && border_width.right > 0
|
||||
|
||||
// Horizontal x-coordinates where the top/bottom side rectangles start/end. When the
|
||||
// adjacent corner is rounded, the side stops at `bounds.x + radius` (where the corner
|
||||
// arc takes over). When not rounded, the side runs to the bounds edge; the perpendicular
|
||||
// side handles the inset to avoid overlap.
|
||||
top_left_x: f32 = top_left_rounded ? bounds.x + corner_radii.topLeft : bounds.x
|
||||
top_right_x: f32 =
|
||||
top_right_rounded ? bounds.x + bounds.width - corner_radii.topRight : bounds.x + bounds.width
|
||||
bottom_left_x: f32 = bottom_left_rounded ? bounds.x + corner_radii.bottomLeft : bounds.x
|
||||
bottom_right_x: f32 =
|
||||
bottom_right_rounded ? bounds.x + bounds.width - corner_radii.bottomRight : bounds.x + bounds.width
|
||||
|
||||
// Vertical y-coordinates where the left/right side rectangles start/end. When the
|
||||
// adjacent corner is rounded, inset by the corner radius. When not rounded, inset by the
|
||||
// adjacent horizontal width — the horizontal side owns the corner area (extending through
|
||||
// it to the bounds edge), so the vertical side starts below it to avoid overdraw of
|
||||
// translucent colors.
|
||||
top_left_y: f32 = top_left_rounded ? bounds.y + corner_radii.topLeft : bounds.y + width_top
|
||||
top_right_y: f32 = top_right_rounded ? bounds.y + corner_radii.topRight : bounds.y + width_top
|
||||
bottom_left_y: f32 =
|
||||
bottom_left_rounded ? bounds.y + bounds.height - corner_radii.bottomLeft : bounds.y + bounds.height - width_bottom
|
||||
bottom_right_y: f32 =
|
||||
bottom_right_rounded ? bounds.y + bounds.height - corner_radii.bottomRight : bounds.y + bounds.height - width_bottom
|
||||
|
||||
// Side rectangles drawn INSIDE `bounds`. Sharp corners, solid fill, no outline. Each
|
||||
// gated on its own width — skipping zero-width sides saves the primitive upload.
|
||||
if border_width.top > 0 {
|
||||
top_side := Rectangle {
|
||||
x = top_left_x,
|
||||
y = bounds.y,
|
||||
width = top_right_x - top_left_x,
|
||||
height = width_top,
|
||||
}
|
||||
rectangle(layer, top_side, border_color)
|
||||
}
|
||||
if border_width.bottom > 0 {
|
||||
bottom_side := Rectangle {
|
||||
x = bottom_left_x,
|
||||
y = bounds.y + bounds.height - width_bottom,
|
||||
width = bottom_right_x - bottom_left_x,
|
||||
height = width_bottom,
|
||||
}
|
||||
rectangle(layer, bottom_side, border_color)
|
||||
}
|
||||
if border_width.left > 0 {
|
||||
left_side := Rectangle {
|
||||
x = bounds.x,
|
||||
y = top_left_y,
|
||||
width = width_left,
|
||||
height = bottom_left_y - top_left_y,
|
||||
}
|
||||
rectangle(layer, left_side, border_color)
|
||||
}
|
||||
if border_width.right > 0 {
|
||||
right_side := Rectangle {
|
||||
x = bounds.x + bounds.width - width_right,
|
||||
y = top_right_y,
|
||||
width = width_right,
|
||||
height = bottom_right_y - top_right_y,
|
||||
}
|
||||
rectangle(layer, right_side, border_color)
|
||||
}
|
||||
|
||||
// Corner arcs (90° quadrants) drawn INSIDE bounds: outer radius matches Clay's
|
||||
// `corner_radii`, inner radius is the outer radius minus the relevant border thickness
|
||||
// (clamped to 0 for thick borders — produces a filled pie slice when border > radius,
|
||||
// matching CSS). Angle convention matches ring(): 0° = +x (right), 90° = +y (down),
|
||||
// 180° = -x (left), 270° = -y (up).
|
||||
if top_left_rounded {
|
||||
radius := corner_radii.topLeft
|
||||
inner_radius := max(0, radius - width_top)
|
||||
center := Vec2{bounds.x + radius, bounds.y + radius}
|
||||
ring(layer, center, inner_radius, radius, border_color, start_angle = 180, end_angle = 270)
|
||||
}
|
||||
if top_right_rounded {
|
||||
radius := corner_radii.topRight
|
||||
inner_radius := max(0, radius - width_top)
|
||||
center := Vec2{bounds.x + bounds.width - radius, bounds.y + radius}
|
||||
ring(layer, center, inner_radius, radius, border_color, start_angle = 270, end_angle = 360)
|
||||
}
|
||||
if bottom_right_rounded {
|
||||
radius := corner_radii.bottomRight
|
||||
inner_radius := max(0, radius - width_bottom)
|
||||
center := Vec2{bounds.x + bounds.width - radius, bounds.y + bounds.height - radius}
|
||||
ring(layer, center, inner_radius, radius, border_color, start_angle = 0, end_angle = 90)
|
||||
}
|
||||
if bottom_left_rounded {
|
||||
radius := corner_radii.bottomLeft
|
||||
inner_radius := max(0, radius - width_bottom)
|
||||
center := Vec2{bounds.x + radius, bounds.y + bounds.height - radius}
|
||||
ring(layer, center, inner_radius, radius, border_color, start_angle = 90, end_angle = 180)
|
||||
}
|
||||
}
|
||||
|
||||
// Try to retroactively merge this Border into a pending Rectangle/Image candidate on the
|
||||
// merge stack. Returns true on success so the caller can skip the standalone Border emission.
|
||||
//
|
||||
// Clay emits a parent element's bg and border bracketing all the children's commands, so a
|
||||
// simple "is the next command a Border?" check (the previous approach) only catches leaf
|
||||
// elements. The stack approach lets us pair them across arbitrary nesting: every Rectangle/
|
||||
// Image push registers itself; every Border pops down until it finds a geometric match.
|
||||
//
|
||||
// Pop semantics: non-matching candidates above the match are discarded — their elements had
|
||||
// no border anyway, so their primitives stay in `tmp_primitives` as plain Rectangles. A
|
||||
// Border that finds no match at all falls back to standalone `clay_emit_partial_border`.
|
||||
//
|
||||
// Predicates that decline a candidate:
|
||||
// - non-uniform or zero border widths (can't be a single uniform outline)
|
||||
// - translucent border (the unmerged path's bg-under-border blending differs)
|
||||
// - mismatched bounds or cornerRadius (the candidate isn't from the same element)
|
||||
//
|
||||
// False-match risk: two unrelated elements with bit-identical bounds and corner radii.
|
||||
// Requires geometric coincidence (rare in practice), and even when it fires, the misattributed
|
||||
// outline still lands at the correct screen position with the correct color — the pixels
|
||||
// match the unmerged ground truth for opaque borders (the only kind we merge).
|
||||
//INTERNAL
|
||||
try_dispatch_clay_border_merge :: proc(bounds: Rectangle, border_data: clay.BorderRenderData) -> bool {
|
||||
border_width := border_data.width
|
||||
uniform_nonzero :=
|
||||
border_width.left == border_width.top &&
|
||||
border_width.top == border_width.right &&
|
||||
border_width.right == border_width.bottom &&
|
||||
border_width.top > 0
|
||||
if !uniform_nonzero do return false
|
||||
if border_data.color[3] < 255 do return false
|
||||
|
||||
for len(GLOB.clay_merge_open_stack) > 0 {
|
||||
candidate := pop(&GLOB.clay_merge_open_stack)
|
||||
if candidate.outer_bounds != bounds do continue
|
||||
if candidate.corner_radii != border_data.cornerRadius do continue
|
||||
apply_clay_border_merge_to_primitive(candidate, border_data)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Mutates `tmp_primitives[candidate.primitive_index]` in place: shrinks the SDF shape by
|
||||
// the uniform border width so the (outward) outline lands at the outer bounds, sets the
|
||||
// outline flag and params, and — for `Fill_Texture` candidates — refits the texture's UV
|
||||
// against `inner_bounds` so the image doesn't overflow into the border strip.
|
||||
//
|
||||
// The primitive's `bounds` field stays at the outer bounds: the rasterized quad already
|
||||
// covers the area the outline now occupies. Skipping the bounds expansion that
|
||||
// `apply_brush_and_outline` would normally do is intentional — expanding here would push the
|
||||
// rasterized quad past Clay's outer edge.
|
||||
//INTERNAL
|
||||
apply_clay_border_merge_to_primitive :: proc(
|
||||
candidate: Clay_Merge_Candidate,
|
||||
border_data: clay.BorderRenderData,
|
||||
) {
|
||||
prim := &GLOB.tmp_primitives[candidate.primitive_index]
|
||||
uniform_width := f32(border_data.width.top)
|
||||
dpi_scale := GLOB.dpi_scaling
|
||||
|
||||
inner_half_width := candidate.outer_bounds.width * 0.5 - uniform_width
|
||||
inner_half_height := candidate.outer_bounds.height * 0.5 - uniform_width
|
||||
prim.params.rrect.half_size_ppx = {inner_half_width * dpi_scale, inner_half_height * dpi_scale}
|
||||
prim.params.rrect.radii_ppx = {
|
||||
max(0, candidate.corner_radii.topLeft - uniform_width) * dpi_scale,
|
||||
max(0, candidate.corner_radii.topRight - uniform_width) * dpi_scale,
|
||||
max(0, candidate.corner_radii.bottomRight - uniform_width) * dpi_scale,
|
||||
max(0, candidate.corner_radii.bottomLeft - uniform_width) * dpi_scale,
|
||||
}
|
||||
|
||||
// Set the outline bit in the packed flags field (low byte = Shape_Kind, bits 8+ = Shape_Flags).
|
||||
prim.flags |= u32(transmute(u8)Shape_Flags{.Outline}) << 8
|
||||
prim.effects.outline_color = Color(border_data.color)
|
||||
prim.effects.outline_packed = pack_f16_pair(f16(uniform_width * dpi_scale), 0)
|
||||
|
||||
if candidate.kind == .Fill_Texture {
|
||||
// The candidate was only pushed if its `fit_rect == outer_bounds` at emission time, so the
|
||||
// image fills the rasterized quad. Refit UVs against `inner_bounds` so the image is scoped
|
||||
// to the area inside the new outline rather than overflowing into the border strip.
|
||||
inner_bounds := Rectangle {
|
||||
x = candidate.outer_bounds.x + uniform_width,
|
||||
y = candidate.outer_bounds.y + uniform_width,
|
||||
width = candidate.outer_bounds.width - 2 * uniform_width,
|
||||
height = candidate.outer_bounds.height - 2 * uniform_width,
|
||||
}
|
||||
uv_rect, _, _ := fit_params(candidate.image_data.fit, inner_bounds, candidate.image_data.texture_id)
|
||||
prim.uv_rect = {uv_rect.x, uv_rect.y, uv_rect.width, uv_rect.height}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// ----- Command dispatch ------------
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// Dispatch a single non-backdrop Clay render command to the appropriate `draw` primitive.
|
||||
// Extracted from the main `prepare_clay_batch` walk so that the deferred-buffer flush path
|
||||
// can replay commands accumulated during an open backdrop scope without duplicating the
|
||||
// per-command lowering code.
|
||||
//INTERNAL
|
||||
dispatch_clay_command :: proc(
|
||||
layer: ^Layer,
|
||||
render_command: ^clay.RenderCommand,
|
||||
custom_draw: Custom_Draw,
|
||||
temp_allocator: runtime.Allocator,
|
||||
) {
|
||||
// Translate bounding box of the primitive by the layer position
|
||||
bounds := Rectangle {
|
||||
x = render_command.boundingBox.x + layer.bounds.x,
|
||||
y = render_command.boundingBox.y + layer.bounds.y,
|
||||
width = render_command.boundingBox.width,
|
||||
height = render_command.boundingBox.height,
|
||||
}
|
||||
|
||||
switch render_command.commandType {
|
||||
case clay.RenderCommandType.None:
|
||||
log.errorf(
|
||||
"Received render command with type None. This generally means we're in some kind of fucked up state.",
|
||||
)
|
||||
case clay.RenderCommandType.Text:
|
||||
render_data := render_command.renderData.text
|
||||
txt := string(render_data.stringContents.chars[:render_data.stringContents.length])
|
||||
c_text := strings.clone_to_cstring(txt, temp_allocator)
|
||||
defer delete(c_text, temp_allocator)
|
||||
// Clay render-command IDs are derived via Clay's internal HashNumber (Jenkins-family)
|
||||
// and namespaced with .Clay so they can never collide with user-provided custom text IDs.
|
||||
sdl_text := cache_get_or_update(
|
||||
Cache_Key{render_command.id, .Clay},
|
||||
c_text,
|
||||
get_font(render_data.fontId, render_data.fontSize),
|
||||
)
|
||||
prepare_text(layer, Text{sdl_text, {bounds.x, bounds.y}, Color(render_data.textColor)})
|
||||
case clay.RenderCommandType.Image:
|
||||
// Any texture
|
||||
render_data := render_command.renderData.image
|
||||
if render_data.imageData == nil do return
|
||||
img_data := (^Clay_Image_Data)(render_data.imageData)^
|
||||
corner_radii_clay := render_data.cornerRadius
|
||||
radii := Rectangle_Radii {
|
||||
top_left = corner_radii_clay.topLeft,
|
||||
top_right = corner_radii_clay.topRight,
|
||||
bottom_right = corner_radii_clay.bottomRight,
|
||||
bottom_left = corner_radii_clay.bottomLeft,
|
||||
}
|
||||
|
||||
background_color := Color(render_data.backgroundColor)
|
||||
uv_rect, sampler, fit_rect := fit_params(img_data.fit, bounds, img_data.texture_id)
|
||||
|
||||
if background_color.a > 0 {
|
||||
// Bg behind image. Push the bg primitive as the merge candidate so a matching Border
|
||||
// turns into a bg+border-merged primitive plus a separate image draw on top.
|
||||
rectangle(layer, bounds, background_color, radii = radii)
|
||||
bg_primitive_index := u32(len(GLOB.tmp_primitives) - 1)
|
||||
rectangle(
|
||||
layer,
|
||||
fit_rect,
|
||||
Texture_Fill{id = img_data.texture_id, tint = img_data.tint, uv_rect = uv_rect, sampler = sampler},
|
||||
radii = radii,
|
||||
)
|
||||
append(
|
||||
&GLOB.clay_merge_open_stack,
|
||||
Clay_Merge_Candidate {
|
||||
primitive_index = bg_primitive_index,
|
||||
outer_bounds = bounds,
|
||||
corner_radii = corner_radii_clay,
|
||||
kind = .Fill_Color,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
// No bg: the image itself can host the outline if its fit fully covers Clay's bounds.
|
||||
// `Fit_Mode.Fit` with aspect mismatch returns a sub-rect, which can't host an outline
|
||||
// (the rasterized quad wouldn't reach Clay's outer edge), so we skip pushing.
|
||||
rectangle(
|
||||
layer,
|
||||
fit_rect,
|
||||
Texture_Fill{id = img_data.texture_id, tint = img_data.tint, uv_rect = uv_rect, sampler = sampler},
|
||||
radii = radii,
|
||||
)
|
||||
if fit_rect == bounds {
|
||||
img_primitive_index := u32(len(GLOB.tmp_primitives) - 1)
|
||||
append(
|
||||
&GLOB.clay_merge_open_stack,
|
||||
Clay_Merge_Candidate {
|
||||
primitive_index = img_primitive_index,
|
||||
outer_bounds = bounds,
|
||||
corner_radii = corner_radii_clay,
|
||||
image_data = img_data,
|
||||
kind = .Fill_Texture,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
case clay.RenderCommandType.ScissorStart:
|
||||
if bounds.width == 0 || bounds.height == 0 do return
|
||||
|
||||
curr_scissor := &GLOB.scissors[layer.scissor_start + layer.scissor_len - 1]
|
||||
|
||||
if curr_scissor.sub_batch_len != 0 {
|
||||
// Scissor has some content, need to make a new scissor
|
||||
new := Scissor {
|
||||
sub_batch_start = curr_scissor.sub_batch_start + curr_scissor.sub_batch_len,
|
||||
bounds = sdl.Rect {
|
||||
c.int(bounds.x * GLOB.dpi_scaling),
|
||||
c.int(bounds.y * GLOB.dpi_scaling),
|
||||
c.int(bounds.width * GLOB.dpi_scaling),
|
||||
c.int(bounds.height * GLOB.dpi_scaling),
|
||||
},
|
||||
}
|
||||
append(&GLOB.scissors, new)
|
||||
layer.scissor_len += 1
|
||||
} else {
|
||||
curr_scissor.bounds = sdl.Rect {
|
||||
c.int(bounds.x * GLOB.dpi_scaling),
|
||||
c.int(bounds.y * GLOB.dpi_scaling),
|
||||
c.int(bounds.width * GLOB.dpi_scaling),
|
||||
c.int(bounds.height * GLOB.dpi_scaling),
|
||||
}
|
||||
}
|
||||
case clay.RenderCommandType.ScissorEnd:
|
||||
case clay.RenderCommandType.OverlayColorStart, clay.RenderCommandType.OverlayColorEnd:
|
||||
unimplemented("Clay overlays not supported yet...")
|
||||
case clay.RenderCommandType.Rectangle:
|
||||
render_data := render_command.renderData.rectangle
|
||||
corner_radii_clay := render_data.cornerRadius
|
||||
background_color := Color(render_data.backgroundColor)
|
||||
radii := Rectangle_Radii {
|
||||
top_left = corner_radii_clay.topLeft,
|
||||
top_right = corner_radii_clay.topRight,
|
||||
bottom_right = corner_radii_clay.bottomRight,
|
||||
bottom_left = corner_radii_clay.bottomLeft,
|
||||
}
|
||||
rectangle(layer, bounds, background_color, radii = radii)
|
||||
// Register this primitive as a merge candidate. If the element has a matching Border
|
||||
// later in the stream (after its children's commands), `try_dispatch_clay_border_merge`
|
||||
// will pop this candidate and mutate the primitive in-place to add the outline.
|
||||
primitive_index := u32(len(GLOB.tmp_primitives) - 1)
|
||||
append(
|
||||
&GLOB.clay_merge_open_stack,
|
||||
Clay_Merge_Candidate {
|
||||
primitive_index = primitive_index,
|
||||
outer_bounds = bounds,
|
||||
corner_radii = corner_radii_clay,
|
||||
kind = .Fill_Color,
|
||||
},
|
||||
)
|
||||
case clay.RenderCommandType.Border:
|
||||
render_data := render_command.renderData.border
|
||||
if try_dispatch_clay_border_merge(bounds, render_data) do return
|
||||
clay_emit_partial_border(
|
||||
layer,
|
||||
bounds,
|
||||
Color(render_data.color),
|
||||
render_data.width,
|
||||
render_data.cornerRadius,
|
||||
)
|
||||
case clay.RenderCommandType.Custom:
|
||||
// Copy the CustomRenderData by value so we can patch its `customData` field for the
|
||||
// user callback without mutating Clay-owned memory. After unwrapping, the callback
|
||||
// sees its own pointer in `render_data.customData`, identical to what it would see
|
||||
// if `Clay_Custom` did not exist as an intermediary.
|
||||
patched := render_command.renderData.custom
|
||||
// Default to nil so a zero-init `Clay_Custom` (no variant set) and an originally-nil
|
||||
// `customData` both surface to the callback as `customData = nil`.
|
||||
patched.customData = nil
|
||||
if custom_data_pointer := render_command.renderData.custom.customData; custom_data_pointer != nil {
|
||||
switch custom_value in (^Clay_Custom)(custom_data_pointer)^ {
|
||||
case Backdrop_Marker: // The walker pre-filters backdrops into `dispatch_clay_backdrop` and never feeds
|
||||
// them here; reaching this branch means either the walker logic is broken or the
|
||||
// `Clay_Custom` variant tag mutated between the walker's `is_clay_backdrop` check
|
||||
// and this re-check (heap corruption / lifetime bug in user-managed customData
|
||||
// memory). Both are renderer-level bugs that warrant a hard failure rather than a
|
||||
// silently-dropped panel.
|
||||
log.panicf(
|
||||
"backdrop marker reached dispatch_clay_command; either the prepare_clay_batch walker is misrouting commands or the customData pointee at %p was mutated mid-frame",
|
||||
render_command.renderData.custom.customData,
|
||||
)
|
||||
case rawptr: patched.customData = custom_value
|
||||
}
|
||||
}
|
||||
if custom_draw != nil {
|
||||
custom_draw(layer, bounds, patched)
|
||||
} else if patched.customData != nil {
|
||||
log.panicf(
|
||||
"Received clay render command of type custom with non-nil user data but no custom_draw proc provided.",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch a single backdrop Clay render command to `backdrop_blur` on the active layer.
|
||||
// Caller guarantees:
|
||||
// - a backdrop scope is open on `layer` so the underlying `append_or_extend_sub_batch`
|
||||
// contract assertion is satisfied;
|
||||
// - the command's `customData` points at a `Clay_Custom` whose active variant is
|
||||
// `Backdrop_Marker` (the walker has already verified this via `is_clay_backdrop`).
|
||||
//INTERNAL
|
||||
dispatch_clay_backdrop :: proc(layer: ^Layer, cmd: ^clay.RenderCommand) {
|
||||
bounds := Rectangle {
|
||||
x = cmd.boundingBox.x + layer.bounds.x,
|
||||
y = cmd.boundingBox.y + layer.bounds.y,
|
||||
width = cmd.boundingBox.width,
|
||||
height = cmd.boundingBox.height,
|
||||
}
|
||||
// Type-asserting form (no `, ok`): panics loudly if the variant tag changed since
|
||||
// `is_clay_backdrop`, which is the desired tripwire for a heap-corruption bug in
|
||||
// user-managed customData.
|
||||
marker := (^Clay_Custom)(cmd.renderData.custom.customData).(Backdrop_Marker)
|
||||
backdrop_blur(
|
||||
layer,
|
||||
bounds,
|
||||
gaussian_sigma = marker.sigma,
|
||||
tint = marker.tint,
|
||||
radii = marker.radii,
|
||||
feather_ppx = marker.feather_ppx,
|
||||
)
|
||||
}
|
||||
|
||||
// Close the in-flight backdrop scope (if open) and replay every command accumulated in the
|
||||
// deferred index buffer. Ordering: end_backdrop first so deferred non-backdrop draws land
|
||||
// at submission position relative to the bracket they followed (the bracket is now closed,
|
||||
// so these draws render after it). Used at every zIndex transition and at end of stream.
|
||||
//INTERNAL
|
||||
flush_deferred_and_close_backdrop_scope :: proc(
|
||||
layer: ^Layer,
|
||||
batch: ^ClayBatch,
|
||||
deferred_indices: ^[dynamic]i32,
|
||||
backdrop_scope_open: ^bool,
|
||||
custom_draw: Custom_Draw,
|
||||
temp_allocator: runtime.Allocator,
|
||||
) {
|
||||
if backdrop_scope_open^ {
|
||||
end_backdrop(layer)
|
||||
backdrop_scope_open^ = false
|
||||
}
|
||||
// Clear the merge stack at scope/stratum boundaries: any pending candidates from the
|
||||
// pre-scope (or pre-transition) commands stay as plain primitives — they can't merge
|
||||
// with Borders on the far side of the boundary because that would change draw order.
|
||||
clear(&GLOB.clay_merge_open_stack)
|
||||
for index in deferred_indices^ {
|
||||
cmd := clay.RenderCommandArray_Get(&batch.cmds, index)
|
||||
dispatch_clay_command(layer, cmd, custom_draw, temp_allocator)
|
||||
}
|
||||
clear(deferred_indices)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// ----- Main entry point ------------
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// Process Clay render commands into shape, text, and backdrop primitives.
|
||||
//
|
||||
// Single-walk dispatcher with a deferred buffer. The walk does three things per command:
|
||||
// 1. zIndex transitions: close the in-flight scope, flush any deferred non-backdrop
|
||||
// commands into the current layer, then open a new layer seeded with `base_layer.bounds`
|
||||
// (NOT the bumping element's bounds — Clay's floating elements with `clipTo = .None`
|
||||
// should not be over-clipped, and `clipTo = .AttachedParent` floating elements get a
|
||||
// Clay-emitted ScissorStart immediately afterward that narrows correctly).
|
||||
// 2. Backdrop commands: open a scope on first encounter (extending it on subsequent ones),
|
||||
// then dispatch the backdrop_blur call.
|
||||
// 3. Non-backdrop commands during an open scope: append to the deferred buffer for replay
|
||||
// after the scope closes. The buffer holds command indices, not pointers, so it stays
|
||||
// valid even if the underlying ClayArray reallocates.
|
||||
// At end of stream, flush whatever remains.
|
||||
prepare_clay_batch :: proc(
|
||||
base_layer: ^Layer,
|
||||
batch: ^ClayBatch,
|
||||
custom_draw: Custom_Draw = nil,
|
||||
temp_allocator := context.temp_allocator,
|
||||
) {
|
||||
layer := base_layer
|
||||
command_count := int(batch.cmds.length)
|
||||
deferred_indices := make([dynamic]i32, 0, 16, temp_allocator)
|
||||
backdrop_scope_open := false
|
||||
// Seed from GLOB.clay_z_index so multi-batch frames preserve the original semantics: a
|
||||
// later call to `prepare_clay_batch` doesn't re-trigger layer splits for zIndex values
|
||||
// the previous batch already saw.
|
||||
previous_z_index := GLOB.clay_z_index
|
||||
|
||||
// Start with a clean merge stack. The stack is also cleared by
|
||||
// `flush_deferred_and_close_backdrop_scope` at every stratum boundary; both clears together
|
||||
// ensure merge candidates never pair across a boundary that would shift draw order.
|
||||
clear(&GLOB.clay_merge_open_stack)
|
||||
for i in 0 ..< command_count {
|
||||
cmd := clay.RenderCommandArray_Get(&batch.cmds, i32(i))
|
||||
|
||||
// zIndex transition: close out current stratum, create new layer, continue.
|
||||
if cmd.zIndex > previous_z_index {
|
||||
log.debug("Higher zIndex found, creating new layer & setting z_index to", cmd.zIndex)
|
||||
flush_deferred_and_close_backdrop_scope(
|
||||
layer,
|
||||
batch,
|
||||
&deferred_indices,
|
||||
&backdrop_scope_open,
|
||||
custom_draw,
|
||||
temp_allocator,
|
||||
)
|
||||
layer = new_layer(layer, base_layer.bounds)
|
||||
previous_z_index = cmd.zIndex
|
||||
// Keep GLOB.clay_z_index in sync for any external readers (debug tooling, etc.).
|
||||
GLOB.clay_z_index = cmd.zIndex
|
||||
}
|
||||
|
||||
if is_clay_backdrop(cmd) {
|
||||
if !backdrop_scope_open {
|
||||
begin_backdrop(layer)
|
||||
backdrop_scope_open = true
|
||||
}
|
||||
dispatch_clay_backdrop(layer, cmd)
|
||||
} else if backdrop_scope_open {
|
||||
append(&deferred_indices, i32(i))
|
||||
} else {
|
||||
// Rectangle/Image dispatches push merge candidates; Border dispatches pop the stack
|
||||
// to retroactively add an outline to a matching candidate. See
|
||||
// `try_dispatch_clay_border_merge` for the matching semantics.
|
||||
dispatch_clay_command(layer, cmd, custom_draw, temp_allocator)
|
||||
}
|
||||
}
|
||||
|
||||
// End-of-stream: flush whatever remains.
|
||||
flush_deferred_and_close_backdrop_scope(
|
||||
layer,
|
||||
batch,
|
||||
&deferred_indices,
|
||||
&backdrop_scope_open,
|
||||
custom_draw,
|
||||
temp_allocator,
|
||||
)
|
||||
}
|
||||
+97
-74
@@ -9,11 +9,8 @@ import sdl_ttf "vendor:sdl3/ttf"
|
||||
|
||||
//----- Vertex layout ----------------------------------
|
||||
|
||||
// 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 layout for tessellated and text geometry. `color` must be premultiplied alpha; see
|
||||
// the package doc's "Color and blending" section for the contract.
|
||||
Vertex_2D :: struct {
|
||||
position: Vec2,
|
||||
uv: [2]f32,
|
||||
@@ -68,34 +65,34 @@ Shape_Flags :: bit_set[Shape_Flag;u8]
|
||||
|
||||
//INTERNAL
|
||||
RRect_Params :: struct {
|
||||
half_size: [2]f32,
|
||||
radii: [4]f32,
|
||||
half_feather: f32, // feather_px * 0.5; shader uses smoothstep(-h, h, d)
|
||||
half_size_ppx: [2]f32,
|
||||
radii_ppx: [4]f32,
|
||||
half_feather_ppx: f32, // feather_ppx * 0.5; shader uses smoothstep(-h, h, d)
|
||||
_: f32,
|
||||
}
|
||||
|
||||
//INTERNAL
|
||||
NGon_Params :: struct {
|
||||
radius: f32,
|
||||
radius_ppx: f32,
|
||||
sides: f32,
|
||||
half_feather: f32, // feather_px * 0.5; shader uses smoothstep(-h, h, d)
|
||||
half_feather_ppx: f32, // feather_ppx * 0.5; shader uses smoothstep(-h, h, d)
|
||||
_: [5]f32,
|
||||
}
|
||||
|
||||
//INTERNAL
|
||||
Ellipse_Params :: struct {
|
||||
radii: [2]f32,
|
||||
half_feather: f32, // feather_px * 0.5; shader uses smoothstep(-h, h, d)
|
||||
radii_ppx: [2]f32,
|
||||
half_feather_ppx: f32, // feather_ppx * 0.5; shader uses smoothstep(-h, h, d)
|
||||
_: [5]f32,
|
||||
}
|
||||
|
||||
//INTERNAL
|
||||
Ring_Arc_Params :: struct {
|
||||
inner_radius: f32, // inner radius in physical pixels (0 for pie slice)
|
||||
outer_radius: f32, // outer radius in physical pixels
|
||||
inner_radius_ppx: f32, // 0 for pie slice
|
||||
outer_radius_ppx: f32,
|
||||
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)
|
||||
half_feather_ppx: f32, // feather_ppx * 0.5; shader uses smoothstep(-h, h, d)
|
||||
_: f32,
|
||||
}
|
||||
|
||||
@@ -176,9 +173,7 @@ Core_2D :: struct {
|
||||
sampler: ^sdl.GPUSampler,
|
||||
}
|
||||
|
||||
// MSAA is not supported by levlib (see init's doc comment in draw.odin); the PSO is hard-wired
|
||||
// to single-sample. SDF text and shapes provide analytical AA via smoothstep; tessellated user
|
||||
// geometry is not anti-aliased.
|
||||
// PSO is hard-wired to single-sample (no MSAA — see package doc's "Anti-aliasing" section).
|
||||
//INTERNAL
|
||||
create_core_2d :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window) -> (core_2d: Core_2D, ok: bool) {
|
||||
// On failure, clean up any partially-created resources
|
||||
@@ -464,10 +459,31 @@ destroy_core_2d :: proc(device: ^sdl.GPUDevice, core: ^Core_2D) {
|
||||
|
||||
//----- Vertex uniforms ----------------------------------
|
||||
|
||||
//
|
||||
// Coordinate-space contract for the main pipeline's vertex shader:
|
||||
//
|
||||
// Tessellated (0) — `v_position` arrives in *logical* pixels. The vertex
|
||||
// shader multiplies by `dpi_scale` before applying the
|
||||
// ortho projection (which is sized to physical pixels).
|
||||
// SDF (1) — `v_position` is a unit-quad corner (0..1). World-space
|
||||
// coordinates come from `Core_2D_Primitive.bounds` in
|
||||
// logical pixels; the shader scales by `dpi_scale`.
|
||||
// Text (2) — `v_position` arrives in *physical* pixels already.
|
||||
// `prepare_text` and `prepare_text_transformed` bake the
|
||||
// anchor + glyph offsets (from SDL_ttf's GPU text engine,
|
||||
// which lays glyphs out in physical pixels) into the
|
||||
// vertex stream and snap the anchor to integer physical
|
||||
// pixels for atlas-aligned bilinear sampling. The shader
|
||||
// therefore must NOT rescale these vertices.
|
||||
//
|
||||
// The two raw-vertex modes (Tessellated, Text) share `prepare_shape`-style
|
||||
// glue but their coord spaces diverge — see `base_2d.vert` for the shader-
|
||||
// side branch.
|
||||
//INTERNAL
|
||||
Core_2D_Mode :: enum u32 {
|
||||
Tessellated = 0,
|
||||
SDF = 1,
|
||||
Text = 2,
|
||||
}
|
||||
|
||||
//INTERNAL
|
||||
@@ -522,7 +538,9 @@ upload :: proc(device: ^sdl.GPUDevice, pass: ^sdl.GPUCopyPass) {
|
||||
sdl.GPUBufferUsageFlags{.VERTEX},
|
||||
)
|
||||
|
||||
vert_array := sdl.MapGPUTransferBuffer(device, GLOB.core_2d.vertex_buffer.transfer, false)
|
||||
// cycle=true: see backdrop.odin upload_backdrop_primitives. Persistent per-frame
|
||||
// streaming buffer; previous frame's blit is still in flight at map time.
|
||||
vert_array := sdl.MapGPUTransferBuffer(device, GLOB.core_2d.vertex_buffer.transfer, true)
|
||||
if vert_array == nil {
|
||||
log.panicf("Failed to map vertex transfer buffer: %s", sdl.GetError())
|
||||
}
|
||||
@@ -553,7 +571,8 @@ upload :: proc(device: ^sdl.GPUDevice, pass: ^sdl.GPUCopyPass) {
|
||||
|
||||
grow_buffer_if_needed(device, &GLOB.core_2d.index_buffer, index_size, sdl.GPUBufferUsageFlags{.INDEX})
|
||||
|
||||
idx_array := sdl.MapGPUTransferBuffer(device, GLOB.core_2d.index_buffer.transfer, false)
|
||||
// cycle=true: see vertex_buffer above.
|
||||
idx_array := sdl.MapGPUTransferBuffer(device, GLOB.core_2d.index_buffer.transfer, true)
|
||||
if idx_array == nil {
|
||||
log.panicf("Failed to map index transfer buffer: %s", sdl.GetError())
|
||||
}
|
||||
@@ -580,7 +599,8 @@ upload :: proc(device: ^sdl.GPUDevice, pass: ^sdl.GPUCopyPass) {
|
||||
sdl.GPUBufferUsageFlags{.GRAPHICS_STORAGE_READ},
|
||||
)
|
||||
|
||||
prim_array := sdl.MapGPUTransferBuffer(device, GLOB.core_2d.primitive_buffer.transfer, false)
|
||||
// cycle=true: see vertex_buffer above.
|
||||
prim_array := sdl.MapGPUTransferBuffer(device, GLOB.core_2d.primitive_buffer.transfer, true)
|
||||
if prim_array == nil {
|
||||
log.panicf("Failed to map primitive transfer buffer: %s", sdl.GetError())
|
||||
}
|
||||
@@ -814,9 +834,12 @@ render_layer_sub_batch_range :: proc(
|
||||
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
|
||||
// Text vertices live in physical-pixel space (see Core_2D_Mode.Text
|
||||
// docs); mode 2 makes the shader skip the `* dpi_scale` step that
|
||||
// the Tessellated path applies to logical-pixel input.
|
||||
if current_mode != .Text {
|
||||
push_globals(cmd_buffer, width, height, .Text)
|
||||
current_mode = .Text
|
||||
}
|
||||
if current_vert_buf != main_vert_buf {
|
||||
sdl.BindGPUVertexBuffers(render_pass, 0, &sdl.GPUBufferBinding{buffer = main_vert_buf, offset = 0}, 1)
|
||||
@@ -922,8 +945,8 @@ prepare_text :: proc(layer: ^Layer, text: Text) {
|
||||
|
||||
// Snap base position to integer physical pixels to avoid atlas sub-pixel
|
||||
// sampling blur (and the off-by-one bottom-row clip that comes with it).
|
||||
base_x := math.round(text.position[0] * GLOB.dpi_scaling)
|
||||
base_y := math.round(text.position[1] * GLOB.dpi_scaling)
|
||||
base_x_ppx := math.round(text.position[0] * GLOB.dpi_scaling)
|
||||
base_y_ppx := math.round(text.position[1] * GLOB.dpi_scaling)
|
||||
|
||||
// Premultiply text color once — reused across all glyph vertices.
|
||||
pm_color := premultiply_color(text.color)
|
||||
@@ -938,7 +961,7 @@ prepare_text :: proc(layer: ^Layer, text: Text) {
|
||||
uv := data.uv[i]
|
||||
append(
|
||||
&GLOB.tmp_text_verts,
|
||||
Vertex_2D{position = {pos.x + base_x, -pos.y + base_y}, uv = {uv.x, uv.y}, color = pm_color},
|
||||
Vertex_2D{position = {pos.x + base_x_ppx, -pos.y + base_y_ppx}, uv = {uv.x, uv.y}, color = pm_color},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1079,7 +1102,7 @@ build_rrect_primitive :: proc(
|
||||
radii: Rectangle_Radii,
|
||||
origin: Vec2,
|
||||
rotation: f32,
|
||||
feather_px: f32,
|
||||
feather_ppx: f32,
|
||||
) -> Core_2D_Primitive {
|
||||
max_radius := min(rect.width, rect.height) * 0.5
|
||||
clamped_top_left := clamp(radii.top_left, 0, max_radius)
|
||||
@@ -1087,8 +1110,8 @@ build_rrect_primitive :: proc(
|
||||
clamped_bottom_right := clamp(radii.bottom_right, 0, max_radius)
|
||||
clamped_bottom_left := clamp(radii.bottom_left, 0, max_radius)
|
||||
|
||||
half_feather := feather_px * 0.5
|
||||
padding := half_feather / GLOB.dpi_scaling
|
||||
half_feather_ppx := feather_ppx * 0.5
|
||||
padding := half_feather_ppx / GLOB.dpi_scaling
|
||||
dpi_scale := GLOB.dpi_scaling
|
||||
|
||||
half_width := rect.width * 0.5
|
||||
@@ -1126,14 +1149,14 @@ build_rrect_primitive :: proc(
|
||||
rotation_sc = has_rotation ? pack_rotation_sc(sin_angle, cos_angle) : 0,
|
||||
}
|
||||
prim.params.rrect = RRect_Params {
|
||||
half_size = {half_width * dpi_scale, half_height * dpi_scale},
|
||||
radii = {
|
||||
half_size_ppx = {half_width * dpi_scale, half_height * dpi_scale},
|
||||
radii_ppx = {
|
||||
clamped_bottom_right * dpi_scale,
|
||||
clamped_top_right * dpi_scale,
|
||||
clamped_bottom_left * dpi_scale,
|
||||
clamped_top_left * dpi_scale,
|
||||
},
|
||||
half_feather = half_feather,
|
||||
half_feather_ppx = half_feather_ppx,
|
||||
}
|
||||
return prim
|
||||
}
|
||||
@@ -1146,10 +1169,10 @@ build_circle_primitive :: proc(
|
||||
radius: f32,
|
||||
origin: Vec2,
|
||||
rotation: f32,
|
||||
feather_px: f32,
|
||||
feather_ppx: f32,
|
||||
) -> Core_2D_Primitive {
|
||||
half_feather := feather_px * 0.5
|
||||
padding := half_feather / GLOB.dpi_scaling
|
||||
half_feather_ppx := feather_ppx * 0.5
|
||||
padding := half_feather_ppx / GLOB.dpi_scaling
|
||||
dpi_scale := GLOB.dpi_scaling
|
||||
|
||||
actual_center := center
|
||||
@@ -1166,11 +1189,11 @@ build_circle_primitive :: proc(
|
||||
actual_center.y + radius + padding,
|
||||
},
|
||||
}
|
||||
scaled_radius := radius * dpi_scale
|
||||
radius_ppx := radius * dpi_scale
|
||||
prim.params.rrect = RRect_Params {
|
||||
half_size = {scaled_radius, scaled_radius},
|
||||
radii = {scaled_radius, scaled_radius, scaled_radius, scaled_radius},
|
||||
half_feather = half_feather,
|
||||
half_size_ppx = {radius_ppx, radius_ppx},
|
||||
radii_ppx = {radius_ppx, radius_ppx, radius_ppx, radius_ppx},
|
||||
half_feather_ppx = half_feather_ppx,
|
||||
}
|
||||
return prim
|
||||
}
|
||||
@@ -1183,10 +1206,10 @@ build_ellipse_primitive :: proc(
|
||||
radius_horizontal, radius_vertical: f32,
|
||||
origin: Vec2,
|
||||
rotation: f32,
|
||||
feather_px: f32,
|
||||
feather_ppx: f32,
|
||||
) -> Core_2D_Primitive {
|
||||
half_feather := feather_px * 0.5
|
||||
padding := half_feather / GLOB.dpi_scaling
|
||||
half_feather_ppx := feather_ppx * 0.5
|
||||
padding := half_feather_ppx / GLOB.dpi_scaling
|
||||
dpi_scale := GLOB.dpi_scaling
|
||||
|
||||
actual_center := center
|
||||
@@ -1218,8 +1241,8 @@ build_ellipse_primitive :: proc(
|
||||
rotation_sc = has_rotation ? pack_rotation_sc(sin_angle, cos_angle) : 0,
|
||||
}
|
||||
prim.params.ellipse = Ellipse_Params {
|
||||
radii = {radius_horizontal * dpi_scale, radius_vertical * dpi_scale},
|
||||
half_feather = half_feather,
|
||||
radii_ppx = {radius_horizontal * dpi_scale, radius_vertical * dpi_scale},
|
||||
half_feather_ppx = half_feather_ppx,
|
||||
}
|
||||
return prim
|
||||
}
|
||||
@@ -1233,10 +1256,10 @@ build_polygon_primitive :: proc(
|
||||
radius: f32,
|
||||
origin: Vec2,
|
||||
rotation: f32,
|
||||
feather_px: f32,
|
||||
feather_ppx: f32,
|
||||
) -> Core_2D_Primitive {
|
||||
half_feather := feather_px * 0.5
|
||||
padding := half_feather / GLOB.dpi_scaling
|
||||
half_feather_ppx := feather_ppx * 0.5
|
||||
padding := half_feather_ppx / GLOB.dpi_scaling
|
||||
dpi_scale := GLOB.dpi_scaling
|
||||
|
||||
actual_center := center
|
||||
@@ -1258,9 +1281,9 @@ build_polygon_primitive :: proc(
|
||||
rotation_sc = rotation != 0 ? pack_rotation_sc(sin_rot, cos_rot) : 0,
|
||||
}
|
||||
prim.params.ngon = NGon_Params {
|
||||
radius = radius * math.cos(math.PI / f32(sides)) * dpi_scale,
|
||||
radius_ppx = radius * math.cos(math.PI / f32(sides)) * dpi_scale,
|
||||
sides = f32(sides),
|
||||
half_feather = half_feather,
|
||||
half_feather_ppx = half_feather_ppx,
|
||||
}
|
||||
return prim
|
||||
}
|
||||
@@ -1278,13 +1301,13 @@ build_ring_arc_primitive :: proc(
|
||||
end_angle: f32,
|
||||
origin: Vec2,
|
||||
rotation: f32,
|
||||
feather_px: f32,
|
||||
feather_ppx: f32,
|
||||
) -> (
|
||||
Core_2D_Primitive,
|
||||
Shape_Flags,
|
||||
) {
|
||||
half_feather := feather_px * 0.5
|
||||
padding := half_feather / GLOB.dpi_scaling
|
||||
half_feather_ppx := feather_ppx * 0.5
|
||||
padding := half_feather_ppx / GLOB.dpi_scaling
|
||||
dpi_scale := GLOB.dpi_scaling
|
||||
|
||||
actual_center := center
|
||||
@@ -1327,11 +1350,11 @@ build_ring_arc_primitive :: proc(
|
||||
},
|
||||
}
|
||||
prim.params.ring_arc = Ring_Arc_Params {
|
||||
inner_radius = inner_radius * dpi_scale,
|
||||
outer_radius = outer_radius * dpi_scale,
|
||||
inner_radius_ppx = inner_radius * dpi_scale,
|
||||
outer_radius_ppx = outer_radius * dpi_scale,
|
||||
normal_start = normal_start,
|
||||
normal_end = normal_end,
|
||||
half_feather = half_feather,
|
||||
half_feather_ppx = half_feather_ppx,
|
||||
}
|
||||
return prim, arc_flags
|
||||
}
|
||||
@@ -1422,9 +1445,9 @@ rectangle :: proc(
|
||||
radii: Rectangle_Radii = {},
|
||||
origin: Vec2 = {},
|
||||
rotation: f32 = 0,
|
||||
feather_px: f32 = DFT_FEATHER_PX,
|
||||
feather_ppx: f32 = DFT_FEATHER_PPX,
|
||||
) {
|
||||
prim := build_rrect_primitive(rect, radii, origin, rotation, feather_px)
|
||||
prim := build_rrect_primitive(rect, radii, origin, rotation, feather_ppx)
|
||||
apply_brush_and_outline(layer, &prim, .RRect, brush, outline_color, outline_width)
|
||||
}
|
||||
|
||||
@@ -1445,9 +1468,9 @@ circle :: proc(
|
||||
outline_width: f32 = 0,
|
||||
origin: Vec2 = {},
|
||||
rotation: f32 = 0,
|
||||
feather_px: f32 = DFT_FEATHER_PX,
|
||||
feather_ppx: f32 = DFT_FEATHER_PPX,
|
||||
) {
|
||||
prim := build_circle_primitive(center, radius, origin, rotation, feather_px)
|
||||
prim := build_circle_primitive(center, radius, origin, rotation, feather_ppx)
|
||||
apply_brush_and_outline(layer, &prim, .RRect, brush, outline_color, outline_width)
|
||||
}
|
||||
|
||||
@@ -1462,9 +1485,9 @@ ellipse :: proc(
|
||||
outline_width: f32 = 0,
|
||||
origin: Vec2 = {},
|
||||
rotation: f32 = 0,
|
||||
feather_px: f32 = DFT_FEATHER_PX,
|
||||
feather_ppx: f32 = DFT_FEATHER_PPX,
|
||||
) {
|
||||
prim := build_ellipse_primitive(center, radius_horizontal, radius_vertical, origin, rotation, feather_px)
|
||||
prim := build_ellipse_primitive(center, radius_horizontal, radius_vertical, origin, rotation, feather_ppx)
|
||||
apply_brush_and_outline(layer, &prim, .Ellipse, brush, outline_color, outline_width)
|
||||
}
|
||||
|
||||
@@ -1481,11 +1504,11 @@ polygon :: proc(
|
||||
outline_width: f32 = 0,
|
||||
origin: Vec2 = {},
|
||||
rotation: f32 = 0,
|
||||
feather_px: f32 = DFT_FEATHER_PX,
|
||||
feather_ppx: f32 = DFT_FEATHER_PPX,
|
||||
) {
|
||||
if sides < 3 do return
|
||||
|
||||
prim := build_polygon_primitive(center, sides, radius, origin, rotation, feather_px)
|
||||
prim := build_polygon_primitive(center, sides, radius, origin, rotation, feather_ppx)
|
||||
apply_brush_and_outline(layer, &prim, .NGon, brush, outline_color, outline_width)
|
||||
}
|
||||
|
||||
@@ -1504,7 +1527,7 @@ ring :: proc(
|
||||
end_angle: f32 = DFT_CIRC_END_ANGLE,
|
||||
origin: Vec2 = {},
|
||||
rotation: f32 = 0,
|
||||
feather_px: f32 = DFT_FEATHER_PX,
|
||||
feather_ppx: f32 = DFT_FEATHER_PPX,
|
||||
) {
|
||||
prim, arc_flags := build_ring_arc_primitive(
|
||||
center,
|
||||
@@ -1514,7 +1537,7 @@ ring :: proc(
|
||||
end_angle,
|
||||
origin,
|
||||
rotation,
|
||||
feather_px,
|
||||
feather_ppx,
|
||||
)
|
||||
apply_brush_and_outline(layer, &prim, .Ring_Arc, brush, outline_color, outline_width, arc_flags)
|
||||
}
|
||||
@@ -1528,7 +1551,7 @@ line :: proc(
|
||||
thickness: f32 = DFT_STROKE_THICKNESS,
|
||||
outline_color: Color = {},
|
||||
outline_width: f32 = 0,
|
||||
feather_px: f32 = DFT_FEATHER_PX,
|
||||
feather_ppx: f32 = DFT_FEATHER_PPX,
|
||||
) {
|
||||
delta_x := end_position.x - start_position.x
|
||||
delta_y := end_position.y - start_position.y
|
||||
@@ -1544,8 +1567,8 @@ line :: proc(
|
||||
half_thickness := thickness * 0.5
|
||||
cap_radius := half_thickness
|
||||
|
||||
half_feather := feather_px * 0.5
|
||||
padding := half_feather / GLOB.dpi_scaling
|
||||
half_feather_ppx := feather_ppx * 0.5
|
||||
padding := half_feather_ppx / GLOB.dpi_scaling
|
||||
dpi_scale := GLOB.dpi_scaling
|
||||
|
||||
// Expand bounds for rotation
|
||||
@@ -1561,14 +1584,14 @@ line :: proc(
|
||||
rotation_sc = pack_rotation_sc(sin_angle, cos_angle),
|
||||
}
|
||||
prim.params.rrect = RRect_Params {
|
||||
half_size = {(half_length + cap_radius) * dpi_scale, half_thickness * dpi_scale},
|
||||
radii = {
|
||||
half_size_ppx = {(half_length + cap_radius) * dpi_scale, half_thickness * dpi_scale},
|
||||
radii_ppx = {
|
||||
cap_radius * dpi_scale,
|
||||
cap_radius * dpi_scale,
|
||||
cap_radius * dpi_scale,
|
||||
cap_radius * dpi_scale,
|
||||
},
|
||||
half_feather = half_feather,
|
||||
half_feather_ppx = half_feather_ppx,
|
||||
}
|
||||
apply_brush_and_outline(layer, &prim, .RRect, brush, outline_color, outline_width)
|
||||
}
|
||||
@@ -1581,10 +1604,10 @@ line_strip :: proc(
|
||||
thickness: f32 = DFT_STROKE_THICKNESS,
|
||||
outline_color: Color = {},
|
||||
outline_width: f32 = 0,
|
||||
feather_px: f32 = DFT_FEATHER_PX,
|
||||
feather_ppx: f32 = DFT_FEATHER_PPX,
|
||||
) {
|
||||
if len(points) < 2 do return
|
||||
for i in 0 ..< len(points) - 1 {
|
||||
line(layer, points[i], points[i + 1], brush, thickness, outline_color, outline_width, feather_px)
|
||||
line(layer, points[i], points[i + 1], brush, thickness, outline_color, outline_width, feather_ppx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -747,10 +747,10 @@ PRE_PAD_X :: SPACE_PANEL // 24
|
||||
|
||||
// ============================================================
|
||||
// SCANLINE OVERLAY (opt-in, terminal surfaces only)
|
||||
// Repeating-stripe pattern at very low opacity. Stripe is 2px
|
||||
// transparent + 2px black-at-3% (TINT_SCANLINE).
|
||||
// Repeating-stripe pattern at very low opacity. Stripe is 2 logical
|
||||
// pixels transparent + 2 logical pixels black-at-3% (TINT_SCANLINE).
|
||||
// ============================================================
|
||||
|
||||
SCANLINE_STRIPE_PX :: 2
|
||||
SCANLINE_GAP_PX :: 2
|
||||
SCANLINE_STRIPE_LPX :: 2
|
||||
SCANLINE_GAP_LPX :: 2
|
||||
SCANLINE_COLOR :: TINT_SCANLINE
|
||||
|
||||
+135
-451
@@ -1,15 +1,75 @@
|
||||
// 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 "core:strings"
|
||||
import sdl "vendor:sdl3"
|
||||
import sdl_ttf "vendor:sdl3/ttf"
|
||||
|
||||
import clay "../vendor/clay"
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// ----- Shader format ------------
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
@@ -51,7 +111,7 @@ 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_PX :: 1 // Total AA feather width in physical pixels (half on each side of boundary).
|
||||
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).
|
||||
@@ -108,6 +168,7 @@ Global :: struct {
|
||||
|
||||
// -- 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.
|
||||
@@ -132,27 +193,13 @@ Global :: struct {
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// A 2D position in world space. Non-distinct alias for [2]f32 — bare literals like {100, 200}
|
||||
// work at non-ambiguous call sites.
|
||||
//
|
||||
// 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 position parameters in the draw API (center, origin, start_position, end_position, etc.)
|
||||
// use this coordinate system.
|
||||
//
|
||||
// Units are logical pixels (pre-DPI-scaling). The renderer multiplies by dpi_scaling internally
|
||||
// before uploading to the GPU. A Vec2{100, 50} refers to the same visual location regardless of
|
||||
// display DPI.
|
||||
// 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.
|
||||
//
|
||||
// Channel order: R, G, B, A (indices 0, 1, 2, 3). Alpha 255 is fully opaque, 0 is fully
|
||||
// transparent. This matches the GPU-side layout: the shader unpacks via unpackUnorm4x8 which
|
||||
// reads the bytes in memory order as R, G, B, A and normalizes each to [0, 1].
|
||||
//
|
||||
// When used in the Core_2D_Primitive or Gaussian_Blur_Primitive structs (e.g. .color), the 4 bytes
|
||||
// are stored as a u32 in native byte order and unpacked by the shader.
|
||||
// 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}
|
||||
@@ -217,21 +264,15 @@ Brush :: union {
|
||||
Texture_Fill,
|
||||
}
|
||||
|
||||
// Convert clay.Color ([4]c.float in 0–255 range) to Color.
|
||||
color_from_clay :: #force_inline proc(clay_color: clay.Color) -> Color {
|
||||
return Color{u8(clay_color[0]), u8(clay_color[1]), u8(clay_color[2]), u8(clay_color[3])}
|
||||
}
|
||||
|
||||
// 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. The tessellated vertex path and text path require
|
||||
// premultiplied colors because the blend state is ONE, ONE_MINUS_SRC_ALPHA and the
|
||||
// tessellated fragment shader passes vertex color through without further modification.
|
||||
// Users who construct Vertex_2D structs manually for prepare_shape must premultiply their colors.
|
||||
// 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 {
|
||||
@@ -249,7 +290,7 @@ premultiply_color :: #force_inline proc(color: Color) -> Color {
|
||||
//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.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.
|
||||
@@ -289,12 +330,6 @@ Scissor :: struct {
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// Initialize the renderer. Returns false if GPU pipeline or text engine creation fails.
|
||||
//
|
||||
// MSAA is intentionally NOT supported. SDF text and shapes compute coverage analytically via
|
||||
// `smoothstep`, so they don't benefit from multisampling. Tessellated user geometry submitted
|
||||
// via `prepare_shape` is not anti-aliased — if you need AA on tessellated content, render it
|
||||
// to your own offscreen target and submit it as a texture. RAD Debugger and the SBC target
|
||||
// (Mali Valhall, where MSAA's per-tile bandwidth multiplier is expensive) drove this decision.
|
||||
@(require_results)
|
||||
init :: proc(
|
||||
device: ^sdl.GPUDevice,
|
||||
@@ -304,8 +339,6 @@ init :: proc(
|
||||
) -> (
|
||||
ok: bool,
|
||||
) {
|
||||
min_memory_size: c.size_t = cast(c.size_t)clay.MinMemorySize()
|
||||
|
||||
core, core_ok := create_core_2d(device, window)
|
||||
if !core_ok {
|
||||
return false
|
||||
@@ -352,7 +385,6 @@ init :: proc(
|
||||
pending_text_releases = make([dynamic]^sdl_ttf.Text, 0, 16, allocator = allocator),
|
||||
odin_context = odin_context,
|
||||
dpi_scaling = sdl.GetWindowDisplayScale(window),
|
||||
clay_memory = make([^]u8, min_memory_size, allocator = allocator),
|
||||
core_2d = core,
|
||||
backdrop = backdrop,
|
||||
text_cache = text_cache,
|
||||
@@ -361,12 +393,7 @@ init :: proc(
|
||||
// Reserve slot 0 for INVALID_TEXTURE
|
||||
append(&GLOB.texture_slots, Texture_Slot{})
|
||||
log.debug("Window DPI scaling:", GLOB.dpi_scaling)
|
||||
arena := clay.CreateArenaWithCapacityAndMemory(min_memory_size, GLOB.clay_memory)
|
||||
window_width, window_height: c.int
|
||||
sdl.GetWindowSize(window, &window_width, &window_height)
|
||||
|
||||
clay.Initialize(arena, {f32(window_width), f32(window_height)}, {handler = clay_error_handler})
|
||||
clay.SetMeasureTextFunction(measure_text_clay, nil)
|
||||
init_clay(window, allocator)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -405,7 +432,7 @@ destroy :: proc(device: ^sdl.GPUDevice, allocator := context.allocator) {
|
||||
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)
|
||||
free(GLOB.clay_memory, allocator)
|
||||
destroy_clay(allocator)
|
||||
process_pending_texture_releases()
|
||||
destroy_all_textures()
|
||||
destroy_sampler_pool()
|
||||
@@ -425,7 +452,6 @@ clear_global :: proc() {
|
||||
clear(&GLOB.pending_text_releases)
|
||||
|
||||
GLOB.curr_layer_index = 0
|
||||
GLOB.clay_z_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)
|
||||
@@ -440,36 +466,13 @@ clear_global :: proc() {
|
||||
clear(&GLOB.tmp_primitives)
|
||||
clear(&GLOB.tmp_sub_batches)
|
||||
clear(&GLOB.tmp_gaussian_blur_primitives)
|
||||
clear_clay_per_frame()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// ----- Frame ------------
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// 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]
|
||||
}
|
||||
|
||||
// Creates a new layer
|
||||
new_layer :: proc(prev_layer: ^Layer, bounds: Rectangle) -> ^Layer {
|
||||
if GLOB.open_backdrop_layer != nil {
|
||||
@@ -499,44 +502,28 @@ new_layer :: proc(prev_layer: ^Layer, bounds: Rectangle) -> ^Layer {
|
||||
return &GLOB.layers[GLOB.curr_layer_index]
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
// 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()
|
||||
|
||||
// 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
|
||||
// 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)
|
||||
|
||||
// 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)
|
||||
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.
|
||||
@@ -625,6 +612,46 @@ end :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window, clear_color: Color = DF
|
||||
}
|
||||
}
|
||||
|
||||
// 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 ------------
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
@@ -683,349 +710,6 @@ append_or_extend_sub_batch :: proc(
|
||||
layer.sub_batch_len += 1
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// ----- Clay ------------
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
@(private = "file")
|
||||
clay_error_handler :: proc "c" (errorData: clay.ErrorData) {
|
||||
context = GLOB.odin_context
|
||||
log.error("Clay error:", errorData.errorType, errorData.errorText)
|
||||
}
|
||||
|
||||
@(private = "file")
|
||||
measure_text_clay :: proc "c" (
|
||||
text: clay.StringSlice,
|
||||
config: ^clay.TextElementConfig,
|
||||
user_data: rawptr,
|
||||
) -> clay.Dimensions {
|
||||
context = GLOB.odin_context
|
||||
text := string(text.chars[:text.length])
|
||||
c_text := strings.clone_to_cstring(text, context.temp_allocator)
|
||||
defer delete(c_text, context.temp_allocator)
|
||||
width, height: c.int
|
||||
if !sdl_ttf.GetStringSize(get_font(config.fontId, config.fontSize), c_text, 0, &width, &height) {
|
||||
log.panicf("Failed to measure text: %s", sdl.GetError())
|
||||
}
|
||||
|
||||
return clay.Dimensions{width = f32(width) / GLOB.dpi_scaling, height = f32(height) / GLOB.dpi_scaling}
|
||||
}
|
||||
|
||||
// Called for each Clay `RenderCommandType.Custom` render command that
|
||||
// `prepare_clay_batch` encounters.
|
||||
//
|
||||
// - `layer` is the layer the command belongs to (post-z-index promotion).
|
||||
// - `bounds` is already translated into the active layer's coordinate system
|
||||
// and pre-DPI, matching what the built-in shape procs expect.
|
||||
// - `render_data` is Clay's `CustomRenderData` for the element, exposing
|
||||
// `backgroundColor`, `cornerRadius`, and the `customData` pointer the caller
|
||||
// attached to `clay.CustomElementConfig.customData`.
|
||||
//
|
||||
// The callback must not call `new_layer` or `prepare_clay_batch`.
|
||||
Custom_Draw :: #type proc(layer: ^Layer, bounds: Rectangle, render_data: clay.CustomRenderData)
|
||||
|
||||
ClayBatch :: struct {
|
||||
bounds: Rectangle,
|
||||
cmds: clay.ClayArray(clay.RenderCommand),
|
||||
}
|
||||
|
||||
// Magic-number-tagged struct that user app data points at via Clay's customData field.
|
||||
// `prepare_clay_batch` recognizes these and routes them through a backdrop scope automatically.
|
||||
// The user populates a `Backdrop_Marker`, points `clay.CustomElementConfig.customData` at it,
|
||||
// and the integration walks the command stream, opening/closing scopes around contiguous
|
||||
// backdrop runs. Magic-number sentinel chosen over a separate userData flag so the marker
|
||||
// type stays self-describing in core dumps and in any non-Odin debugger view of the heap.
|
||||
Backdrop_Marker :: struct {
|
||||
magic: u32,
|
||||
sigma: f32,
|
||||
tint: Color,
|
||||
radii: Rectangle_Radii,
|
||||
feather_px: f32,
|
||||
}
|
||||
|
||||
// 'BDPT' in big-endian ASCII. Picked for greppability and to be obviously non-zero in
|
||||
// uninitialized memory; user code that forgets to set the magic field gets routed through
|
||||
// the regular custom_draw path and surfaces as "my custom draw never fired," not as a
|
||||
// silent backdrop schedule.
|
||||
BACKDROP_MARKER_MAGIC :: u32(0x42445054)
|
||||
|
||||
// Returns true if this Clay render command represents a backdrop primitive.
|
||||
// Identified by a magic-number sentinel in the first 4 bytes of customData.
|
||||
is_clay_backdrop :: proc(cmd: ^clay.RenderCommand) -> bool {
|
||||
if cmd.commandType != .Custom do return false
|
||||
p := cmd.renderData.custom.customData
|
||||
if p == nil do return false
|
||||
return (^Backdrop_Marker)(p).magic == BACKDROP_MARKER_MAGIC
|
||||
}
|
||||
|
||||
// Dispatch a single non-backdrop Clay render command to the appropriate `draw` primitive.
|
||||
// Extracted from the main `prepare_clay_batch` walk so that the deferred-buffer flush path
|
||||
// can replay commands accumulated during an open backdrop scope without duplicating the
|
||||
// per-command lowering code.
|
||||
//INTERNAL
|
||||
dispatch_clay_command :: proc(
|
||||
layer: ^Layer,
|
||||
render_command: ^clay.RenderCommand,
|
||||
custom_draw: Custom_Draw,
|
||||
temp_allocator: runtime.Allocator,
|
||||
) {
|
||||
// Translate bounding box of the primitive by the layer position
|
||||
bounds := Rectangle {
|
||||
x = render_command.boundingBox.x + layer.bounds.x,
|
||||
y = render_command.boundingBox.y + layer.bounds.y,
|
||||
width = render_command.boundingBox.width,
|
||||
height = render_command.boundingBox.height,
|
||||
}
|
||||
|
||||
switch render_command.commandType {
|
||||
case clay.RenderCommandType.None:
|
||||
log.errorf(
|
||||
"Received render command with type None. This generally means we're in some kind of fucked up state.",
|
||||
)
|
||||
case clay.RenderCommandType.Text:
|
||||
render_data := render_command.renderData.text
|
||||
txt := string(render_data.stringContents.chars[:render_data.stringContents.length])
|
||||
c_text := strings.clone_to_cstring(txt, temp_allocator)
|
||||
defer delete(c_text, temp_allocator)
|
||||
// Clay render-command IDs are derived via Clay's internal HashNumber (Jenkins-family)
|
||||
// and namespaced with .Clay so they can never collide with user-provided custom text IDs.
|
||||
sdl_text := cache_get_or_update(
|
||||
Cache_Key{render_command.id, .Clay},
|
||||
c_text,
|
||||
get_font(render_data.fontId, render_data.fontSize),
|
||||
)
|
||||
prepare_text(layer, Text{sdl_text, {bounds.x, bounds.y}, color_from_clay(render_data.textColor)})
|
||||
case clay.RenderCommandType.Image:
|
||||
// Any texture
|
||||
render_data := render_command.renderData.image
|
||||
if render_data.imageData == nil do return
|
||||
img_data := (^Clay_Image_Data)(render_data.imageData)^
|
||||
cr := render_data.cornerRadius
|
||||
radii := Rectangle_Radii {
|
||||
top_left = cr.topLeft,
|
||||
top_right = cr.topRight,
|
||||
bottom_right = cr.bottomRight,
|
||||
bottom_left = cr.bottomLeft,
|
||||
}
|
||||
|
||||
// Background color behind the image (Clay allows it)
|
||||
bg := color_from_clay(render_data.backgroundColor)
|
||||
if bg.a > 0 {
|
||||
rectangle(layer, bounds, bg, radii = radii)
|
||||
}
|
||||
|
||||
// Compute fit UVs
|
||||
uv, sampler, inner := fit_params(img_data.fit, bounds, img_data.texture_id)
|
||||
|
||||
// Draw the image
|
||||
rectangle(
|
||||
layer,
|
||||
inner,
|
||||
Texture_Fill{id = img_data.texture_id, tint = img_data.tint, uv_rect = uv, sampler = sampler},
|
||||
radii = radii,
|
||||
)
|
||||
case clay.RenderCommandType.ScissorStart:
|
||||
if bounds.width == 0 || bounds.height == 0 do return
|
||||
|
||||
curr_scissor := &GLOB.scissors[layer.scissor_start + layer.scissor_len - 1]
|
||||
|
||||
if curr_scissor.sub_batch_len != 0 {
|
||||
// Scissor has some content, need to make a new scissor
|
||||
new := Scissor {
|
||||
sub_batch_start = curr_scissor.sub_batch_start + curr_scissor.sub_batch_len,
|
||||
bounds = sdl.Rect {
|
||||
c.int(bounds.x * GLOB.dpi_scaling),
|
||||
c.int(bounds.y * GLOB.dpi_scaling),
|
||||
c.int(bounds.width * GLOB.dpi_scaling),
|
||||
c.int(bounds.height * GLOB.dpi_scaling),
|
||||
},
|
||||
}
|
||||
append(&GLOB.scissors, new)
|
||||
layer.scissor_len += 1
|
||||
} else {
|
||||
curr_scissor.bounds = sdl.Rect {
|
||||
c.int(bounds.x * GLOB.dpi_scaling),
|
||||
c.int(bounds.y * GLOB.dpi_scaling),
|
||||
c.int(bounds.width * GLOB.dpi_scaling),
|
||||
c.int(bounds.height * GLOB.dpi_scaling),
|
||||
}
|
||||
}
|
||||
case clay.RenderCommandType.ScissorEnd:
|
||||
case clay.RenderCommandType.Rectangle:
|
||||
render_data := render_command.renderData.rectangle
|
||||
cr := render_data.cornerRadius
|
||||
color := color_from_clay(render_data.backgroundColor)
|
||||
radii := Rectangle_Radii {
|
||||
top_left = cr.topLeft,
|
||||
top_right = cr.topRight,
|
||||
bottom_right = cr.bottomRight,
|
||||
bottom_left = cr.bottomLeft,
|
||||
}
|
||||
|
||||
rectangle(layer, bounds, color, radii = radii)
|
||||
case clay.RenderCommandType.Border:
|
||||
render_data := render_command.renderData.border
|
||||
cr := render_data.cornerRadius
|
||||
color := color_from_clay(render_data.color)
|
||||
thickness := f32(render_data.width.top)
|
||||
radii := Rectangle_Radii {
|
||||
top_left = cr.topLeft,
|
||||
top_right = cr.topRight,
|
||||
bottom_right = cr.bottomRight,
|
||||
bottom_left = cr.bottomLeft,
|
||||
}
|
||||
|
||||
rectangle(layer, bounds, BLANK, outline_color = color, outline_width = thickness, radii = radii)
|
||||
case clay.RenderCommandType.Custom: if is_clay_backdrop(render_command) {
|
||||
// The walker pre-filters backdrops into `dispatch_clay_backdrop` and never feeds
|
||||
// them here; reaching this branch means either the walker logic is broken or the
|
||||
// `customData` pointee mutated between the walker's `is_clay_backdrop` check and
|
||||
// this re-check (heap corruption / lifetime bug in user-managed customData
|
||||
// memory). Both are renderer-level bugs that warrant a hard failure rather than a
|
||||
// silently-dropped panel.
|
||||
log.panicf(
|
||||
"backdrop marker reached dispatch_clay_command; either the prepare_clay_batch walker is misrouting commands or the customData pointee at %p was mutated mid-frame",
|
||||
render_command.renderData.custom.customData,
|
||||
)
|
||||
} else if custom_draw != nil {
|
||||
custom_draw(layer, bounds, render_command.renderData.custom)
|
||||
} else {
|
||||
log.panicf("Received clay render command of type custom but no custom_draw proc provided.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch a single backdrop Clay render command to `backdrop_blur` on the active layer.
|
||||
// Caller guarantees a backdrop scope is open on `layer` so the underlying
|
||||
// `append_or_extend_sub_batch` contract assertion is satisfied.
|
||||
//INTERNAL
|
||||
dispatch_clay_backdrop :: proc(layer: ^Layer, cmd: ^clay.RenderCommand) {
|
||||
bounds := Rectangle {
|
||||
x = cmd.boundingBox.x + layer.bounds.x,
|
||||
y = cmd.boundingBox.y + layer.bounds.y,
|
||||
width = cmd.boundingBox.width,
|
||||
height = cmd.boundingBox.height,
|
||||
}
|
||||
marker := (^Backdrop_Marker)(cmd.renderData.custom.customData)
|
||||
backdrop_blur(
|
||||
layer,
|
||||
bounds,
|
||||
gaussian_sigma = marker.sigma,
|
||||
tint = marker.tint,
|
||||
radii = marker.radii,
|
||||
feather_px = marker.feather_px,
|
||||
)
|
||||
}
|
||||
|
||||
// Close the in-flight backdrop scope (if open) and replay every command accumulated in the
|
||||
// deferred index buffer. Ordering: end_backdrop first so deferred non-backdrop draws land
|
||||
// at submission position relative to the bracket they followed (the bracket is now closed,
|
||||
// so these draws render after it). Used at every zIndex transition and at end of stream.
|
||||
//INTERNAL
|
||||
flush_deferred_and_close_backdrop_scope :: proc(
|
||||
layer: ^Layer,
|
||||
batch: ^ClayBatch,
|
||||
deferred_indices: ^[dynamic]i32,
|
||||
backdrop_scope_open: ^bool,
|
||||
custom_draw: Custom_Draw,
|
||||
temp_allocator: runtime.Allocator,
|
||||
) {
|
||||
if backdrop_scope_open^ {
|
||||
end_backdrop(layer)
|
||||
backdrop_scope_open^ = false
|
||||
}
|
||||
for index in deferred_indices^ {
|
||||
cmd := clay.RenderCommandArray_Get(&batch.cmds, index)
|
||||
dispatch_clay_command(layer, cmd, custom_draw, temp_allocator)
|
||||
}
|
||||
clear(deferred_indices)
|
||||
}
|
||||
|
||||
// Process Clay render commands into shape, text, and backdrop primitives.
|
||||
//
|
||||
// Single-walk dispatcher with a deferred buffer. The walk does three things per command:
|
||||
// 1. zIndex transitions: close the in-flight scope, flush any deferred non-backdrop
|
||||
// commands into the current layer, then open a new layer seeded with `base_layer.bounds`
|
||||
// (NOT the bumping element's bounds — Clay's floating elements with `clipTo = .None`
|
||||
// should not be over-clipped, and `clipTo = .AttachedParent` floating elements get a
|
||||
// Clay-emitted ScissorStart immediately afterward that narrows correctly).
|
||||
// 2. Backdrop commands: open a scope on first encounter (extending it on subsequent ones),
|
||||
// then dispatch the backdrop_blur call.
|
||||
// 3. Non-backdrop commands during an open scope: append to the deferred buffer for replay
|
||||
// after the scope closes. The buffer holds command indices, not pointers, so it stays
|
||||
// valid even if the underlying ClayArray reallocates.
|
||||
// At end of stream, flush whatever remains.
|
||||
prepare_clay_batch :: proc(
|
||||
base_layer: ^Layer,
|
||||
batch: ^ClayBatch,
|
||||
mouse_wheel_delta: [2]f32,
|
||||
frame_time: f32 = 0,
|
||||
custom_draw: Custom_Draw = nil,
|
||||
temp_allocator := context.temp_allocator,
|
||||
) {
|
||||
mouse_pos: [2]f32
|
||||
mouse_flags := sdl.GetMouseState(&mouse_pos.x, &mouse_pos.y)
|
||||
|
||||
// Update clay internals
|
||||
clay.SetPointerState(
|
||||
clay.Vector2{mouse_pos.x - base_layer.bounds.x, mouse_pos.y - base_layer.bounds.y},
|
||||
.LEFT in mouse_flags,
|
||||
)
|
||||
clay.UpdateScrollContainers(true, mouse_wheel_delta, frame_time)
|
||||
|
||||
layer := base_layer
|
||||
command_count := int(batch.cmds.length)
|
||||
deferred_indices := make([dynamic]i32, 0, 16, temp_allocator)
|
||||
backdrop_scope_open := false
|
||||
// Seed from GLOB.clay_z_index so multi-batch frames preserve the original semantics: a
|
||||
// later call to `prepare_clay_batch` doesn't re-trigger layer splits for zIndex values
|
||||
// the previous batch already saw.
|
||||
previous_z_index := GLOB.clay_z_index
|
||||
|
||||
for i in 0 ..< command_count {
|
||||
cmd := clay.RenderCommandArray_Get(&batch.cmds, i32(i))
|
||||
|
||||
// zIndex transition: close out current stratum, create new layer, continue.
|
||||
if cmd.zIndex > previous_z_index {
|
||||
log.debug("Higher zIndex found, creating new layer & setting z_index to", cmd.zIndex)
|
||||
flush_deferred_and_close_backdrop_scope(
|
||||
layer,
|
||||
batch,
|
||||
&deferred_indices,
|
||||
&backdrop_scope_open,
|
||||
custom_draw,
|
||||
temp_allocator,
|
||||
)
|
||||
layer = new_layer(layer, base_layer.bounds)
|
||||
previous_z_index = cmd.zIndex
|
||||
// Keep GLOB.clay_z_index in sync for any external readers (debug tooling, etc.).
|
||||
GLOB.clay_z_index = cmd.zIndex
|
||||
}
|
||||
|
||||
if is_clay_backdrop(cmd) {
|
||||
if !backdrop_scope_open {
|
||||
begin_backdrop(layer)
|
||||
backdrop_scope_open = true
|
||||
}
|
||||
dispatch_clay_backdrop(layer, cmd)
|
||||
} else if backdrop_scope_open {
|
||||
append(&deferred_indices, i32(i))
|
||||
} else {
|
||||
dispatch_clay_command(layer, cmd, custom_draw, temp_allocator)
|
||||
}
|
||||
}
|
||||
|
||||
// End-of-stream: flush whatever remains.
|
||||
flush_deferred_and_close_backdrop_scope(
|
||||
layer,
|
||||
batch,
|
||||
&deferred_indices,
|
||||
&backdrop_scope_open,
|
||||
custom_draw,
|
||||
temp_allocator,
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// ----- Buffer ------------
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
+39
-27
@@ -1,54 +1,71 @@
|
||||
package draw_qr
|
||||
|
||||
import "core:mem"
|
||||
import "core:slice"
|
||||
|
||||
import draw ".."
|
||||
import "../../qrcode"
|
||||
|
||||
DFT_QR_DARK :: draw.BLACK // Default QR code dark module color.
|
||||
DFT_QR_LIGHT :: draw.WHITE // Default QR code light module color.
|
||||
DFT_QR_BOOST_ECL :: true // Default QR error correction level boost.
|
||||
DFT_QR_QUIET_ZONE :: 4 // Default light-pixel border on each side; 4 is the QR spec value.
|
||||
|
||||
// Returns the number of bytes to_texture will write for the given encoded
|
||||
// QR buffer. Equivalent to size*size*4 where size = qrcode.get_size(qrcode_buf).
|
||||
texture_size :: #force_inline proc(qrcode_buf: []u8) -> int {
|
||||
// Returns the number of bytes to_texture will write. Equals dim*dim*4 where
|
||||
// dim = qrcode.get_size(qrcode_buf) + 2*quiet_zone.
|
||||
texture_size :: #force_inline proc(qrcode_buf: []u8, quiet_zone: int = DFT_QR_QUIET_ZONE) -> int {
|
||||
size := qrcode.get_size(qrcode_buf)
|
||||
return size * size * 4
|
||||
if size == 0 || quiet_zone < 0 do return 0
|
||||
padded_size := size + 2 * quiet_zone
|
||||
return padded_size * padded_size * 4
|
||||
}
|
||||
|
||||
// Decodes an encoded QR buffer into tightly-packed RGBA pixel data written to
|
||||
// texture_buf. No allocations, no GPU calls. Returns the Texture_Desc the
|
||||
// caller should pass to draw.register_texture alongside texture_buf.
|
||||
//
|
||||
// quiet_zone adds that many `light` pixels on each side; the spec value is 4.
|
||||
// Final dimension is qrcode.get_size + 2*quiet_zone on each axis.
|
||||
//
|
||||
// Returns ok=false when:
|
||||
// - qrcode_buf is invalid (qrcode.get_size returns 0).
|
||||
// - texture_buf is smaller than texture_size(qrcode_buf).
|
||||
// - quiet_zone is negative.
|
||||
// - texture_buf is smaller than texture_size(qrcode_buf, quiet_zone).
|
||||
@(require_results)
|
||||
to_texture :: proc(
|
||||
qrcode_buf: []u8,
|
||||
texture_buf: []u8,
|
||||
dark: draw.Color = DFT_QR_DARK,
|
||||
light: draw.Color = DFT_QR_LIGHT,
|
||||
quiet_zone: int = DFT_QR_QUIET_ZONE,
|
||||
) -> (
|
||||
desc: draw.Texture_Desc,
|
||||
ok: bool,
|
||||
) {
|
||||
size := qrcode.get_size(qrcode_buf)
|
||||
if size == 0 do return {}, false
|
||||
if len(texture_buf) < size * size * 4 do return {}, false
|
||||
if size == 0 || quiet_zone < 0 do return
|
||||
padded_size := size + 2 * quiet_zone
|
||||
if len(texture_buf) < padded_size * padded_size * 4 do return
|
||||
|
||||
// Type-pun to []Color so each store is a single 32-bit write.
|
||||
pixels := mem.slice_data_cast([]draw.Color, texture_buf[:padded_size * padded_size * 4])
|
||||
|
||||
// Bulk-fill with light: handles the border and every light QR module at once.
|
||||
slice.fill(pixels, light)
|
||||
|
||||
// Overwrite only the dark modules, offset by the quiet-zone border.
|
||||
for y in 0 ..< size {
|
||||
row := (y + quiet_zone) * padded_size + quiet_zone
|
||||
for x in 0 ..< size {
|
||||
i := (y * size + x) * 4
|
||||
c := dark if qrcode.get_module(qrcode_buf, x, y) else light
|
||||
texture_buf[i + 0] = c[0]
|
||||
texture_buf[i + 1] = c[1]
|
||||
texture_buf[i + 2] = c[2]
|
||||
texture_buf[i + 3] = c[3]
|
||||
if qrcode.get_module(qrcode_buf, x, y) {
|
||||
pixels[row + x] = dark
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return draw.Texture_Desc {
|
||||
width = u32(size),
|
||||
height = u32(size),
|
||||
width = u32(padded_size),
|
||||
height = u32(padded_size),
|
||||
depth_or_layers = 1,
|
||||
type = .D2,
|
||||
format = .R8G8B8A8_UNORM,
|
||||
@@ -71,19 +88,20 @@ register_texture_from_raw :: proc(
|
||||
qrcode_buf: []u8,
|
||||
dark: draw.Color = DFT_QR_DARK,
|
||||
light: draw.Color = DFT_QR_LIGHT,
|
||||
quiet_zone: int = DFT_QR_QUIET_ZONE,
|
||||
temp_allocator := context.temp_allocator,
|
||||
) -> (
|
||||
texture: draw.Texture_Id,
|
||||
ok: bool,
|
||||
) {
|
||||
tex_size := texture_size(qrcode_buf)
|
||||
tex_size := texture_size(qrcode_buf, quiet_zone)
|
||||
if tex_size == 0 do return draw.INVALID_TEXTURE, false
|
||||
|
||||
pixels, alloc_err := make([]u8, tex_size, temp_allocator)
|
||||
if alloc_err != nil do return draw.INVALID_TEXTURE, false
|
||||
defer delete(pixels, temp_allocator)
|
||||
|
||||
desc := to_texture(qrcode_buf, pixels, dark, light) or_return
|
||||
desc := to_texture(qrcode_buf, pixels, dark, light, quiet_zone) or_return
|
||||
return draw.register_texture(desc, pixels)
|
||||
}
|
||||
|
||||
@@ -103,6 +121,7 @@ register_texture_from_text :: proc(
|
||||
boost_ecl: bool = DFT_QR_BOOST_ECL,
|
||||
dark: draw.Color = DFT_QR_DARK,
|
||||
light: draw.Color = DFT_QR_LIGHT,
|
||||
quiet_zone: int = DFT_QR_QUIET_ZONE,
|
||||
temp_allocator := context.temp_allocator,
|
||||
) -> (
|
||||
texture: draw.Texture_Id,
|
||||
@@ -123,7 +142,7 @@ register_texture_from_text :: proc(
|
||||
temp_allocator,
|
||||
) or_return
|
||||
|
||||
return register_texture_from_raw(qrcode_buf, dark, light, temp_allocator)
|
||||
return register_texture_from_raw(qrcode_buf, dark, light, quiet_zone, temp_allocator)
|
||||
}
|
||||
|
||||
// Encodes arbitrary binary data as a QR Code and registers the result as an RGBA texture.
|
||||
@@ -142,6 +161,7 @@ register_texture_from_binary :: proc(
|
||||
boost_ecl: bool = DFT_QR_BOOST_ECL,
|
||||
dark: draw.Color = DFT_QR_DARK,
|
||||
light: draw.Color = DFT_QR_LIGHT,
|
||||
quiet_zone: int = DFT_QR_QUIET_ZONE,
|
||||
temp_allocator := context.temp_allocator,
|
||||
) -> (
|
||||
texture: draw.Texture_Id,
|
||||
@@ -162,18 +182,10 @@ register_texture_from_binary :: proc(
|
||||
temp_allocator,
|
||||
) or_return
|
||||
|
||||
return register_texture_from_raw(qrcode_buf, dark, light, temp_allocator)
|
||||
return register_texture_from_raw(qrcode_buf, dark, light, quiet_zone, temp_allocator)
|
||||
}
|
||||
|
||||
register_texture_from :: proc {
|
||||
register_texture_from_text,
|
||||
register_texture_from_binary,
|
||||
}
|
||||
|
||||
// Default fit=.Fit preserves the QR's square aspect; override as needed.
|
||||
clay_image :: #force_inline proc(
|
||||
texture: draw.Texture_Id,
|
||||
tint: draw.Color = draw.DFT_TINT,
|
||||
) -> draw.Clay_Image_Data {
|
||||
return draw.clay_image_data(texture, fit = .Fit, tint = tint)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,363 @@
|
||||
package examples
|
||||
|
||||
import "core:os"
|
||||
import sdl "vendor:sdl3"
|
||||
|
||||
import "../../draw"
|
||||
import "../../vendor/clay"
|
||||
import cyber "../cybersteel"
|
||||
|
||||
// Clay border debug example.
|
||||
//
|
||||
// Lays out a grid of bordered Clay elements that exercise every code path in
|
||||
// `clay_emit_partial_border` and `try_dispatch_clay_rect_border_pair`:
|
||||
//
|
||||
// 1. Uniform borders (fast path) — sharp, rounded, and the border-thicker-than-radius
|
||||
// edge case (inner corner clamps to 0).
|
||||
// 2. Background + border combinations — opaque bg + opaque uniform border MERGES into one
|
||||
// SDF primitive; translucent border DECLINES the merge to preserve blend fidelity;
|
||||
// non-uniform border declines and falls through to the slow path; translucent bg with
|
||||
// opaque border still merges (bg alpha doesn't affect merge correctness).
|
||||
// 3. Single-side borders — top / right / bottom / left individually.
|
||||
// 4. Two-side borders — parallel pairs (no corners drawn) and adjacent pairs (one corner
|
||||
// rounds, others stay square).
|
||||
// 5. Three-side borders + asymmetric widths.
|
||||
// 6. Layout correctness — a vertical list with bottom-border separators (each border
|
||||
// lives inside its own item, no bleed between siblings) and a row of adjacent fully
|
||||
// bordered siblings (no border overlap, each in its own bounds).
|
||||
clay_borders :: proc() {
|
||||
if !sdl.Init({.VIDEO}) do os.exit(1)
|
||||
window := sdl.CreateWindow("Clay Borders Debug", 1200, 900, {.HIGH_PIXEL_DENSITY})
|
||||
gpu := sdl.CreateGPUDevice(draw.PLATFORM_SHADER_FORMAT, true, nil)
|
||||
if !sdl.ClaimWindowForGPUDevice(gpu, window) do os.exit(1)
|
||||
if !draw.init(gpu, window) do os.exit(1)
|
||||
PLEX_SANS_REGULAR = draw.register_font(cyber.SANS_REGULAR_RAW)
|
||||
|
||||
// Distinct colors so the fill, border, and translucent variants are visually unambiguous.
|
||||
BG_PAGE :: draw.Color{25, 25, 30, 255}
|
||||
FILL_OPAQUE :: draw.Color{80, 120, 200, 255}
|
||||
FILL_TRANSLUCENT :: draw.Color{80, 120, 200, 128}
|
||||
BORDER_OPAQUE :: draw.Color{255, 200, 100, 255}
|
||||
BORDER_TRANSLUCENT :: draw.Color{255, 200, 100, 128}
|
||||
|
||||
label_config := clay.TextElementConfig {
|
||||
fontId = PLEX_SANS_REGULAR,
|
||||
fontSize = 12,
|
||||
textColor = {220, 220, 220, 255},
|
||||
}
|
||||
header_config := clay.TextElementConfig {
|
||||
fontId = PLEX_SANS_REGULAR,
|
||||
fontSize = 16,
|
||||
textColor = {255, 255, 255, 255},
|
||||
}
|
||||
title_config := clay.TextElementConfig {
|
||||
fontId = PLEX_SANS_REGULAR,
|
||||
fontSize = 22,
|
||||
textColor = {255, 255, 255, 255},
|
||||
}
|
||||
|
||||
for {
|
||||
defer free_all(context.temp_allocator)
|
||||
ev: sdl.Event
|
||||
for sdl.PollEvent(&ev) {
|
||||
if ev.type == .QUIT do return
|
||||
}
|
||||
|
||||
base_layer := draw.begin({width = 1200, height = 900})
|
||||
clay.SetLayoutDimensions({width = base_layer.bounds.width, height = base_layer.bounds.height})
|
||||
clay.BeginLayout()
|
||||
|
||||
if clay.UI(clay.ID("borders_page"))(
|
||||
{
|
||||
layout = {
|
||||
sizing = {clay.SizingGrow({}), clay.SizingGrow({})},
|
||||
padding = clay.PaddingAll(20),
|
||||
childGap = 14,
|
||||
layoutDirection = .TopToBottom,
|
||||
},
|
||||
backgroundColor = clay_color(BG_PAGE),
|
||||
},
|
||||
) {
|
||||
clay.Text("Clay Borders Debug", title_config)
|
||||
|
||||
//----- Section 1: Uniform borders (fast path) -----------------------------------
|
||||
clay.Text("Uniform borders (fast path)", header_config)
|
||||
if clay.UI(clay.ID("row_uniform"))(border_row_layout()) {
|
||||
border_test_card(
|
||||
"1px sharp",
|
||||
label_config,
|
||||
FILL_OPAQUE,
|
||||
BORDER_OPAQUE,
|
||||
{left = 1, right = 1, top = 1, bottom = 1},
|
||||
{},
|
||||
)
|
||||
border_test_card(
|
||||
"2px, radius 8",
|
||||
label_config,
|
||||
FILL_OPAQUE,
|
||||
BORDER_OPAQUE,
|
||||
{left = 2, right = 2, top = 2, bottom = 2},
|
||||
{topLeft = 8, topRight = 8, bottomRight = 8, bottomLeft = 8},
|
||||
)
|
||||
border_test_card(
|
||||
"8px, radius 20",
|
||||
label_config,
|
||||
FILL_OPAQUE,
|
||||
BORDER_OPAQUE,
|
||||
{left = 8, right = 8, top = 8, bottom = 8},
|
||||
{topLeft = 20, topRight = 20, bottomRight = 20, bottomLeft = 20},
|
||||
)
|
||||
border_test_card(
|
||||
"10px > radius 5 (inner clamps)",
|
||||
label_config,
|
||||
FILL_OPAQUE,
|
||||
BORDER_OPAQUE,
|
||||
{left = 10, right = 10, top = 10, bottom = 10},
|
||||
{topLeft = 5, topRight = 5, bottomRight = 5, bottomLeft = 5},
|
||||
)
|
||||
}
|
||||
|
||||
//----- Section 2: Background + border (merge optimization) ----------------------
|
||||
clay.Text("Background + border (merge optimization)", header_config)
|
||||
if clay.UI(clay.ID("row_bg_border"))(border_row_layout()) {
|
||||
border_test_card(
|
||||
"opaque bg + opaque (MERGES: 1 prim)",
|
||||
label_config,
|
||||
FILL_OPAQUE,
|
||||
BORDER_OPAQUE,
|
||||
{left = 2, right = 2, top = 2, bottom = 2},
|
||||
{topLeft = 6, topRight = 6, bottomRight = 6, bottomLeft = 6},
|
||||
)
|
||||
border_test_card(
|
||||
"translucent bg + opaque (MERGES)",
|
||||
label_config,
|
||||
FILL_TRANSLUCENT,
|
||||
BORDER_OPAQUE,
|
||||
{left = 3, right = 3, top = 3, bottom = 3},
|
||||
{topLeft = 6, topRight = 6, bottomRight = 6, bottomLeft = 6},
|
||||
)
|
||||
border_test_card(
|
||||
"opaque bg + translucent (NO merge)",
|
||||
label_config,
|
||||
FILL_OPAQUE,
|
||||
BORDER_TRANSLUCENT,
|
||||
{left = 4, right = 4, top = 4, bottom = 4},
|
||||
{topLeft = 8, topRight = 8, bottomRight = 8, bottomLeft = 8},
|
||||
)
|
||||
border_test_card(
|
||||
"opaque bg + non-uniform (NO merge)",
|
||||
label_config,
|
||||
FILL_OPAQUE,
|
||||
BORDER_OPAQUE,
|
||||
{left = 1, right = 4, top = 2, bottom = 3},
|
||||
{topLeft = 6, topRight = 6, bottomRight = 6, bottomLeft = 6},
|
||||
)
|
||||
}
|
||||
|
||||
//----- Section 3: Single side borders -------------------------------------------
|
||||
clay.Text("Single side", header_config)
|
||||
if clay.UI(clay.ID("row_single_side"))(border_row_layout()) {
|
||||
border_test_card("top only (4px)", label_config, FILL_OPAQUE, BORDER_OPAQUE, {top = 4}, {})
|
||||
border_test_card("right only (4px)", label_config, FILL_OPAQUE, BORDER_OPAQUE, {right = 4}, {})
|
||||
border_test_card(
|
||||
"bottom only (4px, divider)",
|
||||
label_config,
|
||||
FILL_OPAQUE,
|
||||
BORDER_OPAQUE,
|
||||
{bottom = 4},
|
||||
{},
|
||||
)
|
||||
border_test_card("left only (4px)", label_config, FILL_OPAQUE, BORDER_OPAQUE, {left = 4}, {})
|
||||
}
|
||||
|
||||
//----- Section 4: Two side borders ----------------------------------------------
|
||||
clay.Text("Two sides", header_config)
|
||||
if clay.UI(clay.ID("row_two_sides"))(border_row_layout()) {
|
||||
border_test_card(
|
||||
"T+B parallel (no corners)",
|
||||
label_config,
|
||||
FILL_OPAQUE,
|
||||
BORDER_OPAQUE,
|
||||
{top = 3, bottom = 3},
|
||||
{topLeft = 8, topRight = 8, bottomRight = 8, bottomLeft = 8},
|
||||
)
|
||||
border_test_card(
|
||||
"L+R parallel (no corners)",
|
||||
label_config,
|
||||
FILL_OPAQUE,
|
||||
BORDER_OPAQUE,
|
||||
{left = 3, right = 3},
|
||||
{topLeft = 8, topRight = 8, bottomRight = 8, bottomLeft = 8},
|
||||
)
|
||||
border_test_card(
|
||||
"T+L adjacent (TL rounds)",
|
||||
label_config,
|
||||
FILL_OPAQUE,
|
||||
BORDER_OPAQUE,
|
||||
{top = 3, left = 3},
|
||||
{topLeft = 12, topRight = 12, bottomRight = 12, bottomLeft = 12},
|
||||
)
|
||||
border_test_card(
|
||||
"B+R adjacent (BR rounds)",
|
||||
label_config,
|
||||
FILL_OPAQUE,
|
||||
BORDER_OPAQUE,
|
||||
{bottom = 3, right = 3},
|
||||
{topLeft = 12, topRight = 12, bottomRight = 12, bottomLeft = 12},
|
||||
)
|
||||
}
|
||||
|
||||
//----- Section 5: Three sides + asymmetric widths -------------------------------
|
||||
clay.Text("Three sides + asymmetric widths", header_config)
|
||||
if clay.UI(clay.ID("row_advanced"))(border_row_layout()) {
|
||||
border_test_card(
|
||||
"T+R+B (no L), rounded",
|
||||
label_config,
|
||||
FILL_OPAQUE,
|
||||
BORDER_OPAQUE,
|
||||
{top = 3, right = 3, bottom = 3},
|
||||
{topLeft = 8, topRight = 8, bottomRight = 8, bottomLeft = 8},
|
||||
)
|
||||
border_test_card(
|
||||
"T+L+R (no B), rounded",
|
||||
label_config,
|
||||
FILL_OPAQUE,
|
||||
BORDER_OPAQUE,
|
||||
{top = 3, left = 3, right = 3},
|
||||
{topLeft = 8, topRight = 8, bottomRight = 8, bottomLeft = 8},
|
||||
)
|
||||
border_test_card(
|
||||
"asym 1/2/3/4 T/R/B/L",
|
||||
label_config,
|
||||
FILL_OPAQUE,
|
||||
BORDER_OPAQUE,
|
||||
{top = 1, right = 2, bottom = 3, left = 4},
|
||||
{},
|
||||
)
|
||||
border_test_card(
|
||||
"asym + rounded",
|
||||
label_config,
|
||||
FILL_OPAQUE,
|
||||
BORDER_OPAQUE,
|
||||
{top = 2, right = 4, bottom = 2, left = 4},
|
||||
{topLeft = 10, topRight = 10, bottomRight = 10, bottomLeft = 10},
|
||||
)
|
||||
}
|
||||
|
||||
//----- Section 6: Layout correctness --------------------------------------------
|
||||
clay.Text("Layout correctness", header_config)
|
||||
if clay.UI(clay.ID("row_correctness"))(
|
||||
{layout = {sizing = {clay.SizingGrow({}), clay.SizingFit({})}, childGap = 14}},
|
||||
) {
|
||||
// 6a: vertical list with per-item bottom-border separator. Each item's
|
||||
// border draws INSIDE its own bounds, so adjacent items don't bleed.
|
||||
if clay.UI(clay.ID("list_demo"))(
|
||||
{
|
||||
layout = {
|
||||
sizing = {clay.SizingFixed(300), clay.SizingFit({})},
|
||||
padding = clay.PaddingAll(6),
|
||||
childGap = 6,
|
||||
layoutDirection = .TopToBottom,
|
||||
},
|
||||
},
|
||||
) {
|
||||
clay.Text("List with bottom-border separators", label_config)
|
||||
if clay.UI(clay.ID("list_outer"))(
|
||||
{
|
||||
layout = {sizing = {clay.SizingGrow({}), clay.SizingFit({})}, layoutDirection = .TopToBottom},
|
||||
backgroundColor = clay_color(FILL_OPAQUE),
|
||||
},
|
||||
) {
|
||||
for index in 0 ..< 5 {
|
||||
if clay.UI(clay.ID("list_item", u32(index)))(
|
||||
{
|
||||
layout = {sizing = {clay.SizingGrow({}), clay.SizingFixed(28)}, padding = clay.PaddingAll(6)},
|
||||
border = {color = clay_color(BORDER_OPAQUE), width = {bottom = 1}},
|
||||
},
|
||||
) {
|
||||
clay.Text("Item", label_config)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 6b: row of adjacent fully bordered siblings. With borders rendered
|
||||
// INSIDE each element's bounds, the boundary between two siblings shows
|
||||
// the natural 2*width sum (no overlap, no bleed).
|
||||
if clay.UI(clay.ID("adj_demo"))(
|
||||
{
|
||||
layout = {
|
||||
sizing = {clay.SizingFixed(380), clay.SizingFit({})},
|
||||
padding = clay.PaddingAll(6),
|
||||
childGap = 6,
|
||||
layoutDirection = .TopToBottom,
|
||||
},
|
||||
},
|
||||
) {
|
||||
clay.Text("Adjacent bordered siblings (no gap)", label_config)
|
||||
if clay.UI(clay.ID("adj_row"))({layout = {sizing = {clay.SizingGrow({}), clay.SizingFit({})}}}) {
|
||||
for index in 0 ..< 4 {
|
||||
if clay.UI(clay.ID("adj_item", u32(index)))(
|
||||
{
|
||||
layout = {sizing = {clay.SizingFixed(80), clay.SizingFixed(60)}},
|
||||
backgroundColor = clay_color(FILL_OPAQUE),
|
||||
border = {color = clay_color(BORDER_OPAQUE), width = {left = 2, right = 2, top = 2, bottom = 2}},
|
||||
},
|
||||
) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clay_batch := draw.ClayBatch {
|
||||
bounds = base_layer.bounds,
|
||||
cmds = clay.EndLayout(0),
|
||||
}
|
||||
draw.prepare_clay_batch(base_layer, &clay_batch)
|
||||
draw.end(gpu, window)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: convert a draw.Color (RGBA u8) to clay.Color (RGBA float in 0-255 range).
|
||||
clay_color :: proc(c: draw.Color) -> clay.Color {
|
||||
return clay.Color{f32(c[0]), f32(c[1]), f32(c[2]), f32(c[3])}
|
||||
}
|
||||
|
||||
// Helper: shared row container declaration for the test sections.
|
||||
border_row_layout :: proc() -> clay.ElementDeclaration {
|
||||
return clay.ElementDeclaration{layout = {sizing = {clay.SizingGrow({}), clay.SizingFit({})}, childGap = 12}}
|
||||
}
|
||||
|
||||
// One labeled test card: a fixed-width column with a caption above and a sample bordered
|
||||
// rectangle below. Uses `clay.ID_LOCAL` for the inner element so each card gets a unique
|
||||
// child ID without the caller passing one explicitly.
|
||||
border_test_card :: proc(
|
||||
label: string,
|
||||
label_config: clay.TextElementConfig,
|
||||
fill_color: draw.Color,
|
||||
border_color: draw.Color,
|
||||
border_width: clay.BorderWidth,
|
||||
corner_radii: clay.CornerRadius,
|
||||
) {
|
||||
if clay.UI(clay.ID(label))(
|
||||
{
|
||||
layout = {
|
||||
sizing = {clay.SizingFixed(275), clay.SizingFit({})},
|
||||
padding = clay.PaddingAll(4),
|
||||
childGap = 6,
|
||||
layoutDirection = .TopToBottom,
|
||||
},
|
||||
},
|
||||
) {
|
||||
clay.Text(label, label_config)
|
||||
if clay.UI(clay.ID_LOCAL("test_inner"))(
|
||||
{
|
||||
layout = {sizing = {clay.SizingGrow({}), clay.SizingFixed(64)}},
|
||||
backgroundColor = clay_color(fill_color),
|
||||
border = clay.BorderElementConfig{color = clay_color(border_color), width = border_width},
|
||||
cornerRadius = corner_radii,
|
||||
},
|
||||
) {}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ EX_HELLOPE_SHAPES :: "hellope-shapes"
|
||||
EX_HELLOPE_TEXT :: "hellope-text"
|
||||
EX_HELLOPE_CLAY :: "hellope-clay"
|
||||
EX_HELLOPE_CUSTOM :: "hellope-custom"
|
||||
EX_CLAY_BORDERS :: "clay-borders"
|
||||
EX_TEXTURES :: "textures"
|
||||
EX_GAUSSIAN_BLUR :: "gaussian-blur"
|
||||
EX_GAUSSIAN_BLUR_DEBUG :: "gaussian-blur-debug"
|
||||
@@ -23,6 +24,8 @@ AVAILABLE_EXAMPLES_MSG ::
|
||||
", " +
|
||||
EX_HELLOPE_CUSTOM +
|
||||
", " +
|
||||
EX_CLAY_BORDERS +
|
||||
", " +
|
||||
EX_TEXTURES +
|
||||
", " +
|
||||
EX_GAUSSIAN_BLUR +
|
||||
@@ -81,6 +84,7 @@ main :: proc() {
|
||||
case EX_HELLOPE_CUSTOM: hellope_custom()
|
||||
case EX_HELLOPE_SHAPES: hellope_shapes()
|
||||
case EX_HELLOPE_TEXT: hellope_text()
|
||||
case EX_CLAY_BORDERS: clay_borders()
|
||||
case EX_TEXTURES: textures()
|
||||
case EX_GAUSSIAN_BLUR: gaussian_blur()
|
||||
case EX_GAUSSIAN_BLUR_DEBUG: gaussian_blur_debug()
|
||||
|
||||
+58
-27
@@ -63,7 +63,7 @@ hellope_shapes :: proc() {
|
||||
outline_width = 2,
|
||||
origin = draw.center_of(rect),
|
||||
rotation = spin_angle,
|
||||
feather_px = 1,
|
||||
feather_ppx = 1,
|
||||
)
|
||||
|
||||
// Rounded rectangle rotating around its center
|
||||
@@ -244,9 +244,8 @@ hellope_clay :: proc() {
|
||||
base_layer := draw.begin({width = 500, height = 500})
|
||||
clay.SetLayoutDimensions({width = base_layer.bounds.width, height = base_layer.bounds.height})
|
||||
clay.BeginLayout()
|
||||
if clay.UI()(
|
||||
if clay.UI(clay.ID("outer"))(
|
||||
{
|
||||
id = clay.ID("outer"),
|
||||
layout = {
|
||||
sizing = {clay.SizingGrow({}), clay.SizingGrow({})},
|
||||
childAlignment = {x = .Center, y = .Center},
|
||||
@@ -254,13 +253,13 @@ hellope_clay :: proc() {
|
||||
backgroundColor = {127, 127, 127, 255},
|
||||
},
|
||||
) {
|
||||
clay.Text("Hellope!", &text_config)
|
||||
clay.Text("Hellope!", text_config)
|
||||
}
|
||||
clay_batch := draw.ClayBatch {
|
||||
bounds = base_layer.bounds,
|
||||
cmds = clay.EndLayout(),
|
||||
cmds = clay.EndLayout(0),
|
||||
}
|
||||
draw.prepare_clay_batch(base_layer, &clay_batch, {0, 0})
|
||||
draw.prepare_clay_batch(base_layer, &clay_batch)
|
||||
draw.end(gpu, window)
|
||||
}
|
||||
}
|
||||
@@ -282,11 +281,29 @@ hellope_custom :: proc() {
|
||||
gauge := Gauge {
|
||||
value = 0.73,
|
||||
color = {50, 200, 100, 255},
|
||||
bg_color = {80, 80, 80, 255},
|
||||
}
|
||||
gauge2 := Gauge {
|
||||
value = 0.45,
|
||||
color = {200, 100, 50, 255},
|
||||
bg_color = {80, 80, 80, 255},
|
||||
}
|
||||
|
||||
// `clay.CustomElementConfig.customData` is a rawptr; the Clay integration in `draw`
|
||||
// requires it to point at a `Clay_Custom` value. The explicit `rawptr(...)` cast is
|
||||
// necessary because Odin does not chain `^Gauge -> rawptr -> Clay_Custom` implicitly
|
||||
// (variant-to-union and ^T-to-rawptr are each implicit on their own, but not stacked).
|
||||
gauge_custom: draw.Clay_Custom = rawptr(&gauge)
|
||||
gauge2_custom: draw.Clay_Custom = rawptr(&gauge2)
|
||||
|
||||
// Backdrop variant: variant-to-union conversion is implicit, so no cast needed.
|
||||
// `tint = draw.WHITE` is the no-op tint per the backdrop module's convention
|
||||
// (matches `examples/backdrop.odin`'s "pure blur, no color" usage).
|
||||
backdrop_custom: draw.Clay_Custom = draw.Backdrop_Marker {
|
||||
sigma = 8,
|
||||
tint = draw.WHITE,
|
||||
}
|
||||
|
||||
spin_angle: f32 = 0
|
||||
|
||||
for {
|
||||
@@ -304,9 +321,8 @@ hellope_custom :: proc() {
|
||||
clay.SetLayoutDimensions({width = base_layer.bounds.width, height = base_layer.bounds.height})
|
||||
clay.BeginLayout()
|
||||
|
||||
if clay.UI()(
|
||||
if clay.UI(clay.ID("outer"))(
|
||||
{
|
||||
id = clay.ID("outer"),
|
||||
layout = {
|
||||
sizing = {clay.SizingGrow({}), clay.SizingGrow({})},
|
||||
childAlignment = {x = .Center, y = .Center},
|
||||
@@ -316,53 +332,68 @@ hellope_custom :: proc() {
|
||||
backgroundColor = {50, 50, 50, 255},
|
||||
},
|
||||
) {
|
||||
if clay.UI()({id = clay.ID("title"), layout = {sizing = {clay.SizingFit({}), clay.SizingFit({})}}}) {
|
||||
clay.Text("Custom Draw Demo", &text_config)
|
||||
if clay.UI(clay.ID("title"))({layout = {sizing = {clay.SizingFit({}), clay.SizingFit({})}}}) {
|
||||
clay.Text("Custom Draw Demo", text_config)
|
||||
}
|
||||
|
||||
if clay.UI()(
|
||||
// gauge1 is BEHIND the backdrop — the backdrop is declared as a floating CHILD
|
||||
// of gauge1, pinned to gauge1's LeftTop and sized 300x30 so it covers exactly
|
||||
// gauge1's footprint. Clay emits a floating child's render command after the
|
||||
// parent's, so the stream order is gauge1 → backdrop → gauge2: gauge1's pixels
|
||||
// land in `source_texture` before the bracket samples (visible as a blurred
|
||||
// reflection inside the strip), and gauge2 is deferred-replayed by
|
||||
// `prepare_clay_batch` after the bracket closes (renders crisp on top of the
|
||||
// bracket output — unrelated to the strip since they don't overlap).
|
||||
// `backgroundColor` is omitted on the gauges; bg lives on `Gauge.bg_color`. See `draw_custom`.
|
||||
if clay.UI(clay.ID("gauge"))(
|
||||
{
|
||||
id = clay.ID("gauge"),
|
||||
layout = {sizing = {clay.SizingFixed(300), clay.SizingFixed(30)}},
|
||||
custom = {customData = &gauge},
|
||||
backgroundColor = {80, 80, 80, 255},
|
||||
custom = {customData = &gauge_custom},
|
||||
},
|
||||
) {
|
||||
if clay.UI(clay.ID("backdrop"))(
|
||||
{
|
||||
floating = {attachTo = .Parent, attachment = {parent = .LeftTop, element = .LeftTop}},
|
||||
layout = {sizing = {clay.SizingFixed(300), clay.SizingFixed(30)}},
|
||||
custom = {customData = &backdrop_custom},
|
||||
},
|
||||
) {}
|
||||
}
|
||||
|
||||
if clay.UI()(
|
||||
if clay.UI(clay.ID("gauge2"))(
|
||||
{
|
||||
id = clay.ID("gauge2"),
|
||||
layout = {sizing = {clay.SizingFixed(300), clay.SizingFixed(30)}},
|
||||
custom = {customData = &gauge2},
|
||||
backgroundColor = {80, 80, 80, 255},
|
||||
custom = {customData = &gauge2_custom},
|
||||
},
|
||||
) {}
|
||||
}
|
||||
|
||||
clay_batch := draw.ClayBatch {
|
||||
bounds = base_layer.bounds,
|
||||
cmds = clay.EndLayout(),
|
||||
cmds = clay.EndLayout(0),
|
||||
}
|
||||
draw.prepare_clay_batch(base_layer, &clay_batch, {0, 0}, custom_draw = draw_custom)
|
||||
draw.prepare_clay_batch(base_layer, &clay_batch, custom_draw = draw_custom)
|
||||
draw.end(gpu, window)
|
||||
}
|
||||
|
||||
Gauge :: struct {
|
||||
value: f32,
|
||||
color: draw.Color,
|
||||
bg_color: draw.Color,
|
||||
}
|
||||
|
||||
draw_custom :: proc(layer: ^draw.Layer, bounds: draw.Rectangle, render_data: clay.CustomRenderData) {
|
||||
// `render_data.customData` has been unwrapped from the `Clay_Custom` envelope by
|
||||
// `prepare_clay_batch` — it points at the Gauge directly, the same as it would have
|
||||
// before the union refactor.
|
||||
gauge := cast(^Gauge)render_data.customData
|
||||
|
||||
// `gauge.bg_color` instead of `render_data.backgroundColor`: under Clay master, an
|
||||
// element with both `custom.customData` and `backgroundColor` emits a Custom AND a
|
||||
// Rectangle for the same bounds, in that order — the Rectangle paints over the
|
||||
// callback's output. Carrying bg on user data sidesteps it.
|
||||
border_width: f32 = 2
|
||||
draw.rectangle(
|
||||
layer,
|
||||
bounds,
|
||||
draw.color_from_clay(render_data.backgroundColor),
|
||||
outline_color = draw.WHITE,
|
||||
outline_width = border_width,
|
||||
)
|
||||
draw.rectangle(layer, bounds, gauge.bg_color, outline_color = draw.WHITE, outline_width = border_width)
|
||||
|
||||
fill := draw.Rectangle {
|
||||
x = bounds.x,
|
||||
|
||||
@@ -166,12 +166,14 @@ textures :: proc() {
|
||||
|
||||
ROW2_Y :: f32(190)
|
||||
|
||||
// QR code (RGBA texture with baked colors, nearest sampling)
|
||||
// QR code (RGBA texture with baked colors, nearest sampling) + thin framing border.
|
||||
draw.rectangle(base_layer, {COL1, ROW2_Y, ITEM_SIZE, ITEM_SIZE}, draw.Color{255, 255, 255, 255}) // white bg
|
||||
draw.rectangle(
|
||||
base_layer,
|
||||
{COL1, ROW2_Y, ITEM_SIZE, ITEM_SIZE},
|
||||
draw.Texture_Fill{id = qr_texture, tint = draw.WHITE, uv_rect = {0, 0, 1, 1}, sampler = .Nearest_Clamp},
|
||||
outline_color = draw.WHITE,
|
||||
outline_width = 2,
|
||||
)
|
||||
draw.text(
|
||||
base_layer,
|
||||
@@ -182,7 +184,7 @@ textures :: proc() {
|
||||
color = draw.WHITE,
|
||||
)
|
||||
|
||||
// Rounded corners
|
||||
// Rounded corners + outline traces the rounded shape.
|
||||
draw.rectangle(
|
||||
base_layer,
|
||||
{COL2, ROW2_Y, ITEM_SIZE, ITEM_SIZE},
|
||||
@@ -192,6 +194,8 @@ textures :: proc() {
|
||||
uv_rect = {0, 0, 1, 1},
|
||||
sampler = .Nearest_Clamp,
|
||||
},
|
||||
outline_color = draw.Color{255, 200, 100, 255},
|
||||
outline_width = 3,
|
||||
radii = draw.uniform_radii({COL2, ROW2_Y, ITEM_SIZE, ITEM_SIZE}, 0.3),
|
||||
)
|
||||
draw.text(
|
||||
@@ -203,7 +207,7 @@ textures :: proc() {
|
||||
color = draw.WHITE,
|
||||
)
|
||||
|
||||
// Rotating
|
||||
// Rotating + outline rotates with the texture.
|
||||
rot_rect := draw.Rectangle{COL3, ROW2_Y, ITEM_SIZE, ITEM_SIZE}
|
||||
draw.rectangle(
|
||||
base_layer,
|
||||
@@ -214,6 +218,8 @@ textures :: proc() {
|
||||
uv_rect = {0, 0, 1, 1},
|
||||
sampler = .Nearest_Clamp,
|
||||
},
|
||||
outline_color = draw.WHITE,
|
||||
outline_width = 2,
|
||||
origin = draw.center_of(rot_rect),
|
||||
rotation = spin_angle,
|
||||
)
|
||||
@@ -282,7 +288,7 @@ textures :: proc() {
|
||||
color = draw.WHITE,
|
||||
)
|
||||
|
||||
// Per-corner radii
|
||||
// Per-corner radii + outline traces the asymmetric corner shape.
|
||||
draw.rectangle(
|
||||
base_layer,
|
||||
{COL4, ROW3_Y, FIT_SIZE, FIT_SIZE},
|
||||
@@ -292,6 +298,8 @@ textures :: proc() {
|
||||
uv_rect = {0, 0, 1, 1},
|
||||
sampler = .Nearest_Clamp,
|
||||
},
|
||||
outline_color = draw.Color{255, 100, 100, 255},
|
||||
outline_width = 3,
|
||||
radii = {20, 0, 20, 0},
|
||||
)
|
||||
draw.text(
|
||||
@@ -321,12 +329,14 @@ textures :: proc() {
|
||||
sampler = .Nearest_Clamp,
|
||||
}
|
||||
|
||||
// Textured circle
|
||||
// Textured circle + outline (textured shape with built-in border).
|
||||
draw.circle(
|
||||
base_layer,
|
||||
{SHAPE_COL1 + SHAPE_SIZE / 2, ROW4_Y + SHAPE_SIZE / 2},
|
||||
SHAPE_SIZE / 2,
|
||||
checker_fill,
|
||||
outline_color = draw.WHITE,
|
||||
outline_width = 2,
|
||||
)
|
||||
draw.text(
|
||||
base_layer,
|
||||
|
||||
@@ -25,9 +25,9 @@ struct main0_in
|
||||
{
|
||||
float2 p_local [[user(locn0)]];
|
||||
float4 f_color [[user(locn1)]];
|
||||
float2 f_half_size [[user(locn2), flat]];
|
||||
float4 f_radii [[user(locn3), flat]];
|
||||
float f_half_feather [[user(locn4), flat]];
|
||||
float2 f_half_size_ppx [[user(locn2), flat]];
|
||||
float4 f_radii_ppx [[user(locn3), flat]];
|
||||
float f_half_feather_ppx [[user(locn4), flat]];
|
||||
};
|
||||
|
||||
static inline __attribute__((always_inline))
|
||||
@@ -96,16 +96,16 @@ fragment main0_out main0(main0_in in [[stage_in]], constant Uniforms& _108 [[buf
|
||||
return out;
|
||||
}
|
||||
float2 param_1 = in.p_local;
|
||||
float2 param_2 = in.f_half_size;
|
||||
float4 param_3 = in.f_radii;
|
||||
float2 param_2 = in.f_half_size_ppx;
|
||||
float4 param_3 = in.f_radii_ppx;
|
||||
float d = sdRoundedBox(param_1, param_2, param_3);
|
||||
if (d > in.f_half_feather)
|
||||
if (d > in.f_half_feather_ppx)
|
||||
{
|
||||
discard_fragment();
|
||||
}
|
||||
float grad_magnitude = fast::max(fwidth(d), 9.9999999747524270787835121154785e-07);
|
||||
float d_n = d / grad_magnitude;
|
||||
float h_n = in.f_half_feather / grad_magnitude;
|
||||
float h_n = in.f_half_feather_ppx / grad_magnitude;
|
||||
float2 uv_1 = (gl_FragCoord.xy * _108.inv_downsample_factor) * _108.inv_working_size;
|
||||
float3 color_1 = blur_input_tex.sample(blur_input_texSmplr, uv_1).xyz;
|
||||
float3 tinted = mix(color_1, color_1 * in.f_color.xyz, float3(in.f_color.w));
|
||||
|
||||
Binary file not shown.
@@ -55,18 +55,18 @@ struct Uniforms
|
||||
struct Gaussian_Blur_Primitive
|
||||
{
|
||||
float4 bounds;
|
||||
float4 radii;
|
||||
float2 half_size;
|
||||
float half_feather;
|
||||
float4 radii_ppx;
|
||||
float2 half_size_ppx;
|
||||
float half_feather_ppx;
|
||||
uint color;
|
||||
};
|
||||
|
||||
struct Gaussian_Blur_Primitive_1
|
||||
{
|
||||
float4 bounds;
|
||||
float4 radii;
|
||||
float2 half_size;
|
||||
float half_feather;
|
||||
float4 radii_ppx;
|
||||
float2 half_size_ppx;
|
||||
float half_feather_ppx;
|
||||
uint color;
|
||||
};
|
||||
|
||||
@@ -81,9 +81,9 @@ struct main0_out
|
||||
{
|
||||
float2 p_local [[user(locn0)]];
|
||||
float4 f_color [[user(locn1)]];
|
||||
float2 f_half_size [[user(locn2)]];
|
||||
float4 f_radii [[user(locn3)]];
|
||||
float f_half_feather [[user(locn4)]];
|
||||
float2 f_half_size_ppx [[user(locn2)]];
|
||||
float4 f_radii_ppx [[user(locn3)]];
|
||||
float f_half_feather_ppx [[user(locn4)]];
|
||||
float4 gl_Position [[position]];
|
||||
};
|
||||
|
||||
@@ -96,26 +96,26 @@ vertex main0_out main0(constant Uniforms& _13 [[buffer(0)]], const device Gaussi
|
||||
out.gl_Position = float4(ndc, 0.0, 1.0);
|
||||
out.p_local = float2(0.0);
|
||||
out.f_color = float4(0.0);
|
||||
out.f_half_size = float2(0.0);
|
||||
out.f_radii = float4(0.0);
|
||||
out.f_half_feather = 0.0;
|
||||
out.f_half_size_ppx = float2(0.0);
|
||||
out.f_radii_ppx = float4(0.0);
|
||||
out.f_half_feather_ppx = 0.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
Gaussian_Blur_Primitive p;
|
||||
p.bounds = _69.primitives[int(gl_InstanceIndex)].bounds;
|
||||
p.radii = _69.primitives[int(gl_InstanceIndex)].radii;
|
||||
p.half_size = _69.primitives[int(gl_InstanceIndex)].half_size;
|
||||
p.half_feather = _69.primitives[int(gl_InstanceIndex)].half_feather;
|
||||
p.radii_ppx = _69.primitives[int(gl_InstanceIndex)].radii_ppx;
|
||||
p.half_size_ppx = _69.primitives[int(gl_InstanceIndex)].half_size_ppx;
|
||||
p.half_feather_ppx = _69.primitives[int(gl_InstanceIndex)].half_feather_ppx;
|
||||
p.color = _69.primitives[int(gl_InstanceIndex)].color;
|
||||
float2 corner = _97[int(gl_VertexIndex)];
|
||||
float2 world_pos = mix(p.bounds.xy, p.bounds.zw, corner);
|
||||
float2 center = (p.bounds.xy + p.bounds.zw) * 0.5;
|
||||
out.p_local = (world_pos - center) * _13.dpi_scale;
|
||||
out.f_color = unpack_unorm4x8_to_float(p.color);
|
||||
out.f_half_size = p.half_size;
|
||||
out.f_radii = p.radii;
|
||||
out.f_half_feather = p.half_feather;
|
||||
out.f_half_size_ppx = p.half_size_ppx;
|
||||
out.f_radii_ppx = p.radii_ppx;
|
||||
out.f_half_feather_ppx = p.half_feather_ppx;
|
||||
out.gl_Position = _13.projection * float4(world_pos * _13.dpi_scale, 0.0, 1.0);
|
||||
}
|
||||
return out;
|
||||
|
||||
Binary file not shown.
@@ -107,57 +107,57 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
|
||||
}
|
||||
float d = 1000000015047466219876688855040.0;
|
||||
float h = 0.5;
|
||||
float2 half_size = in.f_params.xy;
|
||||
float2 p_local = in.f_local_or_uv;
|
||||
float2 half_size_ppx = in.f_params.xy;
|
||||
float2 p_local_ppx = in.f_local_or_uv;
|
||||
if (kind == 1u)
|
||||
{
|
||||
float4 corner_radii = float4(in.f_params.zw, in.f_params2.xy);
|
||||
float4 corner_radii_ppx = float4(in.f_params.zw, in.f_params2.xy);
|
||||
h = in.f_params2.z;
|
||||
float2 param = p_local;
|
||||
float2 param_1 = half_size;
|
||||
float4 param_2 = corner_radii;
|
||||
float2 param = p_local_ppx;
|
||||
float2 param_1 = half_size_ppx;
|
||||
float4 param_2 = corner_radii_ppx;
|
||||
d = sdRoundedBox(param, param_1, param_2);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (kind == 2u)
|
||||
{
|
||||
float radius = in.f_params.x;
|
||||
float radius_ppx = in.f_params.x;
|
||||
float sides = in.f_params.y;
|
||||
h = in.f_params.z;
|
||||
float2 param_3 = p_local;
|
||||
float param_4 = radius;
|
||||
float2 param_3 = p_local_ppx;
|
||||
float param_4 = radius_ppx;
|
||||
float param_5 = sides;
|
||||
d = sdRegularPolygon(param_3, param_4, param_5);
|
||||
half_size = float2(radius);
|
||||
half_size_ppx = float2(radius_ppx);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (kind == 3u)
|
||||
{
|
||||
float2 ab = in.f_params.xy;
|
||||
float2 radii_ppx = in.f_params.xy;
|
||||
h = in.f_params.z;
|
||||
float2 param_6 = p_local;
|
||||
float2 param_7 = ab;
|
||||
float2 param_6 = p_local_ppx;
|
||||
float2 param_7 = radii_ppx;
|
||||
d = sdEllipseApprox(param_6, param_7);
|
||||
half_size = ab;
|
||||
half_size_ppx = radii_ppx;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (kind == 4u)
|
||||
{
|
||||
float inner = in.f_params.x;
|
||||
float outer = in.f_params.y;
|
||||
float inner_radius_ppx = in.f_params.x;
|
||||
float outer_radius_ppx = in.f_params.y;
|
||||
float2 n_start = in.f_params.zw;
|
||||
float2 n_end = in.f_params2.xy;
|
||||
uint arc_bits = (flags >> 5u) & 3u;
|
||||
h = in.f_params2.z;
|
||||
float r = length(p_local);
|
||||
d = fast::max(inner - r, r - outer);
|
||||
float r = length(p_local_ppx);
|
||||
d = fast::max(inner_radius_ppx - r, r - outer_radius_ppx);
|
||||
if (arc_bits != 0u)
|
||||
{
|
||||
float d_start = dot(p_local, n_start);
|
||||
float d_end = dot(p_local, n_end);
|
||||
float d_start = dot(p_local_ppx, n_start);
|
||||
float d_end = dot(p_local_ppx, n_end);
|
||||
float _338;
|
||||
if (arc_bits == 1u)
|
||||
{
|
||||
@@ -170,7 +170,7 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
|
||||
float d_wedge = _338;
|
||||
d = fast::max(d, d_wedge);
|
||||
}
|
||||
half_size = float2(outer);
|
||||
half_size_ppx = float2(outer_radius_ppx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,7 +185,7 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
|
||||
float4 gradient_end = unpack_unorm4x8_to_float(in.f_effects.x);
|
||||
if ((flags & 4u) != 0u)
|
||||
{
|
||||
float t_1 = length(p_local / half_size);
|
||||
float t_1 = length(p_local_ppx / half_size_ppx);
|
||||
float4 param_8 = gradient_start;
|
||||
float4 param_9 = gradient_end;
|
||||
float param_10 = t_1;
|
||||
@@ -194,7 +194,7 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
|
||||
else
|
||||
{
|
||||
float2 direction = float2(as_type<half2>(in.f_effects.z));
|
||||
float t_2 = (dot(p_local / half_size, direction) * 0.5) + 0.5;
|
||||
float t_2 = (dot(p_local_ppx / half_size_ppx, direction) * 0.5) + 0.5;
|
||||
float4 param_11 = gradient_start;
|
||||
float4 param_12 = gradient_end;
|
||||
float param_13 = t_2;
|
||||
@@ -206,7 +206,7 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
|
||||
if ((flags & 1u) != 0u)
|
||||
{
|
||||
float4 uv_rect = in.f_uv_rect;
|
||||
float2 local_uv = ((p_local / half_size) * 0.5) + float2(0.5);
|
||||
float2 local_uv = ((p_local_ppx / half_size_ppx) * 0.5) + float2(0.5);
|
||||
float2 uv = mix(uv_rect.xy, uv_rect.zw, local_uv);
|
||||
shape_color = in.f_color * tex.sample(texSmplr, uv);
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -60,32 +60,21 @@ struct main0_in
|
||||
float4 v_color [[attribute(2)]];
|
||||
};
|
||||
|
||||
vertex main0_out main0(main0_in in [[stage_in]], constant Uniforms& _12 [[buffer(0)]], const device Core_2D_Primitives& _75 [[buffer(1)]], uint gl_InstanceIndex [[instance_id]])
|
||||
vertex main0_out main0(main0_in in [[stage_in]], constant Uniforms& _12 [[buffer(0)]], const device Core_2D_Primitives& _31 [[buffer(1)]], uint gl_InstanceIndex [[instance_id]])
|
||||
{
|
||||
main0_out out = {};
|
||||
if (_12.mode == 0u)
|
||||
{
|
||||
out.f_color = in.v_color;
|
||||
out.f_local_or_uv = in.v_uv;
|
||||
out.f_params = float4(0.0);
|
||||
out.f_params2 = float4(0.0);
|
||||
out.f_flags = 0u;
|
||||
out.f_uv_rect = float4(0.0);
|
||||
out.f_effects = uint4(0u);
|
||||
out.gl_Position = _12.projection * float4(in.v_position * _12.dpi_scale, 0.0, 1.0);
|
||||
}
|
||||
else
|
||||
if (_12.mode == 1u)
|
||||
{
|
||||
Core_2D_Primitive p;
|
||||
p.bounds = _75.primitives[int(gl_InstanceIndex)].bounds;
|
||||
p.color = _75.primitives[int(gl_InstanceIndex)].color;
|
||||
p.flags = _75.primitives[int(gl_InstanceIndex)].flags;
|
||||
p.rotation_sc = _75.primitives[int(gl_InstanceIndex)].rotation_sc;
|
||||
p._pad = _75.primitives[int(gl_InstanceIndex)]._pad;
|
||||
p.params = _75.primitives[int(gl_InstanceIndex)].params;
|
||||
p.params2 = _75.primitives[int(gl_InstanceIndex)].params2;
|
||||
p.uv_rect = _75.primitives[int(gl_InstanceIndex)].uv_rect;
|
||||
p.effects = _75.primitives[int(gl_InstanceIndex)].effects;
|
||||
p.bounds = _31.primitives[int(gl_InstanceIndex)].bounds;
|
||||
p.color = _31.primitives[int(gl_InstanceIndex)].color;
|
||||
p.flags = _31.primitives[int(gl_InstanceIndex)].flags;
|
||||
p.rotation_sc = _31.primitives[int(gl_InstanceIndex)].rotation_sc;
|
||||
p._pad = _31.primitives[int(gl_InstanceIndex)]._pad;
|
||||
p.params = _31.primitives[int(gl_InstanceIndex)].params;
|
||||
p.params2 = _31.primitives[int(gl_InstanceIndex)].params2;
|
||||
p.uv_rect = _31.primitives[int(gl_InstanceIndex)].uv_rect;
|
||||
p.effects = _31.primitives[int(gl_InstanceIndex)].effects;
|
||||
float2 corner = in.v_position;
|
||||
float2 world_pos = mix(p.bounds.xy, p.bounds.zw, corner);
|
||||
float2 center = (p.bounds.xy + p.bounds.zw) * 0.5;
|
||||
@@ -105,6 +94,27 @@ vertex main0_out main0(main0_in in [[stage_in]], constant Uniforms& _12 [[buffer
|
||||
out.f_effects = p.effects;
|
||||
out.gl_Position = _12.projection * float4(world_pos * _12.dpi_scale, 0.0, 1.0);
|
||||
}
|
||||
else
|
||||
{
|
||||
out.f_color = in.v_color;
|
||||
out.f_local_or_uv = in.v_uv;
|
||||
out.f_params = float4(0.0);
|
||||
out.f_params2 = float4(0.0);
|
||||
out.f_flags = 0u;
|
||||
out.f_uv_rect = float4(0.0);
|
||||
out.f_effects = uint4(0u);
|
||||
float2 _199;
|
||||
if (_12.mode == 2u)
|
||||
{
|
||||
_199 = in.v_position;
|
||||
}
|
||||
else
|
||||
{
|
||||
_199 = in.v_position * _12.dpi_scale;
|
||||
}
|
||||
float2 pos = _199;
|
||||
out.gl_Position = _12.projection * float4(pos, 0.0, 1.0);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
@@ -40,9 +40,9 @@ const uint MAX_KERNEL_PAIRS = 32;
|
||||
// --- Inputs from vertex shader ---
|
||||
layout(location = 0) in vec2 p_local;
|
||||
layout(location = 1) in mediump vec4 f_color;
|
||||
layout(location = 2) flat in vec2 f_half_size;
|
||||
layout(location = 3) flat in vec4 f_radii;
|
||||
layout(location = 4) flat in float f_half_feather;
|
||||
layout(location = 2) flat in vec2 f_half_size_ppx;
|
||||
layout(location = 3) flat in vec4 f_radii_ppx;
|
||||
layout(location = 4) flat in float f_half_feather_ppx;
|
||||
|
||||
// --- Output ---
|
||||
layout(location = 0) out vec4 out_color;
|
||||
@@ -123,15 +123,15 @@ void main() {
|
||||
|
||||
// ---- Mode 1: composite per-primitive.
|
||||
// RRect SDF — early discard for fragments well outside the masked region.
|
||||
float d = sdRoundedBox(p_local, f_half_size, f_radii);
|
||||
if (d > f_half_feather) {
|
||||
float d = sdRoundedBox(p_local, f_half_size_ppx, f_radii_ppx);
|
||||
if (d > f_half_feather_ppx) {
|
||||
discard;
|
||||
}
|
||||
|
||||
// fwidth-based normalization for AA (matches main pipeline approach).
|
||||
float grad_magnitude = max(fwidth(d), 1e-6);
|
||||
float d_n = d / grad_magnitude;
|
||||
float h_n = f_half_feather / grad_magnitude;
|
||||
float h_n = f_half_feather_ppx / grad_magnitude;
|
||||
|
||||
// Sample the fully-blurred working-res texture. gl_FragCoord is full-res; convert to
|
||||
// working-res UV via inv_downsample_factor. No kernel is applied — the H+V blur passes
|
||||
|
||||
@@ -24,12 +24,12 @@
|
||||
layout(location = 0) out vec2 p_local;
|
||||
// f_color: tint, unpacked from primitive.color. Only meaningful in mode 1.
|
||||
layout(location = 1) out mediump vec4 f_color;
|
||||
// f_half_size: RRect half extents in physical pixels (mode 1 only).
|
||||
layout(location = 2) flat out vec2 f_half_size;
|
||||
// f_radii: per-corner radii in physical pixels (mode 1 only).
|
||||
layout(location = 3) flat out vec4 f_radii;
|
||||
// f_half_feather: SDF anti-aliasing feather (mode 1 only).
|
||||
layout(location = 4) flat out float f_half_feather;
|
||||
// f_half_size_ppx: RRect half extents in physical pixels (mode 1 only).
|
||||
layout(location = 2) flat out vec2 f_half_size_ppx;
|
||||
// f_radii_ppx: per-corner radii in physical pixels (mode 1 only).
|
||||
layout(location = 3) flat out vec4 f_radii_ppx;
|
||||
// f_half_feather_ppx: SDF anti-aliasing feather in physical pixels (mode 1 only).
|
||||
layout(location = 4) flat out float f_half_feather_ppx;
|
||||
|
||||
// --- Uniforms (set 1) ---
|
||||
// Backdrop pipeline's own uniform block — distinct from the main pipeline's
|
||||
@@ -53,10 +53,10 @@ layout(set = 1, binding = 0) uniform Uniforms {
|
||||
// edge effects (e.g. liquid-glass-style refraction outlines) would be a dedicated
|
||||
// primitive type with its own pipeline rather than a flag bit here.
|
||||
struct Gaussian_Blur_Primitive {
|
||||
vec4 bounds; // 0-15: min_xy, max_xy (world-space)
|
||||
vec4 radii; // 16-31: per-corner radii (physical px)
|
||||
vec2 half_size; // 32-39: RRect half extents (physical px)
|
||||
float half_feather; // 40-43: SDF anti-aliasing feather (physical px)
|
||||
vec4 bounds; // 0-15: min_xy, max_xy (world-space, logical px)
|
||||
vec4 radii_ppx; // 16-31: per-corner radii
|
||||
vec2 half_size_ppx; // 32-39: RRect half extents
|
||||
float half_feather_ppx; // 40-43: SDF anti-aliasing feather
|
||||
uint color; // 44-47: tint, packed RGBA u8x4
|
||||
};
|
||||
|
||||
@@ -78,9 +78,9 @@ void main() {
|
||||
// Mode 0 doesn't read the per-primitive varyings; zero-init for safety.
|
||||
p_local = vec2(0.0);
|
||||
f_color = vec4(0.0);
|
||||
f_half_size = vec2(0.0);
|
||||
f_radii = vec4(0.0);
|
||||
f_half_feather = 0.0;
|
||||
f_half_size_ppx = vec2(0.0);
|
||||
f_radii_ppx = vec4(0.0);
|
||||
f_half_feather_ppx = 0.0;
|
||||
} else {
|
||||
// ---- Mode 1: V-composite instanced unit-quad over Gaussian_Blur_Primitive ----
|
||||
Gaussian_Blur_Primitive p = primitives[gl_InstanceIndex];
|
||||
@@ -101,9 +101,9 @@ void main() {
|
||||
p_local = (world_pos - center) * dpi_scale;
|
||||
|
||||
f_color = unpackUnorm4x8(p.color);
|
||||
f_half_size = p.half_size;
|
||||
f_radii = p.radii;
|
||||
f_half_feather = p.half_feather;
|
||||
f_half_size_ppx = p.half_size_ppx;
|
||||
f_radii_ppx = p.radii_ppx;
|
||||
f_half_feather_ppx = p.half_feather_ppx;
|
||||
|
||||
gl_Position = projection * vec4(world_pos * dpi_scale, 0.0, 1.0);
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ float sdRegularPolygon(vec2 p, float r, float n) {
|
||||
return length(p) * cos(bn) - r;
|
||||
}
|
||||
|
||||
// Coverage from SDF distance using half-feather width (feather_px * 0.5, pre-computed on CPU).
|
||||
// Coverage from SDF distance using half-feather width (feather_ppx * 0.5, pre-computed on CPU).
|
||||
// Produces a symmetric transition centered on d=0: smoothstep(-h, h, d).
|
||||
float sdf_alpha(float d, float h) {
|
||||
return 1.0 - smoothstep(-h, h, d);
|
||||
@@ -80,56 +80,56 @@ void main() {
|
||||
|
||||
// SDF path — dispatch on kind
|
||||
float d = 1e30;
|
||||
float h = 0.5; // half-feather width; overwritten per shape kind
|
||||
vec2 half_size = f_params.xy; // used by RRect and as reference size for gradients
|
||||
float h = 0.5; // half-feather width (physical px); overwritten per shape kind
|
||||
vec2 half_size_ppx = f_params.xy; // used by RRect and as reference size for gradients
|
||||
|
||||
vec2 p_local = f_local_or_uv; // arrives rotated; vertex shader handled .Rotated
|
||||
vec2 p_local_ppx = f_local_or_uv; // arrives rotated; vertex shader handled .Rotated
|
||||
|
||||
if (kind == 1u) {
|
||||
// RRect — half_feather in params2.z
|
||||
vec4 corner_radii = vec4(f_params.zw, f_params2.xy);
|
||||
// RRect — half_feather_ppx in params2.z
|
||||
vec4 corner_radii_ppx = vec4(f_params.zw, f_params2.xy);
|
||||
h = f_params2.z;
|
||||
d = sdRoundedBox(p_local, half_size, corner_radii);
|
||||
d = sdRoundedBox(p_local_ppx, half_size_ppx, corner_radii_ppx);
|
||||
}
|
||||
else if (kind == 2u) {
|
||||
// NGon — half_feather in params.z
|
||||
float radius = f_params.x;
|
||||
// NGon — half_feather_ppx in params.z
|
||||
float radius_ppx = f_params.x;
|
||||
float sides = f_params.y;
|
||||
h = f_params.z;
|
||||
d = sdRegularPolygon(p_local, radius, sides);
|
||||
half_size = vec2(radius); // for gradient UV computation
|
||||
d = sdRegularPolygon(p_local_ppx, radius_ppx, sides);
|
||||
half_size_ppx = vec2(radius_ppx); // for gradient UV computation
|
||||
}
|
||||
else if (kind == 3u) {
|
||||
// Ellipse — half_feather in params.z
|
||||
vec2 ab = f_params.xy;
|
||||
// Ellipse — half_feather_ppx in params.z
|
||||
vec2 radii_ppx = f_params.xy;
|
||||
h = f_params.z;
|
||||
d = sdEllipseApprox(p_local, ab);
|
||||
half_size = ab; // for gradient UV computation
|
||||
d = sdEllipseApprox(p_local_ppx, radii_ppx);
|
||||
half_size_ppx = radii_ppx; // for gradient UV computation
|
||||
}
|
||||
else if (kind == 4u) {
|
||||
// Ring_Arc — half_feather in params2.z
|
||||
// Ring_Arc — half_feather_ppx in params2.z
|
||||
// Arc mode from flag bits 5-6: 0 = full, 1 = narrow (≤π), 2 = wide (>π)
|
||||
float inner = f_params.x;
|
||||
float outer = f_params.y;
|
||||
float inner_radius_ppx = f_params.x;
|
||||
float outer_radius_ppx = f_params.y;
|
||||
vec2 n_start = f_params.zw;
|
||||
vec2 n_end = f_params2.xy;
|
||||
uint arc_bits = (flags >> 5u) & 3u;
|
||||
|
||||
h = f_params2.z;
|
||||
|
||||
float r = length(p_local);
|
||||
d = max(inner - r, r - outer);
|
||||
float r = length(p_local_ppx);
|
||||
d = max(inner_radius_ppx - r, r - outer_radius_ppx);
|
||||
|
||||
if (arc_bits != 0u) {
|
||||
float d_start = dot(p_local, n_start);
|
||||
float d_end = dot(p_local, n_end);
|
||||
float d_start = dot(p_local_ppx, n_start);
|
||||
float d_end = dot(p_local_ppx, n_end);
|
||||
float d_wedge = (arc_bits == 1u)
|
||||
? max(d_start, d_end) // arc ≤ π: intersect half-planes
|
||||
: min(d_start, d_end); // arc > π: union half-planes
|
||||
d = max(d, d_wedge);
|
||||
}
|
||||
|
||||
half_size = vec2(outer); // for gradient UV computation
|
||||
half_size_ppx = vec2(outer_radius_ppx); // for gradient UV computation
|
||||
}
|
||||
|
||||
// --- fwidth-based normalization for correct AA and stroke width ---
|
||||
@@ -146,18 +146,18 @@ void main() {
|
||||
|
||||
if ((flags & 4u) != 0u) {
|
||||
// Radial gradient (bit 2): t from distance to center
|
||||
mediump float t = length(p_local / half_size);
|
||||
mediump float t = length(p_local_ppx / half_size_ppx);
|
||||
shape_color = gradient_2color(gradient_start, gradient_end, t);
|
||||
} else {
|
||||
// Linear gradient: direction pre-computed on CPU as (cos, sin) f16 pair
|
||||
vec2 direction = unpackHalf2x16(f_effects.z);
|
||||
mediump float t = dot(p_local / half_size, direction) * 0.5 + 0.5;
|
||||
mediump float t = dot(p_local_ppx / half_size_ppx, direction) * 0.5 + 0.5;
|
||||
shape_color = gradient_2color(gradient_start, gradient_end, t);
|
||||
}
|
||||
} else if ((flags & 1u) != 0u) {
|
||||
// Textured (bit 0)
|
||||
vec4 uv_rect = f_uv_rect;
|
||||
vec2 local_uv = p_local / half_size * 0.5 + 0.5;
|
||||
vec2 local_uv = p_local_ppx / half_size_ppx * 0.5 + 0.5;
|
||||
vec2 uv = mix(uv_rect.xy, uv_rect.zw, local_uv);
|
||||
shape_color = f_color * texture(tex, uv);
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#version 450 core
|
||||
|
||||
// ---------- Vertex attributes (used in both modes) ----------
|
||||
// ---------- Vertex attributes (used in all modes) ----------
|
||||
layout(location = 0) in vec2 v_position;
|
||||
layout(location = 1) in vec2 v_uv;
|
||||
layout(location = 2) in vec4 v_color;
|
||||
@@ -16,10 +16,18 @@ layout(location = 6) flat out vec4 f_uv_rect;
|
||||
layout(location = 7) flat out uvec4 f_effects;
|
||||
|
||||
// ---------- Uniforms (single block — avoids spirv-cross reordering on Metal) ----------
|
||||
// Mode values mirror Core_2D_Mode in core_2d.odin:
|
||||
// 0 = Tessellated v_position is in logical pixels; shader scales by dpi_scale.
|
||||
// 1 = SDF v_position is a unit-quad corner; world-space comes from
|
||||
// primitives[gl_InstanceIndex].bounds (logical px). Shader
|
||||
// scales by dpi_scale.
|
||||
// 2 = Text v_position is in *physical* pixels already (the CPU baked
|
||||
// the anchor snap and SDL_ttf glyph offsets, both physical).
|
||||
// Shader must NOT rescale.
|
||||
layout(set = 1, binding = 0) uniform Uniforms {
|
||||
mat4 projection;
|
||||
float dpi_scale;
|
||||
uint mode; // 0 = tessellated, 1 = SDF
|
||||
uint mode;
|
||||
};
|
||||
|
||||
// ---------- SDF primitive storage buffer ----------
|
||||
@@ -44,18 +52,7 @@ layout(std430, set = 0, binding = 0) readonly buffer Core_2D_Primitives {
|
||||
|
||||
// ---------- Entry point ----------
|
||||
void main() {
|
||||
if (mode == 0u) {
|
||||
// ---- Mode 0: Tessellated (used for text and arbitrary user geometry) ----
|
||||
f_color = v_color;
|
||||
f_local_or_uv = v_uv;
|
||||
f_params = vec4(0.0);
|
||||
f_params2 = vec4(0.0);
|
||||
f_flags = 0u;
|
||||
f_uv_rect = vec4(0.0);
|
||||
f_effects = uvec4(0);
|
||||
|
||||
gl_Position = projection * vec4(v_position * dpi_scale, 0.0, 1.0);
|
||||
} else {
|
||||
if (mode == 1u) {
|
||||
// ---- Mode 1: SDF instanced quads ----
|
||||
Core_2D_Primitive p = primitives[gl_InstanceIndex];
|
||||
|
||||
@@ -86,5 +83,25 @@ void main() {
|
||||
f_effects = p.effects;
|
||||
|
||||
gl_Position = projection * vec4(world_pos * dpi_scale, 0.0, 1.0);
|
||||
} else {
|
||||
// ---- Mode 0 (Tessellated) and Mode 2 (Text) ----
|
||||
// Both feed the raw-vertex pipeline (kind 0 in the fragment shader).
|
||||
// They differ only in what coord space `v_position` is in:
|
||||
// Mode 0 — logical pixels, scale here by dpi_scale.
|
||||
// Mode 2 — physical pixels (CPU pre-scaled and snapped to integer
|
||||
// physical pixels for atlas-aligned bilinear sampling).
|
||||
// Do NOT rescale.
|
||||
// `mode` is uniform across the workgroup, so the select compiles to a
|
||||
// uniform-controlled branch with no SIMT divergence cost.
|
||||
f_color = v_color;
|
||||
f_local_or_uv = v_uv;
|
||||
f_params = vec4(0.0);
|
||||
f_params2 = vec4(0.0);
|
||||
f_flags = 0u;
|
||||
f_uv_rect = vec4(0.0);
|
||||
f_effects = uvec4(0);
|
||||
|
||||
vec2 pos = (mode == 2u) ? v_position : (v_position * dpi_scale);
|
||||
gl_Position = projection * vec4(pos, 0.0, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
+40
-10
@@ -21,8 +21,8 @@ auto_segments :: proc(radius: f32, arc_degrees: f32) -> int {
|
||||
|
||||
// ----- Internal helpers -----
|
||||
|
||||
// Color is premultiplied: the tessellated fragment shader passes it through directly
|
||||
// and the blend state is ONE, ONE_MINUS_SRC_ALPHA.
|
||||
// Premultiplies the color before storing it on the vertex (see draw package doc's
|
||||
// "Color and blending" section for why).
|
||||
//INTERNAL
|
||||
solid_vertex :: proc(position: draw.Vec2, color: draw.Color) -> draw.Vertex_2D {
|
||||
return draw.Vertex_2D{position = position, color = draw.premultiply_color(color)}
|
||||
@@ -108,16 +108,23 @@ triangle :: proc(
|
||||
draw.prepare_shape(layer, vertices[:])
|
||||
}
|
||||
|
||||
// Draw an anti-aliased triangle via extruded edge quads.
|
||||
// Draw an anti-aliased triangle via extruded edge quads plus corner fan caps.
|
||||
// Interior vertices get the full premultiplied color; outer fringe vertices get BLANK (0,0,0,0).
|
||||
// The rasterizer linearly interpolates between them, producing a smooth 1-pixel AA band.
|
||||
// `aa_px` controls the extrusion width in logical pixels (default 1.0).
|
||||
// This proc emits 21 vertices (3 interior + 6 edge quads × 3 verts each).
|
||||
// The rasterizer linearly interpolates between them, producing a smooth ~1-physical-pixel AA band.
|
||||
// `aa_ppx` controls the extrusion width in *physical* pixels (default 1.0). The CPU divides by
|
||||
// `dpi_scaling` here so the vertex stream stays in logical px; the mode-0 vertex shader scales
|
||||
// back to physical at draw time. Net AA band is ~aa_ppx physical pixels regardless of DPI.
|
||||
//
|
||||
// Topology: 3 interior verts + 6 edge-quad triangles (×3 verts) + 3 corner-fan triangles (×3 verts)
|
||||
// = 30 verts total. The corner fans plug the wedge gaps that would otherwise appear between
|
||||
// adjacent edge fringes at each triangle vertex; without them, sharp corners show a small
|
||||
// background-colored crescent. Apex vertex is full color, both fringe verts are BLANK, so the
|
||||
// fan rasterizes as an alpha-falloff triangle that blends visually into the adjacent edge bands.
|
||||
triangle_aa :: proc(
|
||||
layer: ^draw.Layer,
|
||||
v1, v2, v3: draw.Vec2,
|
||||
color: draw.Color,
|
||||
aa_px: f32 = draw.DFT_FEATHER_PX,
|
||||
aa_ppx: f32 = draw.DFT_FEATHER_PPX,
|
||||
origin: draw.Vec2 = {},
|
||||
rotation: f32 = 0,
|
||||
) {
|
||||
@@ -164,7 +171,9 @@ triangle_aa :: proc(
|
||||
normal_12 := edge_normal(p1, p2, centroid_x, centroid_y)
|
||||
normal_20 := edge_normal(p2, p0, centroid_x, centroid_y)
|
||||
|
||||
extrude_distance := aa_px * draw.GLOB.dpi_scaling
|
||||
// aa_ppx is in physical pixels; divide by dpi_scaling so the extrusion lives in logical-pixel
|
||||
// space (the mode-0 vertex shader will scale back to physical at draw time).
|
||||
extrude_distance := aa_ppx / draw.GLOB.dpi_scaling
|
||||
|
||||
// Outer fringe vertices: each edge vertex extruded outward
|
||||
outer_0_01 := p0 + normal_01 * extrude_distance
|
||||
@@ -178,8 +187,8 @@ triangle_aa :: proc(
|
||||
// Outer fringe is BLANK = {0,0,0,0} which is already premul.
|
||||
transparent := draw.BLANK
|
||||
|
||||
// 3 interior + 6 × 3 edge-quad = 21 vertices
|
||||
vertices: [21]draw.Vertex_2D
|
||||
// 3 interior + 6 edge-quad tris (×3 verts) + 3 corner-fan tris (×3 verts) = 30 vertices
|
||||
vertices: [30]draw.Vertex_2D
|
||||
|
||||
// Interior triangle
|
||||
vertices[0] = solid_vertex(p0, color)
|
||||
@@ -210,6 +219,27 @@ triangle_aa :: proc(
|
||||
vertices[19] = solid_vertex(outer_0_20, transparent)
|
||||
vertices[20] = solid_vertex(outer_2_20, transparent)
|
||||
|
||||
// Corner fan caps: each fills the wedge gap between the two edge fringes meeting at a
|
||||
// triangle vertex. Apex is full color; both fringe verts are BLANK, so the rasterizer
|
||||
// produces a smooth alpha falloff across the wedge (matches the adjacent edge-band
|
||||
// gradients at the shared edges, so the seams are invisible). Vertex order per fan:
|
||||
// [apex, fringe-from-incoming-edge, fringe-from-outgoing-edge].
|
||||
|
||||
// Cap at p0 (between incoming edge p2→p0 and outgoing edge p0→p1)
|
||||
vertices[21] = solid_vertex(p0, color)
|
||||
vertices[22] = solid_vertex(outer_0_20, transparent)
|
||||
vertices[23] = solid_vertex(outer_0_01, transparent)
|
||||
|
||||
// Cap at p1 (between incoming edge p0→p1 and outgoing edge p1→p2)
|
||||
vertices[24] = solid_vertex(p1, color)
|
||||
vertices[25] = solid_vertex(outer_1_01, transparent)
|
||||
vertices[26] = solid_vertex(outer_1_12, transparent)
|
||||
|
||||
// Cap at p2 (between incoming edge p1→p2 and outgoing edge p2→p0)
|
||||
vertices[27] = solid_vertex(p2, color)
|
||||
vertices[28] = solid_vertex(outer_2_12, transparent)
|
||||
vertices[29] = solid_vertex(outer_2_20, transparent)
|
||||
|
||||
draw.prepare_shape(layer, vertices[:])
|
||||
}
|
||||
|
||||
|
||||
@@ -56,16 +56,6 @@ Texture_Slot :: struct {
|
||||
// GLOB.pending_texture_releases : [dynamic]Texture_Id
|
||||
// GLOB.samplers : [SAMPLER_PRESET_COUNT]^sdl.GPUSampler
|
||||
|
||||
Clay_Image_Data :: struct {
|
||||
texture_id: Texture_Id,
|
||||
fit: Fit_Mode,
|
||||
tint: Color,
|
||||
}
|
||||
|
||||
clay_image_data :: proc(id: Texture_Id, fit: Fit_Mode = .Stretch, tint: Color = WHITE) -> Clay_Image_Data {
|
||||
return {texture_id = id, fit = fit, tint = tint}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// ----- Registration -------------
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
+105
-30
@@ -120,10 +120,52 @@ spinlock_try_lock :: #force_inline proc "contextless" (lock: ^Spinlock) -> bool
|
||||
return lock_acquired
|
||||
}
|
||||
|
||||
// Spins until the lock is acquired, relaxing the CPU between attempts.
|
||||
spinlock_lock :: #force_inline proc "contextless" (lock: ^Spinlock) {
|
||||
for !spinlock_try_lock(lock) {
|
||||
intrinsics.cpu_relax()
|
||||
}
|
||||
}
|
||||
|
||||
spinlock_unlock :: #force_inline proc "contextless" (lock: ^Spinlock) {
|
||||
intrinsics.atomic_store_explicit(lock, false, .Release)
|
||||
}
|
||||
|
||||
// Spins until the lock is acquired, then unlocks at the end of the calling scope. Always returns
|
||||
// true so it can guard a critical section from within an `if`:
|
||||
//
|
||||
// if spinlock_guard(&lock) {
|
||||
// // critical section
|
||||
// }
|
||||
@(deferred_in = spinlock_unlock)
|
||||
spinlock_guard :: #force_inline proc "contextless" (lock: ^Spinlock) -> bool {
|
||||
spinlock_lock(lock)
|
||||
return true
|
||||
}
|
||||
|
||||
// Tries to acquire the lock once without spinning. Returns true and unlocks at the end of the
|
||||
// calling scope if acquired, otherwise returns false and does nothing:
|
||||
//
|
||||
// if spinlock_try_guard(&lock) {
|
||||
// // critical section, entered only if the lock was acquired
|
||||
// }
|
||||
@(deferred_in_out = spinlock_try_guard_unlock)
|
||||
spinlock_try_guard :: #force_inline proc "contextless" (lock: ^Spinlock) -> bool {
|
||||
return spinlock_try_lock(lock)
|
||||
}
|
||||
|
||||
// Deferred companion of `spinlock_try_guard`; unlocks only when the lock was actually acquired.
|
||||
@(private)
|
||||
spinlock_try_guard_unlock :: #force_inline proc "contextless" (lock: ^Spinlock, locked: bool) {
|
||||
if locked {
|
||||
spinlock_unlock(lock)
|
||||
}
|
||||
}
|
||||
|
||||
lock :: proc {
|
||||
spinlock_lock,
|
||||
}
|
||||
|
||||
try_lock :: proc {
|
||||
spinlock_try_lock,
|
||||
}
|
||||
@@ -132,6 +174,14 @@ unlock :: proc {
|
||||
spinlock_unlock,
|
||||
}
|
||||
|
||||
guard :: proc {
|
||||
spinlock_guard,
|
||||
}
|
||||
|
||||
try_guard :: proc {
|
||||
spinlock_try_guard,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// ----- Tests ------------------------
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
@@ -139,10 +189,10 @@ import "core:sync"
|
||||
import "core:testing"
|
||||
import "core:thread"
|
||||
|
||||
@(test)
|
||||
test_concurrent_atomic_add_no_lost_updates :: proc(t: ^testing.T) {
|
||||
// Multiple threads will each add 1.0 this many times.
|
||||
// If any updates are lost due to race conditions, the final sum will be wrong.
|
||||
@(test)
|
||||
test_concurrent_atomic_add_no_lost_updates :: proc(t: ^testing.T) {
|
||||
NUM_THREADS :: 8
|
||||
ITERATIONS_PER_THREAD :: 10_000
|
||||
|
||||
@@ -184,10 +234,10 @@ test_concurrent_atomic_add_no_lost_updates :: proc(t: ^testing.T) {
|
||||
testing.expect_value(t, shared_value, expected)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_concurrent_atomic_sub_no_lost_updates :: proc(t: ^testing.T) {
|
||||
// Start with a known value, multiple threads subtract.
|
||||
// If any updates are lost due to race conditions, the final result will be wrong.
|
||||
@(test)
|
||||
test_concurrent_atomic_sub_no_lost_updates :: proc(t: ^testing.T) {
|
||||
NUM_THREADS :: 8
|
||||
ITERATIONS_PER_THREAD :: 10_000
|
||||
|
||||
@@ -228,11 +278,11 @@ test_concurrent_atomic_sub_no_lost_updates :: proc(t: ^testing.T) {
|
||||
testing.expect_value(t, shared_value, 0.0)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_concurrent_atomic_mul_div_round_trip :: proc(t: ^testing.T) {
|
||||
// Each thread multiplies by 2.0 then divides by 2.0.
|
||||
// Since these are inverses, the final value should equal the starting value
|
||||
// regardless of how operations interleave.
|
||||
@(test)
|
||||
test_concurrent_atomic_mul_div_round_trip :: proc(t: ^testing.T) {
|
||||
NUM_THREADS :: 8
|
||||
ITERATIONS_PER_THREAD :: 10_000
|
||||
|
||||
@@ -274,10 +324,10 @@ test_concurrent_atomic_mul_div_round_trip :: proc(t: ^testing.T) {
|
||||
testing.expect_value(t, shared_value, 1000.0)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_atomic_add_with_f32 :: proc(t: ^testing.T) {
|
||||
// Verify the f32 type dispatch works correctly under contention.
|
||||
// Same approach as the f64 add test but with f32.
|
||||
@(test)
|
||||
test_atomic_add_with_f32 :: proc(t: ^testing.T) {
|
||||
NUM_THREADS :: 8
|
||||
ITERATIONS_PER_THREAD :: 10_000
|
||||
|
||||
@@ -319,8 +369,6 @@ test_atomic_add_with_f32 :: proc(t: ^testing.T) {
|
||||
testing.expect_value(t, shared_value, expected)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_atomic_release_acquire_publish_visibility :: proc(t: ^testing.T) {
|
||||
// Tests that the memory order passed to atomic_float_op's CAS success condition
|
||||
// provides full ordering guarantees for the entire float operation.
|
||||
//
|
||||
@@ -330,6 +378,8 @@ test_atomic_release_acquire_publish_visibility :: proc(t: ^testing.T) {
|
||||
//
|
||||
// NOTE: This test may pass even with Relaxed ordering on x86 due to its strong memory model.
|
||||
// On ARM or other weak-memory architectures, using Relaxed here would likely cause failures.
|
||||
@(test)
|
||||
test_atomic_release_acquire_publish_visibility :: proc(t: ^testing.T) {
|
||||
NUM_READERS :: 4
|
||||
|
||||
Shared_State :: struct {
|
||||
@@ -426,10 +476,11 @@ test_atomic_release_acquire_publish_visibility :: proc(t: ^testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_spinlock_try_lock_mutual_exclusion :: proc(t: ^testing.T) {
|
||||
// Stress test for spinlock_try_lock: N threads spin-acquire the lock and
|
||||
// perform a deliberate non-atomic read-modify-write on shared data.
|
||||
// Stress test for every spinlock acquisition variant: N threads contend on a
|
||||
// single lock and perform a deliberate non-atomic read-modify-write on shared
|
||||
// data. Each iteration rotates through spinlock_try_lock, spinlock_lock,
|
||||
// spinlock_guard, and spinlock_try_guard so every variant runs concurrently and
|
||||
// must uphold mutual exclusion on the same lock.
|
||||
//
|
||||
// If mutual exclusion holds:
|
||||
// - `counter` ends at exactly NUM_THREADS * ITERATIONS_PER_THREAD
|
||||
@@ -437,6 +488,8 @@ test_spinlock_try_lock_mutual_exclusion :: proc(t: ^testing.T) {
|
||||
//
|
||||
// A multi-step RMW (read → relax → write) widens the critical section so
|
||||
// any failure to exclude is virtually guaranteed to corrupt the counter.
|
||||
@(test)
|
||||
test_spinlock_mutual_exclusion :: proc(t: ^testing.T) {
|
||||
NUM_THREADS :: 8
|
||||
ITERATIONS_PER_THREAD :: 50_000
|
||||
|
||||
@@ -461,21 +514,9 @@ test_spinlock_try_lock_mutual_exclusion :: proc(t: ^testing.T) {
|
||||
barrier: sync.Barrier
|
||||
sync.barrier_init(&barrier, NUM_THREADS)
|
||||
|
||||
thread_proc :: proc(th: ^thread.Thread) {
|
||||
ctx := cast(^Thread_Data)th.data
|
||||
s := ctx.shared
|
||||
|
||||
// All threads rendezvous here for maximum contention.
|
||||
sync.barrier_wait(ctx.barrier)
|
||||
|
||||
for _ in 0 ..< ITERATIONS_PER_THREAD {
|
||||
// Spin on try_lock until we acquire it.
|
||||
for !spinlock_try_lock(&s.lock) {
|
||||
intrinsics.cpu_relax()
|
||||
}
|
||||
|
||||
// --- critical section start ---
|
||||
|
||||
// The single critical section every acquisition variant must protect. Sharing
|
||||
// it guarantees they all stress the exact same non-atomic read-modify-write.
|
||||
critical_section :: proc(s: ^Shared) {
|
||||
// Atomically bump the holder count so we can detect overlapping holders.
|
||||
holders := intrinsics.atomic_add_explicit(&s.concurrent_holders, 1, .Relaxed)
|
||||
|
||||
@@ -494,10 +535,44 @@ test_spinlock_try_lock_mutual_exclusion :: proc(t: ^testing.T) {
|
||||
s.counter = val + 1
|
||||
|
||||
intrinsics.atomic_sub_explicit(&s.concurrent_holders, 1, .Relaxed)
|
||||
}
|
||||
|
||||
// --- critical section end ---
|
||||
thread_proc :: proc(th: ^thread.Thread) {
|
||||
ctx := cast(^Thread_Data)th.data
|
||||
s := ctx.shared
|
||||
|
||||
// All threads rendezvous here for maximum contention.
|
||||
sync.barrier_wait(ctx.barrier)
|
||||
|
||||
for i in 0 ..< ITERATIONS_PER_THREAD {
|
||||
// Rotate through every acquisition variant so they all contend on the
|
||||
// same lock simultaneously and must each uphold mutual exclusion.
|
||||
switch i & 3 {
|
||||
case 0:
|
||||
// Manual spin on try_lock until we acquire it.
|
||||
for !spinlock_try_lock(&s.lock) {
|
||||
intrinsics.cpu_relax()
|
||||
}
|
||||
critical_section(s)
|
||||
spinlock_unlock(&s.lock)
|
||||
case 1:
|
||||
// Blocking lock that loops internally until acquired.
|
||||
spinlock_lock(&s.lock)
|
||||
critical_section(s)
|
||||
spinlock_unlock(&s.lock)
|
||||
case 2: // Scoped guard: unlocks automatically at the end of the block.
|
||||
if spinlock_guard(&s.lock) {
|
||||
critical_section(s)
|
||||
}
|
||||
case 3: // Scoped try-guard: retry until acquired, auto-unlocks on success.
|
||||
for {
|
||||
if spinlock_try_guard(&s.lock) {
|
||||
critical_section(s)
|
||||
break
|
||||
}
|
||||
intrinsics.cpu_relax()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ package quantity
|
||||
|
||||
import "base:intrinsics"
|
||||
|
||||
LITERS_PER_GALLON :: 3.785411784
|
||||
|
||||
//----- Liters ----------------------------------
|
||||
Liters :: struct($V: typeid) where intrinsics.type_is_numeric(V) {
|
||||
v: V,
|
||||
@@ -14,6 +16,13 @@ liters_to_milli_liters :: #force_inline proc "contextless" (
|
||||
return Milli_Liters(V){liters.v * MILLI}
|
||||
}
|
||||
|
||||
@(private = "file")
|
||||
liters_to_gallons :: #force_inline proc "contextless" (
|
||||
liters: Liters($V),
|
||||
) -> Gallons(V) where intrinsics.type_is_float(V) {
|
||||
return Gallons(V){liters.v / LITERS_PER_GALLON}
|
||||
}
|
||||
|
||||
//----- Milliliters ----------------------------------
|
||||
Milli_Liters :: struct($V: typeid) where intrinsics.type_is_numeric(V) {
|
||||
v: V,
|
||||
@@ -26,17 +35,34 @@ milli_liters_to_liters :: #force_inline proc "contextless" (
|
||||
return Liters(V){milli_liters.v / MILLI}
|
||||
}
|
||||
|
||||
//----- Gallons ----------------------------------
|
||||
Gallons :: struct($V: typeid) where intrinsics.type_is_numeric(V) {
|
||||
v: V,
|
||||
}
|
||||
|
||||
@(private = "file")
|
||||
gallons_to_liters :: #force_inline proc "contextless" (
|
||||
gallons: Gallons($V),
|
||||
) -> Liters(V) where intrinsics.type_is_float(V) {
|
||||
return Liters(V){gallons.v * LITERS_PER_GALLON}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// ----- Conversion Overloads ------------------------
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
to_liters :: proc {
|
||||
milli_liters_to_liters,
|
||||
gallons_to_liters,
|
||||
}
|
||||
|
||||
to_milli_liters :: proc {
|
||||
liters_to_milli_liters,
|
||||
}
|
||||
|
||||
to_gallons :: proc {
|
||||
liters_to_gallons,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// ----- Tests ------------------------
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
@@ -57,3 +83,19 @@ test_milli_liters_to_liters :: proc(t: ^testing.T) {
|
||||
|
||||
testing.expect_value(t, liters, Liters(int){12})
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_gallons_to_liters :: proc(t: ^testing.T) {
|
||||
gallons := Gallons(f32){1}
|
||||
liters := to_liters(gallons)
|
||||
|
||||
testing.expect(t, liters.v > 3.78 && liters.v < 3.79)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_liters_to_gallons :: proc(t: ^testing.T) {
|
||||
liters := Liters(f32){3.785411784}
|
||||
gallons := to_gallons(liters)
|
||||
|
||||
testing.expect(t, gallons.v > 0.99 && gallons.v < 1.01)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,58 @@ package quantity
|
||||
|
||||
import "base:intrinsics"
|
||||
|
||||
//----- Liters Per Minute ----------------------------------
|
||||
Liters_Per_Minute :: struct($V: typeid) where intrinsics.type_is_numeric(V) {
|
||||
v: V,
|
||||
}
|
||||
|
||||
@(private = "file")
|
||||
liters_per_minute_to_gallons_per_minute :: #force_inline proc "contextless" (
|
||||
liters_per_minute: Liters_Per_Minute($V),
|
||||
) -> Gallons_Per_Minute(V) where intrinsics.type_is_float(V) {
|
||||
return Gallons_Per_Minute(V){liters_per_minute.v / LITERS_PER_GALLON}
|
||||
}
|
||||
|
||||
//----- Gallons Per Minute ----------------------------------
|
||||
Gallons_Per_Minute :: struct($V: typeid) where intrinsics.type_is_numeric(V) {
|
||||
v: V,
|
||||
}
|
||||
|
||||
@(private = "file")
|
||||
gallons_per_minute_to_liters_per_minute :: #force_inline proc "contextless" (
|
||||
gallons_per_minute: Gallons_Per_Minute($V),
|
||||
) -> Liters_Per_Minute(V) where intrinsics.type_is_float(V) {
|
||||
return Liters_Per_Minute(V){gallons_per_minute.v * LITERS_PER_GALLON}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// ----- Conversion Overloads ------------------------
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
to_liters_per_minute :: proc {
|
||||
gallons_per_minute_to_liters_per_minute,
|
||||
}
|
||||
|
||||
to_gallons_per_minute :: proc {
|
||||
liters_per_minute_to_gallons_per_minute,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// ----- Tests ------------------------
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
import "core:testing"
|
||||
|
||||
@(test)
|
||||
test_gallons_per_minute_to_liters_per_minute :: proc(t: ^testing.T) {
|
||||
gallons_per_minute := Gallons_Per_Minute(f32){1}
|
||||
liters_per_minute := to_liters_per_minute(gallons_per_minute)
|
||||
|
||||
testing.expect(t, liters_per_minute.v > 3.78 && liters_per_minute.v < 3.79)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_liters_per_minute_to_gallons_per_minute :: proc(t: ^testing.T) {
|
||||
liters_per_minute := Liters_Per_Minute(f32){3.785411784}
|
||||
gallons_per_minute := to_gallons_per_minute(liters_per_minute)
|
||||
|
||||
testing.expect(t, gallons_per_minute.v > 0.99 && gallons_per_minute.v < 1.01)
|
||||
}
|
||||
|
||||
Vendored
+140
-22
@@ -57,11 +57,6 @@ CornerRadius :: struct {
|
||||
bottomRight: c.float,
|
||||
}
|
||||
|
||||
BorderData :: struct {
|
||||
width: u32,
|
||||
color: Color,
|
||||
}
|
||||
|
||||
ElementId :: struct {
|
||||
id: u32,
|
||||
offset: u32,
|
||||
@@ -69,6 +64,12 @@ ElementId :: struct {
|
||||
stringId: String,
|
||||
}
|
||||
|
||||
ElementIdArray :: struct {
|
||||
capacity: i32,
|
||||
length: i32,
|
||||
internalArray: [^]ElementId,
|
||||
}
|
||||
|
||||
when ODIN_OS == .Windows {
|
||||
EnumBackingType :: u32
|
||||
} else {
|
||||
@@ -83,6 +84,8 @@ RenderCommandType :: enum EnumBackingType {
|
||||
Image,
|
||||
ScissorStart,
|
||||
ScissorEnd,
|
||||
OverlayColorStart,
|
||||
OverlayColorEnd,
|
||||
Custom,
|
||||
}
|
||||
|
||||
@@ -138,6 +141,92 @@ BorderElementConfig :: struct {
|
||||
width: BorderWidth,
|
||||
}
|
||||
|
||||
TransitionData :: struct {
|
||||
boundingBox: BoundingBox,
|
||||
backgroundColor: Color,
|
||||
overlayColor: Color,
|
||||
borderColor: Color,
|
||||
borderWidth: BorderWidth,
|
||||
}
|
||||
|
||||
TransitionState :: enum c.int {
|
||||
Idle,
|
||||
Entering,
|
||||
Transitioning,
|
||||
Exiting,
|
||||
}
|
||||
|
||||
TransitionProperty :: enum c.int {
|
||||
X,
|
||||
Y,
|
||||
Width,
|
||||
Height,
|
||||
BackgroundColor,
|
||||
OverlayColor,
|
||||
CornerRadius,
|
||||
BorderColor,
|
||||
BorderWidth,
|
||||
}
|
||||
|
||||
TransitionPropertyFlags :: bit_set[TransitionProperty;c.int]
|
||||
TransitionPropertyPosition :: TransitionPropertyFlags{.X, .Y}
|
||||
TransitionPropertyDimensions :: TransitionPropertyFlags{.Width, .Height}
|
||||
TransitionPropertyBoundingBox :: TransitionPropertyPosition + TransitionPropertyDimensions
|
||||
TransitionPropertyBorder :: TransitionPropertyFlags{.BorderColor, .BorderWidth}
|
||||
|
||||
TransitionCallbackArguments :: struct {
|
||||
transitionState: TransitionState,
|
||||
initial: TransitionData,
|
||||
current: ^TransitionData,
|
||||
target: TransitionData,
|
||||
elapsedTime: f32,
|
||||
duration: f32,
|
||||
properties: TransitionPropertyFlags,
|
||||
}
|
||||
|
||||
TransitionEnterTriggerType :: enum EnumBackingType {
|
||||
SkipOnFirstParentFrame,
|
||||
TriggerOnFirstParentFrame,
|
||||
}
|
||||
|
||||
TransitionExitTriggerType :: enum EnumBackingType {
|
||||
SkipWhenParentExits,
|
||||
TriggerWhenParentExits,
|
||||
}
|
||||
|
||||
TransitionInteractionHandlingType :: enum EnumBackingType {
|
||||
DisableInteractionsWhileTransitioningPosition,
|
||||
AllowInteractionsWhileTransitioningPosition,
|
||||
}
|
||||
|
||||
ExitTransitionSiblingOrdering :: enum EnumBackingType {
|
||||
UnderneathSiblings,
|
||||
NaturalOrder,
|
||||
AboveSiblings,
|
||||
}
|
||||
|
||||
TransitionElementConfig :: struct {
|
||||
handler: proc "c" (args: TransitionCallbackArguments) -> bool,
|
||||
duration: f32,
|
||||
properties: TransitionPropertyFlags,
|
||||
interactionHandling: TransitionInteractionHandlingType,
|
||||
enter: struct {
|
||||
setInitialState: proc "c" (
|
||||
initialState: TransitionData,
|
||||
properties: TransitionPropertyFlags,
|
||||
) -> TransitionData,
|
||||
trigger: TransitionEnterTriggerType,
|
||||
},
|
||||
exit: struct {
|
||||
setFinalState: proc "c" (
|
||||
finalState: TransitionData,
|
||||
properties: TransitionPropertyFlags,
|
||||
) -> TransitionData,
|
||||
trigger: TransitionExitTriggerType,
|
||||
siblingOrdering: ExitTransitionSiblingOrdering,
|
||||
},
|
||||
}
|
||||
|
||||
ClipElementConfig :: struct {
|
||||
horizontal: bool, // clip overflowing elements on the "X" axis
|
||||
vertical: bool, // clip overflowing elements on the "Y" axis
|
||||
@@ -215,6 +304,15 @@ CustomRenderData :: struct {
|
||||
customData: rawptr,
|
||||
}
|
||||
|
||||
ClipRenderData :: struct {
|
||||
horizontal: bool,
|
||||
vertical: bool,
|
||||
}
|
||||
|
||||
OverlayColorRenderData :: struct {
|
||||
color: Color,
|
||||
}
|
||||
|
||||
BorderRenderData :: struct {
|
||||
color: Color,
|
||||
cornerRadius: CornerRadius,
|
||||
@@ -227,6 +325,8 @@ RenderCommandData :: struct #raw_union {
|
||||
image: ImageRenderData,
|
||||
custom: CustomRenderData,
|
||||
border: BorderRenderData,
|
||||
clip: ClipRenderData,
|
||||
overlayColor: OverlayColorRenderData,
|
||||
}
|
||||
|
||||
RenderCommand :: struct {
|
||||
@@ -338,9 +438,9 @@ ClayArray :: struct($type: typeid) {
|
||||
}
|
||||
|
||||
ElementDeclaration :: struct {
|
||||
id: ElementId,
|
||||
layout: LayoutConfig,
|
||||
backgroundColor: Color,
|
||||
overlayColor: Color,
|
||||
cornerRadius: CornerRadius,
|
||||
aspectRatio: AspectRatioElementConfig,
|
||||
image: ImageElementConfig,
|
||||
@@ -348,6 +448,7 @@ ElementDeclaration :: struct {
|
||||
custom: CustomElementConfig,
|
||||
clip: ClipElementConfig,
|
||||
border: BorderElementConfig,
|
||||
transition: TransitionElementConfig,
|
||||
userData: rawptr,
|
||||
}
|
||||
|
||||
@@ -360,6 +461,7 @@ ErrorType :: enum EnumBackingType {
|
||||
FloatingContainerParentNotFound,
|
||||
PercentageOver1,
|
||||
InternalError,
|
||||
UnbalancedOpenClose,
|
||||
}
|
||||
|
||||
ErrorData :: struct {
|
||||
@@ -378,23 +480,27 @@ Context :: struct {} // opaque structure, only use as a pointer
|
||||
@(link_prefix = "Clay_", default_calling_convention = "c")
|
||||
foreign Clay {
|
||||
_OpenElement :: proc() ---
|
||||
_OpenElementWithId :: proc(id: ElementId) ---
|
||||
_CloseElement :: proc() ---
|
||||
MinMemorySize :: proc() -> u32 ---
|
||||
CreateArenaWithCapacityAndMemory :: proc(capacity: c.size_t, offset: [^]u8) -> Arena ---
|
||||
SetPointerState :: proc(position: Vector2, pointerDown: bool) ---
|
||||
GetPointerState :: proc() -> PointerData ---
|
||||
Initialize :: proc(arena: Arena, layoutDimensions: Dimensions, errorHandler: ErrorHandler) -> ^Context ---
|
||||
GetCurrentContext :: proc() -> ^Context ---
|
||||
SetCurrentContext :: proc(ctx: ^Context) ---
|
||||
UpdateScrollContainers :: proc(enableDragScrolling: bool, scrollDelta: Vector2, deltaTime: c.float) ---
|
||||
SetLayoutDimensions :: proc(dimensions: Dimensions) ---
|
||||
BeginLayout :: proc() ---
|
||||
EndLayout :: proc() -> ClayArray(RenderCommand) ---
|
||||
EndLayout :: proc(deltaTime: c.float) -> ClayArray(RenderCommand) ---
|
||||
GetOpenElementId :: proc() -> u32 ---
|
||||
GetElementId :: proc(id: String) -> ElementId ---
|
||||
GetElementIdWithIndex :: proc(id: String, index: u32) -> ElementId ---
|
||||
GetElementData :: proc(id: ElementId) -> ElementData ---
|
||||
Hovered :: proc() -> bool ---
|
||||
OnHover :: proc(onHoverFunction: proc "c" (id: ElementId, pointerData: PointerData, userData: rawptr), userData: rawptr) ---
|
||||
PointerOver :: proc(id: ElementId) -> bool ---
|
||||
GetPointerOverIds :: proc() -> ElementIdArray ---
|
||||
GetScrollOffset :: proc() -> Vector2 ---
|
||||
GetScrollContainerData :: proc(id: ElementId) -> ScrollContainerData ---
|
||||
SetMeasureTextFunction :: proc(measureTextFunction: proc "c" (text: StringSlice, config: ^TextElementConfig, userData: rawptr) -> Dimensions, userData: rawptr) ---
|
||||
@@ -408,15 +514,15 @@ foreign Clay {
|
||||
GetMaxMeasureTextCacheWordCount :: proc() -> i32 ---
|
||||
SetMaxMeasureTextCacheWordCount :: proc(maxMeasureTextCacheWordCount: i32) ---
|
||||
ResetMeasureTextCache :: proc() ---
|
||||
EaseOut :: proc(arguments: TransitionCallbackArguments) -> bool ---
|
||||
}
|
||||
|
||||
@(link_prefix = "Clay_", default_calling_convention = "c", private)
|
||||
foreign Clay {
|
||||
_ConfigureOpenElement :: proc(config: ElementDeclaration) ---
|
||||
_HashString :: proc(key: String, offset: u32, seed: u32) -> ElementId ---
|
||||
_OpenTextElement :: proc(text: String, textConfig: ^TextElementConfig) ---
|
||||
_StoreTextElementConfig :: proc(config: TextElementConfig) -> ^TextElementConfig ---
|
||||
_GetParentElementId :: proc() -> u32 ---
|
||||
_HashString :: proc(key: String, seed: u32) -> ElementId ---
|
||||
_HashStringWithOffset :: proc(key: String, index: u32, seed: u32) -> ElementId ---
|
||||
_OpenTextElement :: proc(text: String, textConfig: TextElementConfig) ---
|
||||
}
|
||||
|
||||
ConfigureOpenElement :: proc(config: ElementDeclaration) -> bool {
|
||||
@@ -425,25 +531,37 @@ ConfigureOpenElement :: proc(config: ElementDeclaration) -> bool {
|
||||
}
|
||||
|
||||
@(deferred_none = _CloseElement)
|
||||
UI :: proc() -> proc (config: ElementDeclaration) -> bool {
|
||||
UI_WithId :: proc(id: ElementId) -> proc(config: ElementDeclaration) -> bool {
|
||||
_OpenElementWithId(id)
|
||||
return ConfigureOpenElement
|
||||
}
|
||||
|
||||
@(deferred_none = _CloseElement)
|
||||
UI_AutoId :: proc() -> proc(config: ElementDeclaration) -> bool {
|
||||
_OpenElement()
|
||||
return ConfigureOpenElement
|
||||
}
|
||||
|
||||
Text :: proc($text: string, config: ^TextElementConfig) {
|
||||
UI :: proc {
|
||||
UI_WithId,
|
||||
UI_AutoId,
|
||||
}
|
||||
|
||||
Text :: proc {
|
||||
TextStatic,
|
||||
TextDynamic,
|
||||
}
|
||||
|
||||
TextStatic :: proc($text: string, config: TextElementConfig) {
|
||||
wrapped := MakeString(text)
|
||||
wrapped.isStaticallyAllocated = true
|
||||
_OpenTextElement(wrapped, config)
|
||||
}
|
||||
|
||||
TextDynamic :: proc(text: string, config: ^TextElementConfig) {
|
||||
TextDynamic :: proc(text: string, config: TextElementConfig) {
|
||||
_OpenTextElement(MakeString(text), config)
|
||||
}
|
||||
|
||||
TextConfig :: proc(config: TextElementConfig) -> ^TextElementConfig {
|
||||
return _StoreTextElementConfig(config)
|
||||
}
|
||||
|
||||
PaddingAll :: proc(allPadding: u16) -> Padding {
|
||||
return {left = allPadding, right = allPadding, top = allPadding, bottom = allPadding}
|
||||
}
|
||||
@@ -460,11 +578,11 @@ CornerRadiusAll :: proc(radius: f32) -> CornerRadius {
|
||||
return CornerRadius{radius, radius, radius, radius}
|
||||
}
|
||||
|
||||
SizingFit :: proc(sizeMinMax: SizingConstraintsMinMax) -> SizingAxis {
|
||||
SizingFit :: proc(sizeMinMax: SizingConstraintsMinMax = {}) -> SizingAxis {
|
||||
return SizingAxis{type = SizingType.Fit, constraints = {sizeMinMax = sizeMinMax}}
|
||||
}
|
||||
|
||||
SizingGrow :: proc(sizeMinMax: SizingConstraintsMinMax) -> SizingAxis {
|
||||
SizingGrow :: proc(sizeMinMax: SizingConstraintsMinMax = {}) -> SizingAxis {
|
||||
return SizingAxis{type = SizingType.Grow, constraints = {sizeMinMax = sizeMinMax}}
|
||||
}
|
||||
|
||||
@@ -481,9 +599,9 @@ MakeString :: proc(label: string) -> String {
|
||||
}
|
||||
|
||||
ID :: proc(label: string, index: u32 = 0) -> ElementId {
|
||||
return _HashString(MakeString(label), index, 0)
|
||||
return _HashString(MakeString(label), index)
|
||||
}
|
||||
|
||||
ID_LOCAL :: proc(label: string, index: u32 = 0) -> ElementId {
|
||||
return _HashString(MakeString(label), index, _GetParentElementId())
|
||||
return _HashStringWithOffset(MakeString(label), index, GetOpenElementId())
|
||||
}
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
-6
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/DanielGavin/ols/master/misc/odinfmt.schema.json",
|
||||
"character_width": 180,
|
||||
"sort_imports": true,
|
||||
"tabs": false
|
||||
}
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
+4
-4
@@ -68,9 +68,9 @@ main :: proc() {
|
||||
db_handle: mdb.Dbi
|
||||
// Put transaction
|
||||
key := 7
|
||||
key_val := mdb.blittable_val(&key)
|
||||
key_val := mdb.pod_val(&key)
|
||||
put_data := 12
|
||||
put_data_val := mdb.blittable_val(&put_data)
|
||||
put_data_val := mdb.pod_val(&put_data)
|
||||
mdb.panic_on_err(mdb.txn_begin(environment, nil, {}, &txn_handle))
|
||||
mdb.panic_on_err(mdb.dbi_open(txn_handle, nil, {}, &db_handle))
|
||||
mdb.panic_on_err(mdb.put(txn_handle, db_handle, &key_val, &put_data_val, {}))
|
||||
@@ -80,7 +80,7 @@ main :: proc() {
|
||||
data_val: mdb.Val
|
||||
mdb.panic_on_err(mdb.txn_begin(environment, nil, {}, &txn_handle))
|
||||
mdb.panic_on_err(mdb.get(txn_handle, db_handle, &key_val, &data_val))
|
||||
data_cpy := mdb.blittable_copy(&data_val, int)
|
||||
mdb.panic_on_err(mdb.txn_commit(txn_handle))
|
||||
data_cpy := mdb.pod_copy(data_val, int)
|
||||
mdb.txn_abort(txn_handle)
|
||||
fmt.println("Get result:", data_cpy)
|
||||
}
|
||||
|
||||
Vendored
+52
-15
@@ -169,58 +169,86 @@ import "core:fmt"
|
||||
import "core:reflect"
|
||||
import "core:sys/posix"
|
||||
|
||||
import b "../../basic"
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// ----- Added Odin Helpers ------------------------
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// Wrap a blittable value's bytes as an LMDB Val.
|
||||
// Wrap a POD value's bytes as an LMDB Val.
|
||||
// T must be a contiguous type with no indirection (no pointers, slices, strings, maps, etc.).
|
||||
blittable_val :: #force_inline proc(val_ptr: ^$T) -> Val {
|
||||
pod_val :: #force_inline proc(val_ptr: ^$T) -> Val {
|
||||
when ODIN_DEBUG {
|
||||
fmt.assertf(
|
||||
reflect.has_no_indirections(type_info_of(T)),
|
||||
"blitval: type '%v' contains indirection and cannot be stored directly in LMDB",
|
||||
"pod_val: type '%v' contains indirection and cannot be stored directly in LMDB",
|
||||
typeid_of(T),
|
||||
)
|
||||
}
|
||||
return Val{size_of(T), val_ptr}
|
||||
}
|
||||
|
||||
// Reads a blittable T out of the LMDB memory map by copying it into caller
|
||||
// Reads a POD T out of the LMDB memory map by copying it into caller
|
||||
// storage. The returned T has no lifetime tie to the transaction.
|
||||
blittable_copy :: #force_inline proc(val: ^Val, $T: typeid) -> T {
|
||||
pod_copy :: #force_inline proc(val: Val, $T: typeid) -> T {
|
||||
when ODIN_DEBUG {
|
||||
fmt.assertf(
|
||||
reflect.has_no_indirections(type_info_of(T)),
|
||||
"blitval_copy: type '%v' contains indirection and cannot be read directly from LMDB",
|
||||
"pod_copy: type '%v' contains indirection and cannot be read directly from LMDB",
|
||||
typeid_of(T),
|
||||
)
|
||||
}
|
||||
when b.ODIN_BOUNDS_CHECK {
|
||||
fmt.assertf(
|
||||
val.size == size_of(T),
|
||||
"size_of(%v) (%v) != val.size (%v)",
|
||||
typeid_of(T),
|
||||
size_of(T),
|
||||
val.size,
|
||||
)
|
||||
}
|
||||
return (cast(^T)val.data)^
|
||||
}
|
||||
|
||||
// Zero-copy pointer view into the LMDB memory map as a ^T.
|
||||
// Useful for large blittable types where you want to read individual fields
|
||||
// Useful for large POD types where you want to read individual fields
|
||||
// without copying the entire value (e.g. ptr.timestamp, ptr.flags).
|
||||
// MUST NOT be written through — writes either segfault (default env mode)
|
||||
// or silently corrupt the database (ENV_WRITEMAP).
|
||||
// MUST NOT be retained past txn_commit, txn_abort, or any subsequent write
|
||||
// operation on the same env — the pointer is invalidated.
|
||||
blittable_view :: #force_inline proc(val: ^Val, $T: typeid) -> ^T {
|
||||
pod_view :: #force_inline proc(val: Val, $T: typeid) -> ^T {
|
||||
when ODIN_DEBUG {
|
||||
fmt.assertf(
|
||||
reflect.has_no_indirections(type_info_of(T)),
|
||||
"blitval_view: type '%v' contains indirection and cannot be viewed directly from LMDB",
|
||||
"pod_view: type '%v' contains indirection and cannot be viewed directly from LMDB",
|
||||
typeid_of(T),
|
||||
)
|
||||
}
|
||||
when b.ODIN_BOUNDS_CHECK {
|
||||
fmt.assertf(
|
||||
val.size == size_of(T),
|
||||
"size_of(%v) (%v) != val.size (%v)",
|
||||
typeid_of(T),
|
||||
size_of(T),
|
||||
val.size,
|
||||
)
|
||||
}
|
||||
return cast(^T)val.data
|
||||
}
|
||||
|
||||
// Wrap a slice of blittable elements as an LMDB Val for use with put/get.
|
||||
// Wrap a slice of POD elements as an LMDB Val for use with put/get.
|
||||
// T must be a contiguous type with no indirection.
|
||||
// The caller's slice must remain valid (not freed, not resized) for the
|
||||
// duration of the put call that consumes this Val.
|
||||
slice_val :: #force_inline proc(s: []$T) -> Val {
|
||||
pod_slice_val :: #force_inline proc(s: []$T) -> Val {
|
||||
when ODIN_DEBUG {
|
||||
fmt.assertf(
|
||||
reflect.has_no_indirections(type_info_of(T)),
|
||||
"slice_val: element type '%v' contains indirection and cannot be stored directly in LMDB",
|
||||
"pod_slice_val: element type '%v' contains indirection and cannot be stored directly in LMDB",
|
||||
typeid_of(T),
|
||||
)
|
||||
}
|
||||
return Val{uint(len(s) * size_of(T)), raw_data(s)}
|
||||
}
|
||||
|
||||
@@ -231,12 +259,21 @@ slice_val :: #force_inline proc(s: []$T) -> Val {
|
||||
// MUST be copied (e.g. slice.clone) if it needs to outlive the current
|
||||
// transaction; the view is invalidated by txn_commit, txn_abort, or any
|
||||
// subsequent write operation on the same env.
|
||||
slice_view :: #force_inline proc(val: ^Val, $T: typeid) -> []T {
|
||||
pod_slice_view :: #force_inline proc(val: Val, $T: typeid) -> []T {
|
||||
when ODIN_DEBUG {
|
||||
fmt.assertf(
|
||||
reflect.has_no_indirections(type_info_of(T)),
|
||||
"slice_view: element type '%v' contains indirection and cannot be read directly from LMDB",
|
||||
"pod_slice_view: element type '%v' contains indirection and cannot be read directly from LMDB",
|
||||
typeid_of(T),
|
||||
)
|
||||
fmt.assertf(
|
||||
val.size % size_of(T) == 0,
|
||||
"pod_slice_view: val.size (%v) is not a multiple of size_of(%v) (%v)",
|
||||
val.size,
|
||||
typeid_of(T),
|
||||
size_of(T),
|
||||
)
|
||||
}
|
||||
return (cast([^]T)val.data)[:val.size / size_of(T)]
|
||||
}
|
||||
|
||||
@@ -253,7 +290,7 @@ string_val :: #force_inline proc(s: string) -> Val {
|
||||
// MUST be copied (e.g. strings.clone) if it needs to outlive the current
|
||||
// transaction; the view is invalidated by txn_commit, txn_abort, or any
|
||||
// subsequent write operation on the same env.
|
||||
string_view :: #force_inline proc(val: ^Val) -> string {
|
||||
string_view :: #force_inline proc(val: Val) -> string {
|
||||
return string((cast([^]u8)val.data)[:val.size])
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user