Compare commits

..

1 Commits

Author SHA1 Message Date
Zachary Levy c786147720 Initial draw package 2026-04-17 19:48:30 -07:00
92 changed files with 3933 additions and 14697 deletions
+1 -56
View File
@@ -32,14 +32,9 @@
"command": "odin test phased_executor -out=out/debug/test_phased_executor",
"cwd": "$ZED_WORKTREE_ROOT",
},
{
"label": "Test qrcode",
"command": "odin test qrcode -out=out/debug/test_qrcode",
"cwd": "$ZED_WORKTREE_ROOT",
},
{
"label": "Test all",
"command": "odin test many_bits -out=out/debug/test_many_bits && odin test ring -out=out/debug/test_ring && odin test levsort -out=out/debug/test_levsort && odin test levsync -out=out/debug/test_levsync && odin test levmath -out=out/debug/test_levmath && odin test phased_executor -out=out/debug/test_phased_executor && odin test qrcode -out=out/debug/test_qrcode",
"command": "odin test many_bits -out=out/debug/test_many_bits && odin test ring -out=out/debug/test_ring && odin test levsort -out=out/debug/test_levsort && odin test levsync -out=out/debug/test_levsync && odin test levmath -out=out/debug/test_levmath && odin test phased_executor -out=out/debug/test_phased_executor",
"cwd": "$ZED_WORKTREE_ROOT",
},
// ---------------------------------------------------------------------------------------------------------------------
@@ -60,56 +55,6 @@
"command": "odin run draw/examples -debug -out=out/debug/draw-examples -- hellope-shapes",
"cwd": "$ZED_WORKTREE_ROOT",
},
{
"label": "Run draw hellope-text example",
"command": "odin run draw/examples -debug -out=out/debug/draw-examples -- hellope-text",
"cwd": "$ZED_WORKTREE_ROOT",
},
{
"label": "Run draw hellope-custom example",
"command": "odin run draw/examples -debug -out=out/debug/draw-examples -- hellope-custom",
"cwd": "$ZED_WORKTREE_ROOT",
},
{
"label": "Run draw textures example",
"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",
"cwd": "$ZED_WORKTREE_ROOT",
},
{
"label": "Run draw gaussian-blur-debug example",
"command": "odin run draw/examples -debug -out=out/debug/draw-examples -- gaussian-blur-debug",
"cwd": "$ZED_WORKTREE_ROOT",
},
{
"label": "Run qrcode basic example",
"command": "odin run qrcode/examples -debug -out=out/debug/qrcode-examples -- basic",
"cwd": "$ZED_WORKTREE_ROOT",
},
{
"label": "Run qrcode variety example",
"command": "odin run qrcode/examples -debug -out=out/debug/qrcode-examples -- variety",
"cwd": "$ZED_WORKTREE_ROOT",
},
{
"label": "Run qrcode segment example",
"command": "odin run qrcode/examples -debug -out=out/debug/qrcode-examples -- segment",
"cwd": "$ZED_WORKTREE_ROOT",
},
{
"label": "Run qrcode mask example",
"command": "odin run qrcode/examples -debug -out=out/debug/qrcode-examples -- mask",
"cwd": "$ZED_WORKTREE_ROOT",
},
// ---------------------------------------------------------------------------------------------------------------------
// ----- Other ------------------------
// ---------------------------------------------------------------------------------------------------------------------
+164 -653
View File
File diff suppressed because it is too large Load Diff
-1185
View File
File diff suppressed because it is too large Load Diff
-794
View File
@@ -1,794 +0,0 @@
// 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,
)
}
-1613
View File
File diff suppressed because it is too large Load Diff
-756
View File
@@ -1,756 +0,0 @@
// CYBERSTEEL DESIGN SYSTEM — Odin theme constants
//
// Retrofuturist. Technical. Direct. Gruvbox-derived palette
// with Art Deco type system. Every visual token from the
// Cybersteel design system, transferred 1:1 to Odin constants.
//
// Conventions:
// - Colors are [4]u8 RGBA. Alpha 255 = fully opaque.
// Translucent tints carry their alpha in the 4th channel.
// - Times are time.Duration via core:time.
// - Pixel sizes, weights, line-heights, letter-spacings, and
// ratio-like values are plain (untyped) numeric literals so
// callers can use them with whatever numeric type they need.
// - Letter-spacing values are expressed in EMs (multiply by
// the resolved font size to get pixels).
// - Line-heights are unitless multipliers of the font size.
package cybersteel
import "core:time"
import draw ".."
// ============================================================
// BASE BACKGROUNDS — warm dark, Gruvbox-derived
// Never pure black. The warmth is intentional: aged metal,
// amber phosphor, old paper. Order is: deepest chrome first
// (shell), then page, then progressively lighter surfaces.
// ============================================================
// Topbar, sidebar, nav chrome, modal backdrops. Deepest base.
BG_SHELL :: draw.Color{0x1d, 0x20, 0x21, 0xff}
// Default page canvas / main content area. One step up from shell.
BG_PAGE :: draw.Color{0x31, 0x31, 0x31, 0xff}
// Cards, panels, drawers, input fields, code blocks, table rows.
// Slightly lighter than the page so raised surfaces read clearly
// without shadows.
BG_SURFACE :: draw.Color{0x3c, 0x38, 0x36, 0xff}
// Selected rows, active nav items, hover states. One step lighter
// than BG_SURFACE.
BG_ACTIVE :: draw.Color{0x50, 0x49, 0x45, 0xff}
// Disabled buttons / inputs background. Pairs with FG_MUTED text
// only — the contrast is intentionally low.
BG_DISABLED :: draw.Color{0x66, 0x5c, 0x54, 0xff}
// Borders, dividers, rules, input outlines. Never use as a text
// surface — it has no fg-pair guarantee.
BG_BORDER :: draw.Color{0x7c, 0x6f, 0x64, 0xff}
// ============================================================
// BASE FOREGROUNDS — warm cream / ivory, never pure white
// Five-step ramp from brightest (heading) to most muted.
// ============================================================
// Hero text, page headings, display titles. Brightest fg.
FG_HEADING :: draw.Color{0xfb, 0xf1, 0xc7, 0xff}
// Primary body text, default readable content.
FG_BODY :: draw.Color{0xf2, 0xe2, 0xba, 0xff}
// Labels, secondary descriptions, table data.
FG_SECONDARY :: draw.Color{0xe0, 0xd0, 0xa8, 0xff}
// Captions, metadata, timestamps, placeholders.
FG_CAPTION :: draw.Color{0xce, 0xbd, 0x9e, 0xff}
// Disabled text, token labels, subtle UI annotations.
FG_MUTED :: draw.Color{0xb8, 0xa9, 0x8e, 0xff}
// ============================================================
// ACCENT — GOLD (signature color, Art Deco)
// The defining accent of the system. Use sparingly: borders,
// highlights, focus rings, primary interactive states.
// ============================================================
// Primary interactive, focus rings, headline interactive accent.
GOLD_BRIGHT :: draw.Color{0xfa, 0xbd, 0x2f, 0xff}
// Borders, decorative rules, default Art Deco ornament color.
GOLD_DIM :: draw.Color{0xd7, 0x99, 0x21, 0xff}
// Hover states, pressed accents, dimmer gold contexts.
GOLD_MUTED :: draw.Color{0xb5, 0x76, 0x14, 0xff}
// Pure CRT amber. Reserved for terminal-style glow / phosphor
// references — distinct from gold ramp.
AMBER :: draw.Color{0xff, 0xb0, 0x00, 0xff}
// ============================================================
// ACCENT — RED (danger, errors, critical alerts)
// ============================================================
RED_BRIGHT :: draw.Color{0xfb, 0x49, 0x34, 0xff}
RED_DIM :: draw.Color{0xcc, 0x24, 0x1d, 0xff}
RED_MUTED :: draw.Color{0x9d, 0x00, 0x06, 0xff}
// ============================================================
// ACCENT — GREEN (success, safe, complete)
// ============================================================
GREEN_BRIGHT :: draw.Color{0xb8, 0xbb, 0x26, 0xff}
GREEN_DIM :: draw.Color{0x98, 0x97, 0x1a, 0xff}
GREEN_MUTED :: draw.Color{0x79, 0x74, 0x0e, 0xff}
// ============================================================
// ACCENT — BLUE / TEAL (info, links, cool technical elements)
// ============================================================
BLUE_BRIGHT :: draw.Color{0x83, 0xa5, 0x98, 0xff}
BLUE_DIM :: draw.Color{0x45, 0x85, 0x88, 0xff}
BLUE_MUTED :: draw.Color{0x07, 0x66, 0x78, 0xff}
// ============================================================
// ACCENT — ORANGE (warnings, in-progress, hot paths)
// ============================================================
ORANGE_BRIGHT :: draw.Color{0xfe, 0x80, 0x19, 0xff}
ORANGE_DIM :: draw.Color{0xd6, 0x5d, 0x0e, 0xff}
ORANGE_MUTED :: draw.Color{0xaf, 0x3a, 0x03, 0xff}
// ============================================================
// ACCENT — AQUA (cool secondary accent, fresh/active states)
// ============================================================
AQUA_BRIGHT :: draw.Color{0x8e, 0xc0, 0x7c, 0xff}
AQUA_DIM :: draw.Color{0x68, 0x9d, 0x6a, 0xff}
AQUA_MUTED :: draw.Color{0x42, 0x7b, 0x58, 0xff}
// ============================================================
// ACCENT — PURPLE (rare, for categorical / data-vis variety)
// ============================================================
PURPLE_BRIGHT :: draw.Color{0xd3, 0x86, 0x9b, 0xff}
PURPLE_DIM :: draw.Color{0xb1, 0x62, 0x86, 0xff}
PURPLE_MUTED :: draw.Color{0x8f, 0x3f, 0x71, 0xff}
// ============================================================
// SEMANTIC COLOR ROLES
// Aliases to accent ramps, named by intent. Prefer these in
// product code so meaning travels with the value.
// ============================================================
// Primary brand interactive — buttons, key links, focus ring.
COLOR_PRIMARY :: GOLD_BRIGHT
COLOR_PRIMARY_DIM :: GOLD_DIM
// Destructive / error / critical states.
COLOR_DANGER :: RED_BRIGHT
COLOR_DANGER_DIM :: RED_DIM
// Successful operation / safe state / completion.
COLOR_SUCCESS :: GREEN_BRIGHT
COLOR_SUCCESS_DIM :: GREEN_DIM
// Caution / in-progress / non-fatal anomaly.
COLOR_WARNING :: ORANGE_BRIGHT
COLOR_WARNING_DIM :: ORANGE_DIM
// Informational / neutral status / passive notice.
COLOR_INFO :: BLUE_BRIGHT
COLOR_INFO_DIM :: BLUE_DIM
// Hyperlinks at rest and on hover (links flip to gold on hover).
COLOR_LINK :: BLUE_BRIGHT
COLOR_LINK_HOVER :: GOLD_BRIGHT
// Keyboard / programmatic focus ring color.
COLOR_FOCUS :: GOLD_BRIGHT
// ============================================================
// SURFACE ROLES
// Semantic aliases for the bg ramp by usage role.
// ============================================================
SURFACE_PAGE :: BG_PAGE // root canvas
SURFACE_RAISED :: BG_SURFACE // cards, panels, inputs
SURFACE_OVERLAY :: BG_SHELL // modals, popovers, deep chrome
SURFACE_HOVER :: BG_ACTIVE // hovered raised surfaces
SURFACE_ACTIVE :: BG_SURFACE // pressed/active raised surfaces
// ============================================================
// BORDER ROLES
// Cybersteel borders are 1px solid, always crisp, always visible.
// Color carries the meaning; weight rarely changes.
// ============================================================
BORDER :: BG_BORDER // structural borders, default
BORDER_SUBTLE :: BG_DISABLED // very faint separators
BORDER_ACCENT :: GOLD_DIM // decorative / active edge
BORDER_FOCUS :: GOLD_BRIGHT // focus rings
BORDER_DANGER :: RED_DIM // destructive states
BORDER_SUCCESS :: GREEN_DIM // success states
// ============================================================
// TRANSLUCENT ACCENT TINTS
// Used for hover fills behind ghost buttons and for warm
// gradient overlays. Alpha encodes the tint strength.
// ============================================================
// 20% gold tint behind a hovered secondary button.
TINT_GOLD_HOVER :: draw.Color{0xd7, 0x99, 0x21, 0x33} // ~20% alpha
// 20% red tint behind a hovered danger ghost button.
TINT_DANGER_HOVER :: draw.Color{0xcc, 0x24, 0x1d, 0x33}
// 20% green tint behind a hovered success ghost button.
TINT_SUCCESS_HOVER :: draw.Color{0x98, 0x97, 0x1a, 0x33}
// 8% gold tint — top of the diagonal "gold fade" feature
// section overlay.
TINT_GOLD_FADE :: draw.Color{0xfa, 0xbd, 0x2f, 0x14} // ~8% alpha
// 6% amber tint — top of the vertical "amber fade" overlay.
TINT_AMBER_FADE :: draw.Color{0xff, 0xb0, 0x00, 0x0f} // ~6% alpha
// 4% gold tint — corner of card gradient.
TINT_GOLD_CARD :: draw.Color{0xfa, 0xbd, 0x2f, 0x0a} // ~4% alpha
// 3% black tint — scanline overlay stripe color.
TINT_SCANLINE :: draw.Color{0x00, 0x00, 0x00, 0x08} // ~3% alpha
// ============================================================
// SHADOWS
// Cybersteel is FLAT — no drop shadows. Elevation is expressed
// through bg + border only. The single permitted shadow use is
// a 1px gold ring as a focus / active indicator. Constants are
// kept here so callers don't reach for ad-hoc shadow values.
// ============================================================
// 1px inset gold ring — only permitted shadow, used as focus
// or selected-state outline. Width is 1px; color follows.
SHADOW_GOLD_RING_WIDTH :: 1
SHADOW_GOLD_RING_COLOR :: GOLD_DIM
// ============================================================
// SPACING SCALE (8px base grid)
// All spacing values are multiples of 4px, with the main scale
// in multiples of 8px. Names describe the scope of the gap, not
// the raw size — pick by intent, not by pixel count.
// ============================================================
// Badge/tag inner padding, icon-label gap, border offsets, micro nudges.
SPACE_CHIP :: 4
// Inline element gaps, chip/pill padding, icon inset, tight row spacing.
SPACE_ELEMENT :: 8
// Button vertical padding, input inset, list row gap, label-to-field gap.
SPACE_COMPONENT :: 12
// Card inset, input horizontal padding, form field gap, default gap.
SPACE_GROUP :: 16
// Grouped nav items, related form section spacing, compact panel inset.
SPACE_CLUSTER :: 20
// Sidebar / panel inset, modal body padding, drawer inset, section
// subheader gap.
SPACE_PANEL :: 24
// Between distinct content blocks, card grid gutter, toolbar height.
SPACE_BLOCK :: 32
// Major content group spacing, dialog padding, page sub-section gap.
SPACE_CONTENT :: 40
// Page section breaks, feature group dividers, hero subheading gap.
SPACE_SECTION :: 48
// Hero vertical padding, layout area spacing, large feature gaps.
SPACE_REGION :: 64
// Page-scale layout spacing, full-width section vertical rhythm.
SPACE_ZONE :: 80
// Page margins, full-bleed hero top padding, maximum layout gutter.
SPACE_CANVAS :: 96
// ============================================================
// CORNER RADIUS
// Cybersteel does not round its corners like a toy. 04px is the
// preferred range; larger radii exist only for chips/pills.
// ============================================================
RADIUS_NONE :: 0 // sharp corners — preferred default for chrome
RADIUS_SM :: 4 // micro-rounding for inline code, small badges
RADIUS_MD :: 6 // default for cards, buttons, inputs
RADIUS_LG :: 10 // rare — used only for prominent containers
RADIUS_PILL :: 999 // fully-rounded chips, status pills, tags
// ============================================================
// BORDER WIDTH
// 1px solid is the standard. Heavier weights are only used for
// the Art Deco hairline accent on pre/code blocks.
// ============================================================
// Standard border weight everywhere — always crisp, always visible.
BORDER_WIDTH_DEFAULT :: 1
// Accent edge on <pre> blocks (left side, gold) and similar
// emphasized rule treatments.
BORDER_WIDTH_ACCENT :: 2
// ============================================================
// MOTION — TRANSITION DURATIONS
// Fast and purposeful. No bounce, no spring, no elastic. UI
// state changes in well under a quarter-second. Animations
// must explain causality; nothing is decorative.
// ============================================================
// Entering active/pressed state. Snap-down feel — must feel
// instant under the finger.
TRANSITION_PRESS :: 55 * time.Millisecond
// Releasing from a pressed state, and slower hover-out cases.
TRANSITION_UI :: 180 * time.Millisecond
// Hover enter / exit color shift on buttons, cards, links.
TRANSITION_HOVER :: 150 * time.Millisecond
// Overlay / modal / popover fade-in. Slightly longer to
// signal "a layer changed", not "a control changed".
TRANSITION_MODAL :: 200 * time.Millisecond
// Cursor / immediate-feedback transitions (caret moves,
// terminal output ticks).
TRANSITION_CURSOR :: 80 * time.Millisecond
// ============================================================
// MOTION — COMPONENT-LEVEL TIMINGS
// Specific named durations for known interactions. Prefer these
// over picking a raw transition for a given component.
// ============================================================
// Button press fade — primary/secondary/danger/success share this.
BUTTON_PRESS_FADE_DUR :: 55 * time.Millisecond
// Button release / hover-out fade.
BUTTON_RELEASE_FADE_DUR :: 180 * time.Millisecond
// Card hover (border + bg crossfade).
CARD_HOVER_FADE_DUR :: 150 * time.Millisecond
// Card press (border + bg snap to active).
CARD_PRESS_FADE_DUR :: 55 * time.Millisecond
// Modal / overlay enter.
MODAL_ENTER_DUR :: 200 * time.Millisecond
// Modal / overlay exit (mirror of enter for symmetry).
MODAL_EXIT_DUR :: 200 * time.Millisecond
// Link color crossfade on hover.
LINK_HOVER_FADE_DUR :: 180 * time.Millisecond
// Terminal scanline flicker tick — single frame of the loop.
SCANLINE_FLICKER_TICK :: 80 * time.Millisecond
// ============================================================
// TYPOGRAPHY — FONT FAMILY NAMES
// Sans: IBM Plex Sans
// Mono: Lilex — IBM Plex Mono with programming ligatures.
// Drop-in Plex Mono replacement; same skeleton, same
// proportions, plus =>, !=, >=, <=, etc. ligatures.
// Plex Sans covers display, body, and condensed roles by
// default. Lilex is for code, terminal output, data values,
// and full mono-mode surfaces.
// ============================================================
// Plain family names
FONT_FAMILY_SANS :: "IBM Plex Sans"
FONT_FAMILY_MONO :: "Lilex"
// IBM Plex Sans raw font data
SANS_THIN_RAW :: #load("fonts/IBMPlexSans-Thin.ttf") // IBM Plex Sans
SANS_THIN_ITALIC_RAW :: #load("fonts/IBMPlexSans-ThinItalic.ttf") // IBM Plex Sans
SANS_EXTRALIGHT_RAW :: #load("fonts/IBMPlexSans-ExtraLight.ttf") // IBM Plex Sans
SANS_EXTRALIGHT_ITALIC_RAW :: #load("fonts/IBMPlexSans-ExtraLightItalic.ttf") // IBM Plex Sans
SANS_LIGHT_RAW :: #load("fonts/IBMPlexSans-Light.ttf") // IBM Plex Sans
SANS_LIGHT_ITALIC_RAW :: #load("fonts/IBMPlexSans-LightItalic.ttf") // IBM Plex Sans
SANS_REGULAR_RAW :: #load("fonts/IBMPlexSans-Regular.ttf") // IBM Plex Sans
SANS_ITALIC_RAW :: #load("fonts/IBMPlexSans-Italic.ttf") // IBM Plex Sans
SANS_MEDIUM_RAW :: #load("fonts/IBMPlexSans-Medium.ttf") // IBM Plex Sans
SANS_MEDIUM_ITALIC_RAW :: #load("fonts/IBMPlexSans-MediumItalic.ttf") // IBM Plex Sans
SANS_SEMIBOLD_RAW :: #load("fonts/IBMPlexSans-SemiBold.ttf") // IBM Plex Sans
SANS_SEMIBOLD_ITALIC_RAW :: #load("fonts/IBMPlexSans-SemiBoldItalic.ttf") // IBM Plex Sans
SANS_BOLD_RAW :: #load("fonts/IBMPlexSans-Bold.ttf") // IBM Plex Sans
SANS_BOLD_ITALIC_RAW :: #load("fonts/IBMPlexSans-BoldItalic.ttf") // IBM Plex Sans
// Lilex raw font data
MONO_THIN_RAW :: #load("fonts/Lilex-Thin.ttf") // Lilex
MONO_THIN_ITALIC_RAW :: #load("fonts/Lilex-ThinItalic.ttf") // Lilex
MONO_EXTRALIGHT_RAW :: #load("fonts/Lilex-ExtraLight.ttf") // Lilex
MONO_EXTRALIGHT_ITALIC_RAW :: #load("fonts/Lilex-ExtraLightItalic.ttf") // Lilex
MONO_LIGHT_RAW :: #load("fonts/Lilex-Light.ttf") // Lilex
MONO_LIGHT_ITALIC_RAW :: #load("fonts/Lilex-LightItalic.ttf") // Lilex
MONO_REGULAR_RAW :: #load("fonts/Lilex-Regular.ttf") // Lilex
MONO_ITALIC_RAW :: #load("fonts/Lilex-Italic.ttf") // Lilex
MONO_MEDIUM_RAW :: #load("fonts/Lilex-Medium.ttf") // Lilex
MONO_MEDIUM_ITALIC_RAW :: #load("fonts/Lilex-MediumItalic.ttf") // Lilex
MONO_SEMIBOLD_RAW :: #load("fonts/Lilex-SemiBold.ttf") // Lilex
MONO_SEMIBOLD_ITALIC_RAW :: #load("fonts/Lilex-SemiBoldItalic.ttf") // Lilex
MONO_BOLD_RAW :: #load("fonts/Lilex-Bold.ttf") // Lilex
MONO_BOLD_ITALIC_RAW :: #load("fonts/Lilex-BoldItalic.ttf") // Lilex
// ============================================================
// TYPOGRAPHY — TYPE SCALE (1.25 modular ratio, base 16px)
// Minimum body size on web is 14px; print is 12pt.
// ============================================================
TEXT_XS :: 11 // status badges, fine print
TEXT_SM :: 13 // secondary labels, captions
TEXT_BASE :: 15 // default body text
TEXT_MD :: 16 // slightly prominent body
TEXT_LG :: 18 // subheadings, emphasized labels
TEXT_XL :: 22 // H3 level
TEXT_2XL :: 28 // H2 level
TEXT_3XL :: 36 // H1 level
TEXT_4XL :: 48 // display / hero
TEXT_5XL :: 64 // hero display
TEXT_6XL :: 96 // max scale; masthead only
// ============================================================
// TYPOGRAPHY — FONT WEIGHTS
// Constrained to the STATIC weights that BOTH faces actually
// ship from Google Fonts — IBM Plex Sans and Lilex share the
// same seven static instances:
// 100 Thin · 200 ExtraLight · 300 Light · 400 Regular ·
// 500 Medium · 600 SemiBold · 700 Bold
// There is no 800 ExtraBold and no 900 Black for either face.
// Do not request a weight outside this set — Google's API
// will fail or substitute, and the design will drift.
// ============================================================
WEIGHT_THIN :: 100
WEIGHT_EXTRALIGHT :: 200
WEIGHT_LIGHT :: 300
WEIGHT_REGULAR :: 400
WEIGHT_MEDIUM :: 500
WEIGHT_SEMIBOLD :: 600
WEIGHT_BOLD :: 700
// ============================================================
// TYPOGRAPHY — LINE HEIGHTS (unitless multipliers)
// Multiply by font size to derive a leading in pixels.
// ============================================================
LEADING_TIGHT :: 1.15 // display headings
LEADING_SNUG :: 1.30 // subheadings
LEADING_NORMAL :: 1.50 // default body prose
LEADING_LOOSE :: 1.70 // long-form reading, sparse density
LEADING_MONO :: 1.40 // code / terminal output
// ============================================================
// TYPOGRAPHY — LETTER SPACING (in EM units)
// Multiply by the resolved font size to get pixel spacing.
// ============================================================
TRACKING_TIGHT :: -0.02 // large headings, tightened display
TRACKING_NORMAL :: 0.00 // body default
TRACKING_WIDE :: 0.05 // H1/H2 ALL CAPS, button labels
TRACKING_WIDER :: 0.10 // H5 caps, section headers
TRACKING_WIDEST :: 0.20 // .label / .label-mono — ALL CAPS chip text
// ============================================================
// HEADING ROLES — paired size + tracking + casing intent
// Casing is documentation only; these are the numbers a
// renderer actually consumes.
// ============================================================
// H1 — page title, masthead. Title Case, ALL CAPS at display.
H1_SIZE :: TEXT_3XL
H1_WEIGHT :: WEIGHT_BOLD
H1_TRACKING :: TRACKING_WIDE
H1_LEADING :: LEADING_TIGHT
// H2 — major section. ALL CAPS.
H2_SIZE :: TEXT_2XL
H2_WEIGHT :: WEIGHT_BOLD
H2_TRACKING :: TRACKING_WIDE
H2_LEADING :: LEADING_TIGHT
// H3 — subsection. Sentence case, condensed semibold.
H3_SIZE :: TEXT_XL
H3_WEIGHT :: WEIGHT_SEMIBOLD
H3_TRACKING :: TRACKING_NORMAL
H3_LEADING :: LEADING_TIGHT
// H4 — minor subsection.
H4_SIZE :: TEXT_LG
H4_WEIGHT :: WEIGHT_SEMIBOLD
H4_TRACKING :: TRACKING_NORMAL
H4_LEADING :: LEADING_SNUG
// H5 — small caps section header (uses FG_SECONDARY).
H5_SIZE :: TEXT_BASE
H5_WEIGHT :: WEIGHT_SEMIBOLD
H5_TRACKING :: TRACKING_WIDER
H5_LEADING :: LEADING_SNUG
// H6 — mono caps eyebrow / overline (uses FG_CAPTION).
H6_SIZE :: TEXT_SM
H6_WEIGHT :: WEIGHT_REGULAR
H6_TRACKING :: TRACKING_WIDEST
H6_LEADING :: LEADING_SNUG
// ============================================================
// LABEL ROLES — small caps annotation chips
// ============================================================
// .label — sans condensed, ALL CAPS, FG_CAPTION.
LABEL_SIZE :: TEXT_XS
LABEL_WEIGHT :: WEIGHT_SEMIBOLD
LABEL_TRACKING :: TRACKING_WIDEST
// .label-mono — mono ALL CAPS, FG_MUTED.
LABEL_MONO_SIZE :: TEXT_XS
LABEL_MONO_WEIGHT :: WEIGHT_REGULAR
LABEL_MONO_TRACKING :: TRACKING_WIDEST
// ============================================================
// FOCUS RING
// 1px solid gold outline at 2px offset. Crisp, never blurry.
// No glow, no box-shadow halo.
// ============================================================
FOCUS_RING_WIDTH :: 1
FOCUS_RING_OFFSET :: 2
FOCUS_RING_COLOR :: BORDER_FOCUS // GOLD_BRIGHT
// ============================================================
// COMPONENT — BUTTONS
// Cybersteel buttons are uppercase, semibold→bold, with wide
// tracking. Default size is "md"; sm/lg shift padding + size.
// ============================================================
// Default (md) padding: vertical / horizontal
BUTTON_PAD_Y :: 8
BUTTON_PAD_X :: 18
BUTTON_FONT_SIZE :: 12
BUTTON_FONT_WEIGHT :: WEIGHT_BOLD
BUTTON_TRACKING :: 0.07 // EM — ALL CAPS button label
BUTTON_RADIUS :: RADIUS_MD
BUTTON_BORDER :: BORDER_WIDTH_DEFAULT
// Small button
BUTTON_SM_PAD_Y :: 5
BUTTON_SM_PAD_X :: 12
BUTTON_SM_FONT_SIZE :: 10
// Large button
BUTTON_LG_PAD_Y :: 11
BUTTON_LG_PAD_X :: 24
BUTTON_LG_FONT_SIZE :: 14
// Primary — solid gold fill, dark text. Hover brightens, press
// flips to fg-heading (cream) fill.
BUTTON_PRIMARY_BG :: GOLD_DIM
BUTTON_PRIMARY_FG :: BG_SHELL
BUTTON_PRIMARY_BORDER :: GOLD_DIM
BUTTON_PRIMARY_BG_HOVER :: GOLD_BRIGHT
BUTTON_PRIMARY_BORDER_HOVER :: GOLD_BRIGHT
BUTTON_PRIMARY_BG_PRESS :: FG_HEADING
BUTTON_PRIMARY_FG_PRESS :: BG_SHELL
BUTTON_PRIMARY_BORDER_PRESS :: FG_HEADING
// Secondary — transparent bg, structural border, hover gains
// gold tint + gold-dim border, press fills with gold-bright.
BUTTON_SECONDARY_BG :: [4]u8{0, 0, 0, 0} // transparent
BUTTON_SECONDARY_FG :: FG_SECONDARY
BUTTON_SECONDARY_BORDER :: BG_BORDER
BUTTON_SECONDARY_BG_HOVER :: TINT_GOLD_HOVER
BUTTON_SECONDARY_BORDER_HOVER :: GOLD_DIM
BUTTON_SECONDARY_FG_HOVER :: FG_BODY
BUTTON_SECONDARY_BG_PRESS :: GOLD_BRIGHT
BUTTON_SECONDARY_FG_PRESS :: [4]u8{0xff, 0xff, 0xff, 0xff}
BUTTON_SECONDARY_BORDER_PRESS :: GOLD_BRIGHT
// Ghost — fully transparent, no border. Hover lifts to BG_ACTIVE.
BUTTON_GHOST_BG :: [4]u8{0, 0, 0, 0}
BUTTON_GHOST_FG :: FG_CAPTION
BUTTON_GHOST_BORDER :: [4]u8{0, 0, 0, 0}
BUTTON_GHOST_BG_HOVER :: BG_ACTIVE
BUTTON_GHOST_FG_HOVER :: FG_BODY
BUTTON_GHOST_BG_PRESS :: GOLD_DIM
BUTTON_GHOST_FG_PRESS :: [4]u8{0xff, 0xff, 0xff, 0xff}
// Danger — destructive ghost button.
BUTTON_DANGER_BG :: [4]u8{0, 0, 0, 0}
BUTTON_DANGER_FG :: RED_BRIGHT
BUTTON_DANGER_BORDER :: RED_DIM
BUTTON_DANGER_BG_HOVER :: TINT_DANGER_HOVER
BUTTON_DANGER_BORDER_HOVER :: RED_BRIGHT
BUTTON_DANGER_FG_HOVER :: FG_BODY
BUTTON_DANGER_BG_PRESS :: RED_BRIGHT
BUTTON_DANGER_FG_PRESS :: [4]u8{0xff, 0xff, 0xff, 0xff}
BUTTON_DANGER_BORDER_PRESS :: RED_BRIGHT
// Success — confirming ghost button.
BUTTON_SUCCESS_BG :: [4]u8{0, 0, 0, 0}
BUTTON_SUCCESS_FG :: GREEN_BRIGHT
BUTTON_SUCCESS_BORDER :: GREEN_DIM
BUTTON_SUCCESS_BG_HOVER :: TINT_SUCCESS_HOVER
BUTTON_SUCCESS_BORDER_HOVER :: GREEN_BRIGHT
BUTTON_SUCCESS_FG_HOVER :: FG_BODY
BUTTON_SUCCESS_BG_PRESS :: GREEN_BRIGHT
BUTTON_SUCCESS_FG_PRESS :: [4]u8{0xff, 0xff, 0xff, 0xff}
BUTTON_SUCCESS_BORDER_PRESS :: GREEN_BRIGHT
// Disabled — flat low-contrast surface, opacity-dimmed.
BUTTON_DISABLED_BG :: BG_ACTIVE
BUTTON_DISABLED_FG :: FG_MUTED
BUTTON_DISABLED_BORDER :: BG_BORDER
BUTTON_DISABLED_OPACITY :: 0.5
// ============================================================
// COMPONENT — CARDS
// Flat, structural, mechanical. Background sits one step above
// page; border is structural by default and shifts to gold-dim
// on hover/press. Corner radius is the default 6px (RADIUS_MD).
// ============================================================
CARD_BG :: BG_SURFACE
CARD_BORDER :: BG_BORDER
CARD_BORDER_HOVER :: GOLD_DIM
CARD_BG_PRESS :: BG_ACTIVE
CARD_BORDER_PRESS :: GOLD_DIM
CARD_RADIUS :: RADIUS_MD
CARD_BORDER_WIDTH :: BORDER_WIDTH_DEFAULT
CARD_PADDING :: SPACE_GROUP // 16px default inset
// ============================================================
// COMPONENT — INPUTS
// Inputs sit on BG_SURFACE with structural borders. Focus
// promotes the border to gold-bright; the focus ring follows.
// ============================================================
INPUT_BG :: BG_SURFACE
INPUT_FG :: FG_BODY
INPUT_PLACEHOLDER :: FG_CAPTION
INPUT_BORDER :: BG_BORDER
INPUT_BORDER_HOVER :: GOLD_DIM
INPUT_BORDER_FOCUS :: GOLD_BRIGHT
INPUT_BORDER_DANGER :: RED_DIM
INPUT_RADIUS :: RADIUS_MD
INPUT_PAD_Y :: SPACE_COMPONENT // 12
INPUT_PAD_X :: SPACE_GROUP // 16
// ============================================================
// COMPONENT — BADGES / STATUS PILLS
// ============================================================
BADGE_FONT_SIZE :: TEXT_XS
BADGE_WEIGHT :: WEIGHT_SEMIBOLD
BADGE_TRACKING :: TRACKING_WIDEST
BADGE_PAD_Y :: SPACE_CHIP // 4
BADGE_PAD_X :: SPACE_ELEMENT // 8
BADGE_RADIUS :: RADIUS_SM
// ============================================================
// COMPONENT — DECO RULE
// Hairline Art Deco horizontal rule: 1px gold-dim top + 1px
// structural drop, with panel-sized vertical margins.
// ============================================================
DECO_RULE_TOP_WIDTH :: 1
DECO_RULE_TOP_COLOR :: GOLD_DIM
DECO_RULE_DROP_WIDTH :: 1
DECO_RULE_DROP_COLOR :: BG_BORDER
DECO_RULE_MARGIN_Y :: SPACE_PANEL // 24
// ============================================================
// LAYOUT — FIXED CHROME WIDTHS
// Sidebar widths are fixed; content lives in 8 or 12 column
// grids. No responsive collapsing for chrome — Cybersteel UIs
// run on real workstations.
// ============================================================
SIDEBAR_WIDTH_NARROW :: 240
SIDEBAR_WIDTH_WIDE :: 280
GRID_COLUMNS_NARROW :: 8
GRID_COLUMNS_WIDE :: 12
// Toolbar height matches SPACE_BLOCK so vertical rhythm aligns.
TOOLBAR_HEIGHT :: SPACE_BLOCK // 32
// ============================================================
// CODE BLOCKS — <pre>
// Mono, BG_SHELL surface with a 1px structural border and a
// 2px gold-dim accent on the left edge.
// ============================================================
CODE_INLINE_BG :: BG_SURFACE
CODE_INLINE_FG :: GOLD_BRIGHT
CODE_INLINE_BORDER :: BG_BORDER
CODE_INLINE_PAD_Y :: 2
CODE_INLINE_PAD_X :: 6
CODE_INLINE_RADIUS :: RADIUS_SM
PRE_BG :: BG_SHELL
PRE_FG :: FG_BODY
PRE_BORDER :: BG_BORDER
PRE_BORDER_LEFT_COLOR :: GOLD_DIM
PRE_BORDER_LEFT_WIDTH :: BORDER_WIDTH_ACCENT // 2
PRE_PAD_Y :: SPACE_GROUP // 16
PRE_PAD_X :: SPACE_PANEL // 24
// ============================================================
// SCANLINE OVERLAY (opt-in, terminal surfaces only)
// Repeating-stripe pattern at very low opacity. Stripe is 2 logical
// pixels transparent + 2 logical pixels black-at-3% (TINT_SCANLINE).
// ============================================================
SCANLINE_STRIPE_LPX :: 2
SCANLINE_GAP_LPX :: 2
SCANLINE_COLOR :: TINT_SCANLINE
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+475 -733
View File
File diff suppressed because it is too large Load Diff
-191
View File
@@ -1,191 +0,0 @@
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. 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)
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).
// - 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 || 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 {
if qrcode.get_module(qrcode_buf, x, y) {
pixels[row + x] = dark
}
}
}
return draw.Texture_Desc {
width = u32(padded_size),
height = u32(padded_size),
depth_or_layers = 1,
type = .D2,
format = .R8G8B8A8_UNORM,
usage = {.SAMPLER},
mip_levels = 1,
kind = .Static,
},
true
}
// Allocates pixel buffer via temp_allocator, decodes qrcode_buf into it, and
// registers with the GPU. The pixel allocation is freed before return.
//
// Returns ok=false when:
// - qrcode_buf is invalid (qrcode.get_size returns 0).
// - temp_allocator fails to allocate the pixel buffer.
// - GPU texture registration fails.
@(require_results)
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, 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, quiet_zone) or_return
return draw.register_texture(desc, pixels)
}
// Encodes text as a QR Code and registers the result as an RGBA texture.
//
// Returns ok=false when:
// - temp_allocator fails to allocate.
// - The text cannot fit in any version within [min_version, max_version] at the given ECL.
// - GPU texture registration fails.
@(require_results)
register_texture_from_text :: proc(
text: string,
ecl: qrcode.Ecc = .Low,
min_version: int = qrcode.VERSION_MIN,
max_version: int = qrcode.VERSION_MAX,
mask: Maybe(qrcode.Mask) = nil,
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,
ok: bool,
) {
qrcode_buf, alloc_err := make([]u8, qrcode.buffer_len_for_version(max_version), temp_allocator)
if alloc_err != nil do return draw.INVALID_TEXTURE, false
defer delete(qrcode_buf, temp_allocator)
qrcode.encode_auto(
text,
qrcode_buf,
ecl,
min_version,
max_version,
mask,
boost_ecl,
temp_allocator,
) or_return
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.
//
// Returns ok=false when:
// - temp_allocator fails to allocate.
// - The payload cannot fit in any version within [min_version, max_version] at the given ECL.
// - GPU texture registration fails.
@(require_results)
register_texture_from_binary :: proc(
bin_data: []u8,
ecl: qrcode.Ecc = .Low,
min_version: int = qrcode.VERSION_MIN,
max_version: int = qrcode.VERSION_MAX,
mask: Maybe(qrcode.Mask) = nil,
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,
ok: bool,
) {
qrcode_buf, alloc_err := make([]u8, qrcode.buffer_len_for_version(max_version), temp_allocator)
if alloc_err != nil do return draw.INVALID_TEXTURE, false
defer delete(qrcode_buf, temp_allocator)
qrcode.encode_auto(
bin_data,
qrcode_buf,
ecl,
min_version,
max_version,
mask,
boost_ecl,
temp_allocator,
) or_return
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,
}
-409
View File
@@ -1,409 +0,0 @@
package examples
import "core:fmt"
import "core:math"
import "core:os"
import sdl "vendor:sdl3"
import "../../draw"
import cyber "../cybersteel"
// Backdrop example.
//
// Exercises the bracket scheduler end-to-end. The demo is structured as three zones in one
// window so we can stress-test the cases that matter:
//
// Zone 1 (top, base layer): animated colorful background + two side-by-side frosted panels
// with DIFFERENT sigmas and DIFFERENT tints. Tests sigma grouping
// and per-primitive tint.
//
// Zone 2 (bottom-left, second layer): a small frosted panel in a NEW layer; its bracket sees
// Zone 1's full content (base layer's bracket output is
// carried forward via source_texture). Tests multi-layer
// backdrop sampling.
//
// Zone 3 (bottom-right, base layer): edge cases. A sigma=0 "mirror" panel (no blur), two
// same-sigma panels stacked (tests sub-batch coalescing
// via append_or_extend_sub_batch), and text drawn ON TOP
// of a backdrop (tests Pass B post-bracket rendering).
//
// Animation: an orbiting gradient stripe plus a few orbiting circles in Zone 1. Motion is the
// only way to visually confirm the blur is Gaussian; a static panel can't tell you whether the
// kernel coefficients are right.
gaussian_blur :: proc() {
if !sdl.Init({.VIDEO}) do os.exit(1)
window := sdl.CreateWindow("Backdrop blur", 800, 600, {.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)
WINDOW_W :: f32(800)
WINDOW_H :: f32(600)
FONT_SIZE :: u16(14)
t: f32 = 0
for {
defer free_all(context.temp_allocator)
ev: sdl.Event
for sdl.PollEvent(&ev) {
if ev.type == .QUIT do return
}
t += 1
base_layer := draw.begin({width = WINDOW_W, height = WINDOW_H})
//----- Background fill ----------------------------------
draw.rectangle(base_layer, {0, 0, WINDOW_W, WINDOW_H}, draw.Color{20, 20, 28, 255})
//----- Zone 1: animated background for the top frosted panels ----------------------------------
// A wide rotating gradient stripe sweeps left-to-right across Zone 1. The angle changes
// over time so the gradient itself shifts visibly.
stripe_angle := t * 0.4
draw.rectangle(
base_layer,
{20, 20, WINDOW_W - 40, 240},
draw.Linear_Gradient {
start_color = {255, 80, 60, 255},
end_color = {60, 120, 255, 255},
angle = stripe_angle,
},
)
// Five orbiting circles inside Zone 1's strip. The blur should smooth their hard edges
// and the gradient behind them into a continuous wash.
for i in 0 ..< 5 {
phase := f32(i) * 1.2 + t * 0.04
cx := 100 + f32(i) * 140 + math.cos(phase) * 30
cy := 140 + math.sin(phase) * 50
circle_color := draw.Color {
u8(clamp(120 + math.cos(phase) * 100, 0, 255)),
u8(clamp(180 + math.sin(phase * 1.3) * 60, 0, 255)),
u8(clamp(220 - math.sin(phase) * 80, 0, 255)),
255,
}
draw.circle(base_layer, {cx, cy}, 22, circle_color)
}
// Bright accent rectangles to give the blur some sharp edges to munch on.
draw.rectangle(base_layer, {200, 60, 60, 12}, draw.Color{255, 255, 200, 255})
draw.rectangle(base_layer, {500, 200, 80, 16}, draw.Color{200, 255, 200, 255})
//----- Zone 1 frosted panels: different sigmas, different tints --------------------------------
// Panel A: heavy blur, cool blue-grey tint. sigma=14 in logical px.
// Both panels share rounded corners.
panel_radii := draw.Rectangle_Radii{16, 16, 16, 16}
// Both zone1 panels share one scope. Different sigmas still trigger separate blur
// passes (cost scales with unique sigmas, not with backdrop count); the scope just
// declares "these draws form one bracket." `backdrop_scope` is the RAII-style API:
// `end_backdrop` fires automatically when the block exits.
{
draw.backdrop_scope(base_layer)
draw.backdrop_blur(
base_layer,
{60, 80, 320, 140},
gaussian_sigma = 30,
tint = draw.Color{170, 200, 240, 200}, // cool blue, strong mix
radii = panel_radii,
)
// Panel B: lighter blur, warm amber tint. sigma=6.
draw.backdrop_blur(
base_layer,
{420, 80, 320, 140},
gaussian_sigma = 6,
tint = draw.Color{255, 220, 160, 200}, // warm amber, strong mix
radii = panel_radii,
)
}
// Text labels for the two panels. Drawn AFTER `end_backdrop` (which fires at the
// scope-block exit above), so they composite on top of both panels.
draw.text(
base_layer,
"sigma = 20, cool tint",
{72, 90},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.Color{30, 35, 50, 255},
)
draw.text(
base_layer,
"sigma = 6, warm tint",
{432, 90},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.Color{60, 40, 20, 255},
)
// Post-bracket verification: a white stripe drawn AFTER `end_backdrop` in the same
// layer. Should render ON TOP of both panels because the backdrop scope (and its
// composite output) is now closed; any non-backdrop draw on this layer composites
// with LOAD on top of whatever the bracket left in source_texture.
draw.rectangle(base_layer, {WINDOW_W * 0.5 - 4, 70, 8, 160}, draw.Color{255, 255, 255, 230})
//----- Zone 2: second layer with its own backdrop --------------------------------
// Zone 2's panel is in a NEW layer. Its bracket samples source_texture as it stands
// after the base layer fully finished (including the base layer's bracket V-composite
// output). So this panel sees Zone 1's frosted panels through its own blur.
zone2 := draw.new_layer(base_layer, {0, 280, WINDOW_W * 0.55, WINDOW_H - 280})
// Pass A content for zone2: a translucent darker overlay to make the panel pop.
draw.rectangle(zone2, {20, 300, WINDOW_W * 0.55 - 40, WINDOW_H - 320}, draw.Color{0, 0, 0, 80})
// Animated diagonal stripe in Zone 2 so the blur in this layer's panel has motion to
// smooth, not just the static base-layer content.
stripe_y := 320 + (math.sin(t * 0.05) * 0.5 + 0.5) * 200
draw.rectangle(zone2, {30, stripe_y, WINDOW_W * 0.55 - 60, 18}, draw.Color{255, 100, 200, 200})
// Zone 2's frosted panel. Single-panel scope; `backdrop_scope` keeps the begin/end
// pair tied to the block.
{
draw.backdrop_scope(zone2)
draw.backdrop_blur(
zone2,
{60, 360, WINDOW_W * 0.55 - 120, 160},
gaussian_sigma = 10,
tint = draw.WHITE, // pure blur (white tint with any alpha is a no-op)
radii = draw.Rectangle_Radii{24, 24, 24, 24},
)
}
draw.text(
zone2,
"Layer 2 backdrop",
{72, 372},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.Color{30, 30, 30, 255},
)
draw.text(
zone2,
"sigma = 10",
{72, 392},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.Color{60, 60, 60, 255},
)
//----- Zone 3: edge cases (back in base layer would also work, but we use zone2 to keep --------
// the demo's two-layer structure simple). Zone 3 lives in a third layer so it gets
// a fresh source snapshot too.
zone3 := draw.new_layer(zone2, {WINDOW_W * 0.55, 280, WINDOW_W * 0.45, WINDOW_H - 280})
// Animated background patch for Zone 3 so its mirror panel has something to reflect.
for i in 0 ..< 4 {
phase := f32(i) * 1.5 + t * 0.06
y := 310 + f32(i) * 60 + math.sin(phase) * 8
draw.rectangle(
zone3,
{WINDOW_W * 0.55 + 20, y, WINDOW_W * 0.45 - 40, 14},
draw.Color {
u8(clamp(200 + math.cos(phase) * 50, 0, 255)),
u8(clamp(150 + math.sin(phase) * 80, 0, 255)),
u8(clamp(220 - math.cos(phase * 1.7) * 60, 0, 255)),
255,
},
)
}
// All three Zone 3 backdrops share one scope. The sigma=0 mirror, then the two
// contiguous sigma=8 panels. The sigma=8 pair stays contiguous in the sub-batch list,
// so `append_or_extend_sub_batch` still coalesces them into a single instanced
// composite draw — scope boundaries don't affect coalescing, only kind/sigma identity.
{
draw.backdrop_scope(zone3)
// Edge case 1: sigma = 0 "mirror" — sharp framebuffer sample, no blur. Should reproduce
// the underlying pixels exactly through the SDF mask. Tinted slightly so it's visible.
draw.backdrop_blur(
zone3,
{WINDOW_W * 0.55 + 30, 310, 150, 70},
gaussian_sigma = 0,
tint = draw.WHITE, // pure mirror (no blur, no tint)
radii = draw.Rectangle_Radii{12, 12, 12, 12},
)
// Edge case 2: two same-sigma panels submitted contiguously. The sub-batch coalescer
// should merge these into a single instanced V-composite draw. Visually, both should
// look identical (modulo position) — same blur radius, same tint.
draw.backdrop_blur(
zone3,
{WINDOW_W * 0.55 + 30, 400, 150, 70},
gaussian_sigma = 8,
tint = draw.Color{160, 255, 160, 200}, // green tint, strong mix
radii = draw.Rectangle_Radii{12, 12, 12, 12},
)
draw.backdrop_blur(
zone3,
{WINDOW_W * 0.55 + 200, 400, 150, 70},
gaussian_sigma = 8,
tint = draw.Color{160, 255, 160, 200}, // identical: tests sub-batch coalescing
radii = draw.Rectangle_Radii{12, 12, 12, 12},
)
}
// Edge case 3: text drawn AFTER `end_backdrop` in the same layer. Composites on top of
// the bracket's V-composite output and should appear sharply over the green panels.
draw.text(
zone3,
"sigma=0 (mirror)",
{WINDOW_W * 0.55 + 38, 318},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.Color{20, 20, 20, 255},
)
draw.text(
zone3,
"sigma=8 (coalesced pair)",
{WINDOW_W * 0.55 + 38, 408},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.Color{20, 40, 20, 255},
)
draw.text(
zone3,
"Post-scope text overlay",
{WINDOW_W * 0.55 + 38, 480},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.WHITE,
)
draw.end(gpu, window, draw.Color{15, 15, 22, 255})
}
}
// Backdrop diagnostic example.
//
// Minimal isolation harness for debugging the blur. ONE panel, ONE sigma, NO animation. The
// fixed background gives the eye a stable reference: the blur should smooth a *known* set of
// hard edges, and any artifacts (crisp circles, ghost mirrors, no apparent change with sigma)
// stand out clearly.
//
// Controls:
// UP / DOWN arrow : adjust sigma by ±1
// LEFT / RIGHT arrow : adjust sigma by ±5
// SPACE : reset to sigma=10
// T : toggle the test rectangle on top of the panel
//
// Sigma is printed to the title bar so you can correlate visual behavior with the numeric
// value as you adjust it.
gaussian_blur_debug :: proc() {
if !sdl.Init({.VIDEO}) do os.exit(1)
window := sdl.CreateWindow("Backdrop debug", 800, 600, {.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)
defer draw.destroy(gpu)
PLEX_SANS_REGULAR = draw.register_font(cyber.SANS_REGULAR_RAW)
WINDOW_W :: f32(800)
WINDOW_H :: f32(600)
FONT_SIZE :: u16(14)
sigma: f32 = 10
show_test_rect := true
for {
defer free_all(context.temp_allocator)
ev: sdl.Event
for sdl.PollEvent(&ev) {
if ev.type == .QUIT do return
if ev.type == .KEY_DOWN {
#partial switch ev.key.scancode {
case .UP: sigma += 1
case .DOWN: sigma = max(sigma - 1, 0)
case .RIGHT: sigma += 5
case .LEFT: sigma = max(sigma - 5, 0)
case .SPACE: sigma = 10
case .T: show_test_rect = !show_test_rect
}
}
}
// Update title with current sigma so we can correlate visuals to numbers.
title := fmt.ctprintf("Backdrop debug | sigma = %.1f", sigma)
sdl.SetWindowTitle(window, title)
base_layer := draw.begin({width = WINDOW_W, height = WINDOW_H})
// Background: deliberately high-contrast static content. The eye can verify whether
// hard edges (the black grid lines, the crisp circles, the fine vertical bars) get
// smoothed by the panel. NOTHING animates here — every difference between frames is
// caused by user input (sigma change), not by the demo itself.
draw.rectangle(base_layer, {0, 0, WINDOW_W, WINDOW_H}, draw.Color{255, 255, 255, 255})
// Black grid: 8x6 cells with thin lines. Each grid cell is 100x100 logical px.
for x: f32 = 0; x <= WINDOW_W; x += 100 {
draw.rectangle(base_layer, {x - 1, 0, 2, WINDOW_H}, draw.BLACK)
}
for y: f32 = 0; y <= WINDOW_H; y += 100 {
draw.rectangle(base_layer, {0, y - 1, WINDOW_W, 2}, draw.BLACK)
}
// A row of small bright circles across the middle. Their crisp edges are the most
// sensitive blur indicator.
for i in 0 ..< 8 {
cx := f32(i) * 100 + 50
color := draw.Color{u8((i * 32) & 0xff), u8((i * 64) & 0xff), u8(255 - (i * 32) & 0xff), 255}
draw.circle(base_layer, {cx, 350}, 25, color)
}
// Vertical fine-detail stripes on the left edge. At any meaningful sigma these should
// merge into a flat color through the panel.
for i in 0 ..< 20 {
x := 30 + f32(i) * 6
color := draw.RED if i % 2 == 0 else draw.BLUE
draw.rectangle(base_layer, {x, 200, 4, 200}, color)
}
// THE PANEL UNDER TEST. Square, centered, large enough to cover multiple grid cells and
// the circle row. Square shape makes any horizontal-vs-vertical asymmetry purely
// renderer-driven (geometry can't introduce it).
//
// Uses the explicit begin/end form (instead of `backdrop_scope`) to exercise the
// alternative API surface in the diagnostic harness.
panel := draw.Rectangle{250, 150, 300, 300}
draw.begin_backdrop(base_layer)
draw.backdrop_blur(
base_layer,
panel,
gaussian_sigma = sigma,
tint = draw.WHITE,
radii = draw.Rectangle_Radii{20, 20, 20, 20},
)
draw.end_backdrop(base_layer)
// Post-scope test: a bright rectangle drawn AFTER `end_backdrop` in the same layer.
// Should always render on top of the panel. If the panel ever shows a "ghost" of this
// rect inside its blur, the V-composite is sampling the wrong texture state.
if show_test_rect {
draw.rectangle(base_layer, {380, 280, 40, 40}, draw.Color{0, 200, 0, 255})
}
// Sigma label at the bottom in giant text so you can read it from across the room.
draw.text(
base_layer,
fmt.tprintf("sigma = %.1f", sigma),
{20, WINDOW_H - 40},
PLEX_SANS_REGULAR,
28,
color = draw.BLACK,
)
draw.text(
base_layer,
"UP/DOWN ±1 LEFT/RIGHT ±5 SPACE reset T toggle test rect",
{20, WINDOW_H - 70},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.Color{60, 60, 60, 255},
)
draw.end(gpu, window, draw.Color{255, 255, 255, 255})
}
}
-363
View File
@@ -1,363 +0,0 @@
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,
},
) {}
}
}
-96
View File
@@ -1,96 +0,0 @@
package examples
import "core:fmt"
import "core:log"
import "core:mem"
import "core:os"
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"
AVAILABLE_EXAMPLES_MSG ::
"Available examples: " +
EX_HELLOPE_SHAPES +
", " +
EX_HELLOPE_TEXT +
", " +
EX_HELLOPE_CLAY +
", " +
EX_HELLOPE_CUSTOM +
", " +
EX_CLAY_BORDERS +
", " +
EX_TEXTURES +
", " +
EX_GAUSSIAN_BLUR +
", " +
EX_GAUSSIAN_BLUR_DEBUG
main :: proc() {
//----- General setup ----------------------------------
// Temp
track_temp: mem.Tracking_Allocator
mem.tracking_allocator_init(&track_temp, context.temp_allocator)
context.temp_allocator = mem.tracking_allocator(&track_temp)
// Default
track: mem.Tracking_Allocator
mem.tracking_allocator_init(&track, context.allocator)
context.allocator = mem.tracking_allocator(&track)
// Log a warning about any memory that was not freed by the end of the program.
// This could be fine for some global state or it could be a memory leak.
defer {
// Temp allocator
if len(track_temp.bad_free_array) > 0 {
fmt.eprintf("=== %v incorrect frees - temp allocator: ===\n", len(track_temp.bad_free_array))
for entry in track_temp.bad_free_array {
fmt.eprintf("- %p @ %v\n", entry.memory, entry.location)
}
mem.tracking_allocator_destroy(&track_temp)
}
// Default allocator
if len(track.allocation_map) > 0 {
fmt.eprintf("=== %v allocations not freed - main allocator: ===\n", len(track.allocation_map))
for _, entry in track.allocation_map {
fmt.eprintf("- %v bytes @ %v\n", entry.size, entry.location)
}
}
if len(track.bad_free_array) > 0 {
fmt.eprintf("=== %v incorrect frees - main allocator: ===\n", len(track.bad_free_array))
for entry in track.bad_free_array {
fmt.eprintf("- %p @ %v\n", entry.memory, entry.location)
}
}
mem.tracking_allocator_destroy(&track)
}
context.logger = log.create_console_logger()
defer log.destroy_console_logger(context.logger)
args := os.args
if len(args) < 2 {
fmt.eprintln("Usage: examples <example_name>")
fmt.eprintln(AVAILABLE_EXAMPLES_MSG)
os.exit(1)
}
switch args[1] {
case EX_HELLOPE_CLAY: hellope_clay()
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()
case:
fmt.eprintf("Unknown example: %v\n", args[1])
fmt.eprintln(AVAILABLE_EXAMPLES_MSG)
os.exit(1)
}
}
Binary file not shown.
Binary file not shown.
+62 -321
View File
@@ -1,156 +1,73 @@
package examples
import "core:math"
import "../../draw"
import "../../vendor/clay"
import "core:c"
import "core:os"
import sdl "vendor:sdl3"
import sdl_ttf "vendor:sdl3/ttf"
import "../../draw"
import "../../draw/tess"
import "../../vendor/clay"
import cyber "../cybersteel"
PLEX_SANS_REGULAR: draw.Font_Id = max(draw.Font_Id) // Max so we crash if registration is forgotten
JETBRAINS_MONO_REGULAR_RAW :: #load("fonts/JetBrainsMono-Regular.ttf")
JETBRAINS_MONO_REGULAR: draw.Font_Id = max(draw.Font_Id) // Max so we crash if registration is forgotten
hellope_shapes :: proc() {
if !sdl.Init({.VIDEO}) do os.exit(1)
window := sdl.CreateWindow("Hellope!", 500, 500, {.HIGH_PIXEL_DENSITY})
gpu := sdl.CreateGPUDevice(draw.PLATFORM_SHADER_FORMAT, true, nil)
gpu := sdl.CreateGPUDevice({.MSL}, true, nil)
if !sdl.ClaimWindowForGPUDevice(gpu, window) do os.exit(1)
if !draw.init(gpu, window) do os.exit(1)
spin_angle: f32 = 0
for {
defer free_all(context.temp_allocator)
ev: sdl.Event
for sdl.PollEvent(&ev) {
if ev.type == .QUIT do return
}
spin_angle += 1
base_layer := draw.begin({width = 500, height = 500})
base_layer := draw.begin({w = 500, h = 500})
// Background
draw.rectangle(base_layer, {0, 0, 500, 500}, draw.Color{40, 40, 40, 255})
draw.rectangle(base_layer, {0, 0, 500, 500}, {40, 40, 40, 255})
// ----- Shapes without rotation (existing demo) -----
draw.rectangle(
base_layer,
{20, 20, 200, 120},
draw.Color{80, 120, 200, 255},
outline_color = draw.WHITE,
outline_width = 2,
radii = {top_right = 15, top_left = 5},
)
red_rect_raddi := draw.uniform_radii({240, 20, 240, 120}, 0.3)
red_rect_raddi.bottom_left = 0
draw.rectangle(base_layer, {240, 20, 240, 120}, draw.Color{200, 80, 80, 255}, radii = red_rect_raddi)
draw.rectangle(
// Shapes demo
draw.rectangle(base_layer, {20, 20, 200, 120}, {80, 120, 200, 255})
draw.rectangle_lines(base_layer, {20, 20, 200, 120}, draw.WHITE, thick = 2)
draw.rectangle_rounded(base_layer, {240, 20, 240, 120}, 0.3, {200, 80, 80, 255})
draw.rectangle_gradient(
base_layer,
{20, 160, 460, 60},
draw.Linear_Gradient{start_color = {255, 0, 0, 255}, end_color = {0, 0, 255, 255}, angle = 0},
{255, 0, 0, 255},
{0, 255, 0, 255},
{0, 0, 255, 255},
{255, 255, 0, 255},
)
// ----- Rotation demos -----
draw.circle(base_layer, {120, 320}, 60, {100, 200, 100, 255})
draw.circle_lines(base_layer, {120, 320}, 60, draw.WHITE, thick = 2)
draw.circle_gradient(base_layer, {300, 320}, 60, {255, 200, 50, 255}, {200, 50, 50, 255})
draw.ring(base_layer, {430, 320}, 30, 55, 0, 270, {100, 100, 220, 255})
// Rectangle rotating around its center
rect := draw.Rectangle{100, 320, 80, 50}
draw.rectangle(
base_layer,
rect,
draw.Color{100, 200, 100, 255},
outline_color = draw.WHITE,
outline_width = 2,
origin = draw.center_of(rect),
rotation = spin_angle,
feather_ppx = 1,
)
// Rounded rectangle rotating around its center
rrect := draw.Rectangle{230, 300, 100, 80}
draw.rectangle(
base_layer,
rrect,
draw.Color{200, 100, 200, 255},
radii = draw.uniform_radii(rrect, 0.4),
origin = draw.center_of(rrect),
rotation = spin_angle,
)
// Ellipse rotating around its center (tilted ellipse)
draw.ellipse(base_layer, {410, 340}, 50, 30, draw.Color{255, 200, 50, 255}, rotation = spin_angle)
// Circle orbiting a point (moon orbiting planet)
// Convention B: center = pivot point (planet), origin = offset from moon center to pivot.
// Moon's visual center at rotation=0: planet_pos - origin = (100, 450) - (0, 40) = (100, 410).
planet_pos := draw.Vec2{100, 450}
draw.circle(base_layer, planet_pos, 8, draw.Color{200, 200, 200, 255}) // planet (stationary)
draw.circle(
base_layer,
planet_pos,
5,
draw.Color{100, 150, 255, 255},
origin = draw.Vec2{0, 40},
rotation = spin_angle,
) // moon orbiting
// Sector (pie slice) rotating in place
draw.ring(
base_layer,
draw.Vec2{250, 450},
0,
30,
draw.Color{100, 100, 220, 255},
start_angle = 0,
end_angle = 270,
rotation = spin_angle,
)
// Triangle rotating around its center
tv1 := draw.Vec2{350, 420}
tv2 := draw.Vec2{420, 480}
tv3 := draw.Vec2{340, 480}
tess.triangle_aa(
base_layer,
tv1,
tv2,
tv3,
{220, 180, 60, 255},
origin = draw.center_of(tv1, tv2, tv3),
rotation = spin_angle,
)
// Polygon rotating around its center (already had rotation; now with origin for orbit)
draw.polygon(
base_layer,
{460, 450},
6,
30,
draw.Color{180, 100, 220, 255},
outline_color = draw.WHITE,
outline_width = 2,
rotation = spin_angle,
)
draw.triangle(base_layer, {60, 420}, {180, 480}, {20, 480}, {220, 180, 60, 255})
draw.line(base_layer, {220, 420}, {460, 480}, {255, 255, 100, 255}, thick = 3)
draw.poly(base_layer, {350, 450}, 6, 40, {180, 100, 220, 255}, rotation = 30)
draw.poly_lines(base_layer, {350, 450}, 6, 40, draw.WHITE, rotation = 30, thick = 2)
draw.end(gpu, window)
}
}
hellope_text :: proc() {
HELLOPE_ID :: 1
ROTATING_SENTENCE_ID :: 2
MEASURED_ID :: 3
CORNER_SPIN_ID :: 4
if !sdl.Init({.VIDEO}) do os.exit(1)
window := sdl.CreateWindow("Hellope!", 600, 600, {.HIGH_PIXEL_DENSITY})
gpu := sdl.CreateGPUDevice(draw.PLATFORM_SHADER_FORMAT, true, nil)
window := sdl.CreateWindow("Hellope!", 500, 500, {.HIGH_PIXEL_DENSITY})
gpu := sdl.CreateGPUDevice({.MSL}, 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)
JETBRAINS_MONO_REGULAR = draw.register_font(JETBRAINS_MONO_REGULAR_RAW)
FONT_SIZE :: u16(24)
spin_angle: f32 = 0
TEXT_ID :: u32(1)
font := draw.get_font(JETBRAINS_MONO_REGULAR, FONT_SIZE)
dpi := sdl.GetWindowDisplayScale(window)
for {
defer free_all(context.temp_allocator)
@@ -158,80 +75,44 @@ hellope_text :: proc() {
for sdl.PollEvent(&ev) {
if ev.type == .QUIT do return
}
spin_angle += 0.5
base_layer := draw.begin({width = 600, height = 600})
base_layer := draw.begin({w = 500, h = 500})
// ----- Text API demos -----
// Grey background
draw.rectangle(base_layer, {0, 0, 500, 500}, {127, 127, 127, 255})
// Cached text with id — TTF_Text reused across frames (good for text-heavy apps)
draw.text(
base_layer,
// Measure and center text
tw, th: c.int
sdl_ttf.GetStringSize(font, "Hellope!", 0, &tw, &th)
text_w := f32(tw) / dpi
text_h := f32(th) / dpi
pos_x := (500.0 - text_w) / 2.0
pos_y := (500.0 - text_h) / 2.0
txt := draw.text(
TEXT_ID,
"Hellope!",
{300, 80},
PLEX_SANS_REGULAR,
FONT_SIZE,
{pos_x, pos_y},
color = draw.WHITE,
origin = draw.center_of("Hellope!", PLEX_SANS_REGULAR, FONT_SIZE),
id = HELLOPE_ID,
font_id = JETBRAINS_MONO_REGULAR,
font_size = FONT_SIZE,
)
draw.prepare_text(base_layer, txt)
// Rotating sentence — verifies multi-word text rotation around center
draw.text(
base_layer,
"Hellope World!",
{300, 250},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = {255, 200, 50, 255},
origin = draw.center_of("Hellope World!", PLEX_SANS_REGULAR, FONT_SIZE),
rotation = spin_angle,
id = ROTATING_SENTENCE_ID,
)
// Uncached text (no id) — created and destroyed each frame, simplest usage
draw.text(base_layer, "Top-left anchored", {20, 450}, PLEX_SANS_REGULAR, FONT_SIZE, color = draw.WHITE)
// Measure text for manual layout
size := draw.measure_text("Measured!", PLEX_SANS_REGULAR, FONT_SIZE)
draw.rectangle(base_layer, {300 - size.x / 2, 380, size.x, size.y}, draw.Color{60, 60, 60, 200})
draw.text(
base_layer,
"Measured!",
{300, 380},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.WHITE,
origin = draw.top_of("Measured!", PLEX_SANS_REGULAR, FONT_SIZE),
id = MEASURED_ID,
)
// Rotating text anchored at top-left (no origin offset) — spins around top-left corner
draw.text(
base_layer,
"Corner spin",
{150, 530},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = {100, 200, 255, 255},
rotation = spin_angle,
id = CORNER_SPIN_ID,
)
draw.end(gpu, window, draw.Color{127, 127, 127, 255})
draw.end(gpu, window)
}
}
hellope_clay :: proc() {
if !sdl.Init({.VIDEO}) do os.exit(1)
window := sdl.CreateWindow("Hellope!", 500, 500, {.HIGH_PIXEL_DENSITY})
gpu := sdl.CreateGPUDevice(draw.PLATFORM_SHADER_FORMAT, true, nil)
gpu := sdl.CreateGPUDevice({.MSL}, 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)
JETBRAINS_MONO_REGULAR = draw.register_font(JETBRAINS_MONO_REGULAR_RAW)
text_config := clay.TextElementConfig {
fontId = PLEX_SANS_REGULAR,
fontSize = 36,
fontId = JETBRAINS_MONO_REGULAR,
fontSize = 24,
textColor = {255, 255, 255, 255},
}
@@ -241,11 +122,12 @@ hellope_clay :: proc() {
for sdl.PollEvent(&ev) {
if ev.type == .QUIT do return
}
base_layer := draw.begin({width = 500, height = 500})
clay.SetLayoutDimensions({width = base_layer.bounds.width, height = base_layer.bounds.height})
base_layer := draw.begin({w = 500, h = 500})
clay.SetLayoutDimensions({width = base_layer.bounds.w, height = base_layer.bounds.h})
clay.BeginLayout()
if clay.UI(clay.ID("outer"))(
if clay.UI()(
{
id = clay.ID("outer"),
layout = {
sizing = {clay.SizingGrow({}), clay.SizingGrow({})},
childAlignment = {x = .Center, y = .Center},
@@ -253,154 +135,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(0),
cmds = clay.EndLayout(),
}
draw.prepare_clay_batch(base_layer, &clay_batch)
draw.prepare_clay_batch(base_layer, &clay_batch, {0, 0})
draw.end(gpu, window)
}
}
hellope_custom :: proc() {
if !sdl.Init({.VIDEO}) do os.exit(1)
window := sdl.CreateWindow("Hellope Custom!", 600, 400, {.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)
text_config := clay.TextElementConfig {
fontId = PLEX_SANS_REGULAR,
fontSize = 24,
textColor = {255, 255, 255, 255},
}
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 {
defer free_all(context.temp_allocator)
ev: sdl.Event
for sdl.PollEvent(&ev) {
if ev.type == .QUIT do return
}
spin_angle += 1
gauge.value = (math.sin(spin_angle * 0.02) + 1) * 0.5
gauge2.value = (math.cos(spin_angle * 0.03) + 1) * 0.5
base_layer := draw.begin({width = 600, height = 400})
clay.SetLayoutDimensions({width = base_layer.bounds.width, height = base_layer.bounds.height})
clay.BeginLayout()
if clay.UI(clay.ID("outer"))(
{
layout = {
sizing = {clay.SizingGrow({}), clay.SizingGrow({})},
childAlignment = {x = .Center, y = .Center},
layoutDirection = .TopToBottom,
childGap = 20,
},
backgroundColor = {50, 50, 50, 255},
},
) {
if clay.UI(clay.ID("title"))({layout = {sizing = {clay.SizingFit({}), clay.SizingFit({})}}}) {
clay.Text("Custom Draw Demo", text_config)
}
// 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"))(
{
layout = {sizing = {clay.SizingFixed(300), clay.SizingFixed(30)}},
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(clay.ID("gauge2"))(
{
layout = {sizing = {clay.SizingFixed(300), clay.SizingFixed(30)}},
custom = {customData = &gauge2_custom},
},
) {}
}
clay_batch := draw.ClayBatch {
bounds = base_layer.bounds,
cmds = clay.EndLayout(0),
}
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, gauge.bg_color, outline_color = draw.WHITE, outline_width = border_width)
fill := draw.Rectangle {
x = bounds.x,
y = bounds.y,
width = bounds.width * gauge.value,
height = bounds.height,
}
draw.rectangle(layer, fill, gauge.color)
}
}
+73
View File
@@ -0,0 +1,73 @@
package examples
import "core:fmt"
import "core:mem"
import "core:os"
main :: proc() {
//----- Tracking allocator ----------------------------------
{
tracking_temp_allocator := false
// Temp
track_temp: mem.Tracking_Allocator
if tracking_temp_allocator {
mem.tracking_allocator_init(&track_temp, context.temp_allocator)
context.temp_allocator = mem.tracking_allocator(&track_temp)
}
// Default
track: mem.Tracking_Allocator
mem.tracking_allocator_init(&track, context.allocator)
context.allocator = mem.tracking_allocator(&track)
// Log a warning about any memory that was not freed by the end of the program.
// This could be fine for some global state or it could be a memory leak.
defer {
// Temp allocator
if tracking_temp_allocator {
if len(track_temp.allocation_map) > 0 {
fmt.eprintf("=== %v allocations not freed - temp allocator: ===\n", len(track_temp.allocation_map))
for _, entry in track_temp.allocation_map {
fmt.eprintf("- %v bytes @ %v\n", entry.size, entry.location)
}
}
if len(track_temp.bad_free_array) > 0 {
fmt.eprintf("=== %v incorrect frees - temp allocator: ===\n", len(track_temp.bad_free_array))
for entry in track_temp.bad_free_array {
fmt.eprintf("- %p @ %v\n", entry.memory, entry.location)
}
}
mem.tracking_allocator_destroy(&track_temp)
}
// Default allocator
if len(track.allocation_map) > 0 {
fmt.eprintf("=== %v allocations not freed - main allocator: ===\n", len(track.allocation_map))
for _, entry in track.allocation_map {
fmt.eprintf("- %v bytes @ %v\n", entry.size, entry.location)
}
}
if len(track.bad_free_array) > 0 {
fmt.eprintf("=== %v incorrect frees - main allocator: ===\n", len(track.bad_free_array))
for entry in track.bad_free_array {
fmt.eprintf("- %p @ %v\n", entry.memory, entry.location)
}
}
mem.tracking_allocator_destroy(&track)
}
}
args := os.args
if len(args) < 2 {
fmt.eprintln("Usage: examples <example_name>")
fmt.eprintln("Available examples: hellope-shapes, hellope-text, hellope-clay")
os.exit(1)
}
switch args[1] {
case "hellope-clay": hellope_clay()
case "hellope-shapes": hellope_shapes()
case "hellope-text": hellope_text()
case:
fmt.eprintf("Unknown example: %v\n", args[1])
fmt.eprintln("Available examples: hellope-shapes, hellope-text, hellope-clay")
os.exit(1)
}
}
-420
View File
@@ -1,420 +0,0 @@
package examples
import "core:os"
import sdl "vendor:sdl3"
import "../../draw"
import "../../draw/draw_qr"
import cyber "../cybersteel"
textures :: proc() {
if !sdl.Init({.VIDEO}) do os.exit(1)
window := sdl.CreateWindow("Textures", 800, 750, {.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)
FONT_SIZE :: u16(14)
LABEL_OFFSET :: f32(8) // gap between item and its label
//----- Texture registration ----------------------------------
checker_size :: 8
checker_pixels: [checker_size * checker_size * 4]u8
for y in 0 ..< checker_size {
for x in 0 ..< checker_size {
i := (y * checker_size + x) * 4
is_dark := ((x + y) % 2) == 0
val: u8 = 40 if is_dark else 220
checker_pixels[i + 0] = val // R
checker_pixels[i + 1] = val / 2 // G — slight color tint
checker_pixels[i + 2] = val // B
checker_pixels[i + 3] = 255 // A
}
}
checker_texture, _ := draw.register_texture(
draw.Texture_Desc {
width = checker_size,
height = checker_size,
depth_or_layers = 1,
type = .D2,
format = .R8G8B8A8_UNORM,
usage = {.SAMPLER},
mip_levels = 1,
},
checker_pixels[:],
)
defer draw.unregister_texture(checker_texture)
stripe_w :: 16
stripe_h :: 8
stripe_pixels: [stripe_w * stripe_h * 4]u8
for y in 0 ..< stripe_h {
for x in 0 ..< stripe_w {
i := (y * stripe_w + x) * 4
stripe_pixels[i + 0] = u8(x * 255 / (stripe_w - 1)) // R gradient left→right
stripe_pixels[i + 1] = u8(y * 255 / (stripe_h - 1)) // G gradient top→bottom
stripe_pixels[i + 2] = 128 // B constant
stripe_pixels[i + 3] = 255 // A
}
}
stripe_texture, _ := draw.register_texture(
draw.Texture_Desc {
width = stripe_w,
height = stripe_h,
depth_or_layers = 1,
type = .D2,
format = .R8G8B8A8_UNORM,
usage = {.SAMPLER},
mip_levels = 1,
},
stripe_pixels[:],
)
defer draw.unregister_texture(stripe_texture)
qr_texture, _ := draw_qr.register_texture_from("https://x.com/miiilato/status/1880241066471051443")
defer draw.unregister_texture(qr_texture)
spin_angle: f32 = 0
//----- Draw loop ----------------------------------
for {
defer free_all(context.temp_allocator)
ev: sdl.Event
for sdl.PollEvent(&ev) {
if ev.type == .QUIT do return
}
spin_angle += 1
base_layer := draw.begin({width = 800, height = 750})
// Background
draw.rectangle(base_layer, {0, 0, 800, 750}, draw.Color{30, 30, 30, 255})
//----- Row 1: Sampler presets (y=30) ----------------------------------
ROW1_Y :: f32(30)
ITEM_SIZE :: f32(120)
COL1 :: f32(30)
COL2 :: f32(180)
COL3 :: f32(330)
COL4 :: f32(480)
// Nearest (sharp pixel edges)
draw.rectangle(
base_layer,
{COL1, ROW1_Y, ITEM_SIZE, ITEM_SIZE},
draw.Texture_Fill {
id = checker_texture,
tint = draw.WHITE,
uv_rect = {0, 0, 1, 1},
sampler = .Nearest_Clamp,
},
)
draw.text(
base_layer,
"Nearest",
{COL1, ROW1_Y + ITEM_SIZE + LABEL_OFFSET},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.WHITE,
)
// Linear (bilinear blur)
draw.rectangle(
base_layer,
{COL2, ROW1_Y, ITEM_SIZE, ITEM_SIZE},
draw.Texture_Fill {
id = checker_texture,
tint = draw.WHITE,
uv_rect = {0, 0, 1, 1},
sampler = .Linear_Clamp,
},
)
draw.text(
base_layer,
"Linear",
{COL2, ROW1_Y + ITEM_SIZE + LABEL_OFFSET},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.WHITE,
)
// Tiled (4x repeat)
draw.rectangle(
base_layer,
{COL3, ROW1_Y, ITEM_SIZE, ITEM_SIZE},
draw.Texture_Fill {
id = checker_texture,
tint = draw.WHITE,
uv_rect = {0, 0, 4, 4},
sampler = .Nearest_Repeat,
},
)
draw.text(
base_layer,
"Tiled 4x",
{COL3, ROW1_Y + ITEM_SIZE + LABEL_OFFSET},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.WHITE,
)
//----- Row 2: Sampler presets (y=190) ----------------------------------
ROW2_Y :: f32(190)
// 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,
"QR Code",
{COL1, ROW2_Y + ITEM_SIZE + LABEL_OFFSET},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.WHITE,
)
// Rounded corners + outline traces the rounded shape.
draw.rectangle(
base_layer,
{COL2, ROW2_Y, ITEM_SIZE, ITEM_SIZE},
draw.Texture_Fill {
id = checker_texture,
tint = draw.WHITE,
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(
base_layer,
"Rounded",
{COL2, ROW2_Y + ITEM_SIZE + LABEL_OFFSET},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.WHITE,
)
// Rotating + outline rotates with the texture.
rot_rect := draw.Rectangle{COL3, ROW2_Y, ITEM_SIZE, ITEM_SIZE}
draw.rectangle(
base_layer,
rot_rect,
draw.Texture_Fill {
id = checker_texture,
tint = draw.WHITE,
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,
)
draw.text(
base_layer,
"Rotating",
{COL3, ROW2_Y + ITEM_SIZE + LABEL_OFFSET},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.WHITE,
)
//----- Row 3: Fit modes + Per-corner radii (y=360) ----------------------------------
ROW3_Y :: f32(360)
FIT_SIZE :: f32(120) // square target rect
// Stretch
uv_s, sampler_s, inner_s := draw.fit_params(.Stretch, {COL1, ROW3_Y, FIT_SIZE, FIT_SIZE}, stripe_texture)
draw.rectangle(base_layer, {COL1, ROW3_Y, FIT_SIZE, FIT_SIZE}, draw.Color{60, 60, 60, 255}) // bg
draw.rectangle(
base_layer,
inner_s,
draw.Texture_Fill{id = stripe_texture, tint = draw.WHITE, uv_rect = uv_s, sampler = sampler_s},
)
draw.text(
base_layer,
"Stretch",
{COL1, ROW3_Y + FIT_SIZE + LABEL_OFFSET},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.WHITE,
)
// Fill (center-crop)
uv_f, sampler_f, inner_f := draw.fit_params(.Fill, {COL2, ROW3_Y, FIT_SIZE, FIT_SIZE}, stripe_texture)
draw.rectangle(base_layer, {COL2, ROW3_Y, FIT_SIZE, FIT_SIZE}, draw.Color{60, 60, 60, 255})
draw.rectangle(
base_layer,
inner_f,
draw.Texture_Fill{id = stripe_texture, tint = draw.WHITE, uv_rect = uv_f, sampler = sampler_f},
)
draw.text(
base_layer,
"Fill",
{COL2, ROW3_Y + FIT_SIZE + LABEL_OFFSET},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.WHITE,
)
// Fit (letterbox)
uv_ft, sampler_ft, inner_ft := draw.fit_params(.Fit, {COL3, ROW3_Y, FIT_SIZE, FIT_SIZE}, stripe_texture)
draw.rectangle(base_layer, {COL3, ROW3_Y, FIT_SIZE, FIT_SIZE}, draw.Color{60, 60, 60, 255}) // visible margin bg
draw.rectangle(
base_layer,
inner_ft,
draw.Texture_Fill{id = stripe_texture, tint = draw.WHITE, uv_rect = uv_ft, sampler = sampler_ft},
)
draw.text(
base_layer,
"Fit",
{COL3, ROW3_Y + FIT_SIZE + LABEL_OFFSET},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.WHITE,
)
// Per-corner radii + outline traces the asymmetric corner shape.
draw.rectangle(
base_layer,
{COL4, ROW3_Y, FIT_SIZE, FIT_SIZE},
draw.Texture_Fill {
id = checker_texture,
tint = draw.WHITE,
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(
base_layer,
"Per-corner",
{COL4, ROW3_Y + FIT_SIZE + LABEL_OFFSET},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.WHITE,
)
//----- Row 4: Textured shapes (y=520) ----------------------------------
ROW4_Y :: f32(520)
SHAPE_SIZE :: f32(80)
SHAPE_GAP :: f32(30)
SHAPE_COL1 :: f32(30)
SHAPE_COL2 :: SHAPE_COL1 + SHAPE_SIZE + SHAPE_GAP
SHAPE_COL3 :: SHAPE_COL2 + SHAPE_SIZE + SHAPE_GAP
SHAPE_COL4 :: SHAPE_COL3 + SHAPE_SIZE + SHAPE_GAP
SHAPE_COL5 :: SHAPE_COL4 + SHAPE_SIZE + SHAPE_GAP
checker_fill := draw.Texture_Fill {
id = checker_texture,
tint = draw.WHITE,
uv_rect = {0, 0, 1, 1},
sampler = .Nearest_Clamp,
}
// 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,
"Circle",
{SHAPE_COL1, ROW4_Y + SHAPE_SIZE + LABEL_OFFSET},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.WHITE,
)
// Textured ellipse
draw.ellipse(
base_layer,
{SHAPE_COL2 + SHAPE_SIZE / 2, ROW4_Y + SHAPE_SIZE / 2},
SHAPE_SIZE / 2,
SHAPE_SIZE / 3,
checker_fill,
)
draw.text(
base_layer,
"Ellipse",
{SHAPE_COL2, ROW4_Y + SHAPE_SIZE + LABEL_OFFSET},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.WHITE,
)
// Textured polygon (hexagon)
draw.polygon(
base_layer,
{SHAPE_COL3 + SHAPE_SIZE / 2, ROW4_Y + SHAPE_SIZE / 2},
6,
SHAPE_SIZE / 2,
checker_fill,
)
draw.text(
base_layer,
"Polygon",
{SHAPE_COL3, ROW4_Y + SHAPE_SIZE + LABEL_OFFSET},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.WHITE,
)
// Textured ring
draw.ring(
base_layer,
{SHAPE_COL4 + SHAPE_SIZE / 2, ROW4_Y + SHAPE_SIZE / 2},
SHAPE_SIZE / 4,
SHAPE_SIZE / 2,
checker_fill,
)
draw.text(
base_layer,
"Ring",
{SHAPE_COL4, ROW4_Y + SHAPE_SIZE + LABEL_OFFSET},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.WHITE,
)
// Textured line (capsule)
draw.line(
base_layer,
{SHAPE_COL5, ROW4_Y + SHAPE_SIZE / 2},
{SHAPE_COL5 + SHAPE_SIZE, ROW4_Y + SHAPE_SIZE / 2},
checker_fill,
thickness = 20,
)
draw.text(
base_layer,
"Line",
{SHAPE_COL5, ROW4_Y + SHAPE_SIZE + LABEL_OFFSET},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.WHITE,
)
draw.end(gpu, window)
}
}
+688
View File
@@ -0,0 +1,688 @@
package draw
import "core:c"
import "core:log"
import "core:mem"
import sdl "vendor:sdl3"
Vertex :: struct {
position: [2]f32,
uv: [2]f32,
color: Color,
}
TextBatch :: struct {
atlas_texture: ^sdl.GPUTexture,
vertex_start: u32,
vertex_count: u32,
index_start: u32,
index_count: u32,
}
// ----------------------------------------------------------------------------------------------------------------
// ----- SDF primitive types -----------
// ----------------------------------------------------------------------------------------------------------------
Shape_Kind :: enum u8 {
Solid = 0,
RRect = 1,
Circle = 2,
Ellipse = 3,
Segment = 4,
Ring_Arc = 5,
NGon = 6,
}
Shape_Flag :: enum u8 {
Stroke,
}
Shape_Flags :: bit_set[Shape_Flag;u8]
RRect_Params :: struct {
half_size: [2]f32,
radii: [4]f32,
soft_px: f32,
stroke_px: f32,
}
Circle_Params :: struct {
radius: f32,
soft_px: f32,
stroke_px: f32,
_: [5]f32,
}
Ellipse_Params :: struct {
radii: [2]f32,
soft_px: f32,
stroke_px: f32,
_: [4]f32,
}
Segment_Params :: struct {
a: [2]f32,
b: [2]f32,
width: f32,
soft_px: f32,
_: [2]f32,
}
Ring_Arc_Params :: struct {
inner_radius: f32,
outer_radius: f32,
start_rad: f32,
end_rad: f32,
soft_px: f32,
_: [3]f32,
}
NGon_Params :: struct {
radius: f32,
rotation: f32,
sides: f32,
soft_px: f32,
stroke_px: f32,
_: [3]f32,
}
Shape_Params :: struct #raw_union {
rrect: RRect_Params,
circle: Circle_Params,
ellipse: Ellipse_Params,
segment: Segment_Params,
ring_arc: Ring_Arc_Params,
ngon: NGon_Params,
raw: [8]f32,
}
#assert(size_of(Shape_Params) == 32)
// GPU layout: 64 bytes, std430-compatible. The shader declares this as a storage buffer struct.
Primitive :: struct {
bounds: [4]f32, // 0: min_x, min_y, max_x, max_y (world-space, pre-DPI)
color: Color, // 16: u8x4, unpacked in shader via unpackUnorm4x8
kind_flags: u32, // 20: (kind as u32) | (flags as u32 << 8)
_pad: [2]f32, // 24: alignment to vec4 boundary
params: Shape_Params, // 32: two vec4s of shape params
}
#assert(size_of(Primitive) == 64)
pack_kind_flags :: #force_inline proc(kind: Shape_Kind, flags: Shape_Flags) -> u32 {
return u32(kind) | (u32(transmute(u8)flags) << 8)
}
Pipeline_2D_Base :: struct {
sdl_pipeline: ^sdl.GPUGraphicsPipeline,
vertex_buffer: Buffer,
index_buffer: Buffer,
unit_quad_buffer: ^sdl.GPUBuffer,
primitive_buffer: Buffer,
white_texture: ^sdl.GPUTexture,
sampler: ^sdl.GPUSampler,
}
@(private)
create_pipeline_2d_base :: proc(
device: ^sdl.GPUDevice,
window: ^sdl.Window,
sample_count: sdl.GPUSampleCount,
) -> (
pipeline: Pipeline_2D_Base,
ok: bool,
) {
// On failure, clean up any partially-created resources
defer if !ok {
if pipeline.sampler != nil do sdl.ReleaseGPUSampler(device, pipeline.sampler)
if pipeline.white_texture != nil do sdl.ReleaseGPUTexture(device, pipeline.white_texture)
if pipeline.unit_quad_buffer != nil do sdl.ReleaseGPUBuffer(device, pipeline.unit_quad_buffer)
if pipeline.primitive_buffer.gpu != nil do destroy_buffer(device, &pipeline.primitive_buffer)
if pipeline.index_buffer.gpu != nil do destroy_buffer(device, &pipeline.index_buffer)
if pipeline.vertex_buffer.gpu != nil do destroy_buffer(device, &pipeline.vertex_buffer)
if pipeline.sdl_pipeline != nil do sdl.ReleaseGPUGraphicsPipeline(device, pipeline.sdl_pipeline)
}
when ODIN_OS == .Darwin {
base_2d_vert_raw := #load("shaders/generated/base_2d.vert.metal")
base_2d_frag_raw := #load("shaders/generated/base_2d.frag.metal")
} else {
base_2d_vert_raw := #load("shaders/generated/base_2d.vert.spv")
base_2d_frag_raw := #load("shaders/generated/base_2d.frag.spv")
}
log.debug("Loaded", len(base_2d_vert_raw), "vert bytes")
log.debug("Loaded", len(base_2d_frag_raw), "frag bytes")
vert_info := sdl.GPUShaderCreateInfo {
code_size = len(base_2d_vert_raw),
code = raw_data(base_2d_vert_raw),
entrypoint = ENTRY_POINT,
format = SHADER_TYPE,
stage = .VERTEX,
num_uniform_buffers = 1,
num_storage_buffers = 1,
}
frag_info := sdl.GPUShaderCreateInfo {
code_size = len(base_2d_frag_raw),
code = raw_data(base_2d_frag_raw),
entrypoint = ENTRY_POINT,
format = SHADER_TYPE,
stage = .FRAGMENT,
num_samplers = 1,
}
vert_shader := sdl.CreateGPUShader(device, vert_info)
if vert_shader == nil {
log.errorf("Could not create draw vertex shader: %s", sdl.GetError())
return pipeline, false
}
frag_shader := sdl.CreateGPUShader(device, frag_info)
if frag_shader == nil {
sdl.ReleaseGPUShader(device, vert_shader)
log.errorf("Could not create draw fragment shader: %s", sdl.GetError())
return pipeline, false
}
vertex_attributes: [3]sdl.GPUVertexAttribute = {
// position (GLSL location 0)
sdl.GPUVertexAttribute{buffer_slot = 0, location = 0, format = .FLOAT2, offset = 0},
// uv (GLSL location 1)
sdl.GPUVertexAttribute{buffer_slot = 0, location = 1, format = .FLOAT2, offset = size_of([2]f32)},
// color (GLSL location 2, u8x4 normalized to float by GPU)
sdl.GPUVertexAttribute{buffer_slot = 0, location = 2, format = .UBYTE4_NORM, offset = size_of([2]f32) * 2},
}
pipeline_info := sdl.GPUGraphicsPipelineCreateInfo {
vertex_shader = vert_shader,
fragment_shader = frag_shader,
primitive_type = .TRIANGLELIST,
multisample_state = sdl.GPUMultisampleState{sample_count = sample_count},
target_info = sdl.GPUGraphicsPipelineTargetInfo {
color_target_descriptions = &sdl.GPUColorTargetDescription {
format = sdl.GetGPUSwapchainTextureFormat(device, window),
blend_state = sdl.GPUColorTargetBlendState {
enable_blend = true,
enable_color_write_mask = true,
src_color_blendfactor = .SRC_ALPHA,
dst_color_blendfactor = .ONE_MINUS_SRC_ALPHA,
color_blend_op = .ADD,
src_alpha_blendfactor = .SRC_ALPHA,
dst_alpha_blendfactor = .ONE_MINUS_SRC_ALPHA,
alpha_blend_op = .ADD,
color_write_mask = sdl.GPUColorComponentFlags{.R, .G, .B, .A},
},
},
num_color_targets = 1,
},
vertex_input_state = sdl.GPUVertexInputState {
vertex_buffer_descriptions = &sdl.GPUVertexBufferDescription {
slot = 0,
input_rate = .VERTEX,
pitch = size_of(Vertex),
},
num_vertex_buffers = 1,
vertex_attributes = raw_data(vertex_attributes[:]),
num_vertex_attributes = 3,
},
}
pipeline.sdl_pipeline = sdl.CreateGPUGraphicsPipeline(device, pipeline_info)
// Shaders are no longer needed regardless of pipeline creation success
sdl.ReleaseGPUShader(device, vert_shader)
sdl.ReleaseGPUShader(device, frag_shader)
if pipeline.sdl_pipeline == nil {
log.errorf("Failed to create draw graphics pipeline: %s", sdl.GetError())
return pipeline, false
}
// Create vertex buffer
vb_ok: bool
pipeline.vertex_buffer, vb_ok = create_buffer(
device,
size_of(Vertex) * BUFFER_INIT_SIZE,
sdl.GPUBufferUsageFlags{.VERTEX},
)
if !vb_ok do return pipeline, false
// Create index buffer (used by text)
ib_ok: bool
pipeline.index_buffer, ib_ok = create_buffer(
device,
size_of(c.int) * BUFFER_INIT_SIZE,
sdl.GPUBufferUsageFlags{.INDEX},
)
if !ib_ok do return pipeline, false
// Create primitive storage buffer (used by SDF instanced drawing)
pb_ok: bool
pipeline.primitive_buffer, pb_ok = create_buffer(
device,
size_of(Primitive) * BUFFER_INIT_SIZE,
sdl.GPUBufferUsageFlags{.GRAPHICS_STORAGE_READ},
)
if !pb_ok do return pipeline, false
// Create static 6-vertex unit quad buffer (two triangles, TRIANGLELIST)
pipeline.unit_quad_buffer = sdl.CreateGPUBuffer(
device,
sdl.GPUBufferCreateInfo{usage = {.VERTEX}, size = 6 * size_of(Vertex)},
)
if pipeline.unit_quad_buffer == nil {
log.errorf("Failed to create unit quad buffer: %s", sdl.GetError())
return pipeline, false
}
// Create 1x1 white pixel texture
pipeline.white_texture = sdl.CreateGPUTexture(
device,
sdl.GPUTextureCreateInfo {
type = .D2,
format = .R8G8B8A8_UNORM,
usage = {.SAMPLER},
width = 1,
height = 1,
layer_count_or_depth = 1,
num_levels = 1,
sample_count = ._1,
},
)
if pipeline.white_texture == nil {
log.errorf("Failed to create white pixel texture: %s", sdl.GetError())
return pipeline, false
}
// Upload white pixel and unit quad data in a single command buffer
white_pixel := [4]u8{255, 255, 255, 255}
white_transfer := sdl.CreateGPUTransferBuffer(
device,
sdl.GPUTransferBufferCreateInfo{usage = .UPLOAD, size = size_of(white_pixel)},
)
if white_transfer == nil {
log.errorf("Failed to create white pixel transfer buffer: %s", sdl.GetError())
return pipeline, false
}
defer sdl.ReleaseGPUTransferBuffer(device, white_transfer)
white_ptr := sdl.MapGPUTransferBuffer(device, white_transfer, false)
if white_ptr == nil {
log.errorf("Failed to map white pixel transfer buffer: %s", sdl.GetError())
return pipeline, false
}
mem.copy(white_ptr, &white_pixel, size_of(white_pixel))
sdl.UnmapGPUTransferBuffer(device, white_transfer)
quad_verts := [6]Vertex{
{position = {0, 0}}, {position = {1, 0}}, {position = {0, 1}},
{position = {0, 1}}, {position = {1, 0}}, {position = {1, 1}},
}
quad_transfer := sdl.CreateGPUTransferBuffer(
device,
sdl.GPUTransferBufferCreateInfo{usage = .UPLOAD, size = size_of(quad_verts)},
)
if quad_transfer == nil {
log.errorf("Failed to create unit quad transfer buffer: %s", sdl.GetError())
return pipeline, false
}
defer sdl.ReleaseGPUTransferBuffer(device, quad_transfer)
quad_ptr := sdl.MapGPUTransferBuffer(device, quad_transfer, false)
if quad_ptr == nil {
log.errorf("Failed to map unit quad transfer buffer: %s", sdl.GetError())
return pipeline, false
}
mem.copy(quad_ptr, &quad_verts, size_of(quad_verts))
sdl.UnmapGPUTransferBuffer(device, quad_transfer)
upload_cmd := sdl.AcquireGPUCommandBuffer(device)
if upload_cmd == nil {
log.errorf("Failed to acquire command buffer for init upload: %s", sdl.GetError())
return pipeline, false
}
upload_pass := sdl.BeginGPUCopyPass(upload_cmd)
sdl.UploadToGPUTexture(
upload_pass,
sdl.GPUTextureTransferInfo{transfer_buffer = white_transfer},
sdl.GPUTextureRegion{texture = pipeline.white_texture, w = 1, h = 1, d = 1},
false,
)
sdl.UploadToGPUBuffer(
upload_pass,
sdl.GPUTransferBufferLocation{transfer_buffer = quad_transfer},
sdl.GPUBufferRegion{
buffer = pipeline.unit_quad_buffer,
offset = 0,
size = size_of(quad_verts),
},
false,
)
sdl.EndGPUCopyPass(upload_pass)
if !sdl.SubmitGPUCommandBuffer(upload_cmd) {
log.errorf("Failed to submit init upload command buffer: %s", sdl.GetError())
return pipeline, false
}
log.debug("White pixel texture and unit quad buffer created and uploaded")
// Create sampler (shared by shapes and text)
pipeline.sampler = sdl.CreateGPUSampler(
device,
sdl.GPUSamplerCreateInfo {
min_filter = .LINEAR,
mag_filter = .LINEAR,
mipmap_mode = .LINEAR,
address_mode_u = .CLAMP_TO_EDGE,
address_mode_v = .CLAMP_TO_EDGE,
address_mode_w = .CLAMP_TO_EDGE,
},
)
if pipeline.sampler == nil {
log.errorf("Could not create GPU sampler: %s", sdl.GetError())
return pipeline, false
}
log.debug("Done creating unified draw pipeline")
return pipeline, true
}
@(private)
upload :: proc(device: ^sdl.GPUDevice, pass: ^sdl.GPUCopyPass) {
// Upload vertices (shapes then text into one buffer)
shape_vert_count := u32(len(GLOB.tmp_shape_verts))
text_vert_count := u32(len(GLOB.tmp_text_verts))
total_vert_count := shape_vert_count + text_vert_count
if total_vert_count > 0 {
total_vert_size := total_vert_count * size_of(Vertex)
shape_vert_size := shape_vert_count * size_of(Vertex)
text_vert_size := text_vert_count * size_of(Vertex)
grow_buffer_if_needed(
device,
&GLOB.pipeline_2d_base.vertex_buffer,
total_vert_size,
sdl.GPUBufferUsageFlags{.VERTEX},
)
v_array := sdl.MapGPUTransferBuffer(device, GLOB.pipeline_2d_base.vertex_buffer.transfer, false)
if v_array == nil {
log.panicf("Failed to map vertex transfer buffer: %s", sdl.GetError())
}
if shape_vert_size > 0 {
mem.copy(v_array, raw_data(GLOB.tmp_shape_verts), int(shape_vert_size))
}
if text_vert_size > 0 {
mem.copy(
rawptr(uintptr(v_array) + uintptr(shape_vert_size)),
raw_data(GLOB.tmp_text_verts),
int(text_vert_size),
)
}
sdl.UnmapGPUTransferBuffer(device, GLOB.pipeline_2d_base.vertex_buffer.transfer)
sdl.UploadToGPUBuffer(
pass,
sdl.GPUTransferBufferLocation{transfer_buffer = GLOB.pipeline_2d_base.vertex_buffer.transfer},
sdl.GPUBufferRegion{
buffer = GLOB.pipeline_2d_base.vertex_buffer.gpu,
offset = 0,
size = total_vert_size,
},
false,
)
}
// Upload text indices
index_count := u32(len(GLOB.tmp_text_indices))
if index_count > 0 {
index_size := index_count * size_of(c.int)
grow_buffer_if_needed(
device,
&GLOB.pipeline_2d_base.index_buffer,
index_size,
sdl.GPUBufferUsageFlags{.INDEX},
)
i_array := sdl.MapGPUTransferBuffer(device, GLOB.pipeline_2d_base.index_buffer.transfer, false)
if i_array == nil {
log.panicf("Failed to map index transfer buffer: %s", sdl.GetError())
}
mem.copy(i_array, raw_data(GLOB.tmp_text_indices), int(index_size))
sdl.UnmapGPUTransferBuffer(device, GLOB.pipeline_2d_base.index_buffer.transfer)
sdl.UploadToGPUBuffer(
pass,
sdl.GPUTransferBufferLocation{transfer_buffer = GLOB.pipeline_2d_base.index_buffer.transfer},
sdl.GPUBufferRegion{
buffer = GLOB.pipeline_2d_base.index_buffer.gpu,
offset = 0,
size = index_size,
},
false,
)
}
// Upload SDF primitives
prim_count := u32(len(GLOB.tmp_primitives))
if prim_count > 0 {
prim_size := prim_count * size_of(Primitive)
grow_buffer_if_needed(
device,
&GLOB.pipeline_2d_base.primitive_buffer,
prim_size,
sdl.GPUBufferUsageFlags{.GRAPHICS_STORAGE_READ},
)
p_array := sdl.MapGPUTransferBuffer(
device, GLOB.pipeline_2d_base.primitive_buffer.transfer, false,
)
if p_array == nil {
log.panicf("Failed to map primitive transfer buffer: %s", sdl.GetError())
}
mem.copy(p_array, raw_data(GLOB.tmp_primitives), int(prim_size))
sdl.UnmapGPUTransferBuffer(device, GLOB.pipeline_2d_base.primitive_buffer.transfer)
sdl.UploadToGPUBuffer(
pass,
sdl.GPUTransferBufferLocation{
transfer_buffer = GLOB.pipeline_2d_base.primitive_buffer.transfer,
},
sdl.GPUBufferRegion{
buffer = GLOB.pipeline_2d_base.primitive_buffer.gpu,
offset = 0,
size = prim_size,
},
false,
)
}
}
@(private)
draw_layer :: proc(
device: ^sdl.GPUDevice,
window: ^sdl.Window,
cmd_buffer: ^sdl.GPUCommandBuffer,
render_texture: ^sdl.GPUTexture,
swapchain_w: u32,
swapchain_h: u32,
clear_color: [4]f32,
layer: ^Layer,
) {
if layer.sub_batch_len == 0 {
if !GLOB.cleared {
pass := sdl.BeginGPURenderPass(
cmd_buffer,
&sdl.GPUColorTargetInfo {
texture = render_texture,
clear_color = sdl.FColor {
clear_color[0], clear_color[1], clear_color[2], clear_color[3],
},
load_op = .CLEAR,
store_op = .STORE,
},
1,
nil,
)
sdl.EndGPURenderPass(pass)
GLOB.cleared = true
}
return
}
render_pass := sdl.BeginGPURenderPass(
cmd_buffer,
&sdl.GPUColorTargetInfo {
texture = render_texture,
clear_color = sdl.FColor {
clear_color[0], clear_color[1], clear_color[2], clear_color[3],
},
load_op = GLOB.cleared ? .LOAD : .CLEAR,
store_op = .STORE,
},
1,
nil,
)
GLOB.cleared = true
sdl.BindGPUGraphicsPipeline(render_pass, GLOB.pipeline_2d_base.sdl_pipeline)
// Bind storage buffer (read by vertex shader in SDF mode)
sdl.BindGPUVertexStorageBuffers(
render_pass,
0,
([^]^sdl.GPUBuffer)(&GLOB.pipeline_2d_base.primitive_buffer.gpu),
1,
)
// Always bind index buffer — harmless if no indexed draws are issued
sdl.BindGPUIndexBuffer(
render_pass,
sdl.GPUBufferBinding{buffer = GLOB.pipeline_2d_base.index_buffer.gpu, offset = 0},
._32BIT,
)
// Shorthand aliases for frequently-used pipeline resources
main_vbuf := GLOB.pipeline_2d_base.vertex_buffer.gpu
unit_quad := GLOB.pipeline_2d_base.unit_quad_buffer
white := GLOB.pipeline_2d_base.white_texture
sampler := GLOB.pipeline_2d_base.sampler
w := f32(swapchain_w)
h := f32(swapchain_h)
// Initial GPU state: tessellated mode, main vertex buffer, no atlas bound yet
push_globals(cmd_buffer, w, h, .Tessellated)
sdl.BindGPUVertexBuffers(
render_pass, 0, &sdl.GPUBufferBinding{buffer = main_vbuf, offset = 0}, 1,
)
current_mode: Draw_Mode = .Tessellated
current_vbuf := main_vbuf
current_atlas: ^sdl.GPUTexture
// Text vertices live after shape vertices in the GPU vertex buffer
text_vertex_gpu_base := u32(len(GLOB.tmp_shape_verts))
for &scissor in GLOB.scissors[layer.scissor_start:][:layer.scissor_len] {
sdl.SetGPUScissor(render_pass, scissor.bounds)
for &batch in GLOB.tmp_sub_batches[scissor.sub_batch_start:][:scissor.sub_batch_len] {
switch batch.kind {
case .Shapes:
if current_mode != .Tessellated {
push_globals(cmd_buffer, w, h, .Tessellated)
current_mode = .Tessellated
}
if current_vbuf != main_vbuf {
sdl.BindGPUVertexBuffers(
render_pass, 0,
&sdl.GPUBufferBinding{buffer = main_vbuf, offset = 0}, 1,
)
current_vbuf = main_vbuf
}
if current_atlas != white {
sdl.BindGPUFragmentSamplers(
render_pass, 0,
&sdl.GPUTextureSamplerBinding{texture = white, sampler = sampler}, 1,
)
current_atlas = white
}
sdl.DrawGPUPrimitives(render_pass, batch.count, 1, batch.offset, 0)
case .Text:
if current_mode != .Tessellated {
push_globals(cmd_buffer, w, h, .Tessellated)
current_mode = .Tessellated
}
if current_vbuf != main_vbuf {
sdl.BindGPUVertexBuffers(
render_pass, 0,
&sdl.GPUBufferBinding{buffer = main_vbuf, offset = 0}, 1,
)
current_vbuf = main_vbuf
}
chunk := &GLOB.tmp_text_batches[batch.offset]
if current_atlas != chunk.atlas_texture {
sdl.BindGPUFragmentSamplers(
render_pass, 0,
&sdl.GPUTextureSamplerBinding {
texture = chunk.atlas_texture,
sampler = sampler,
},
1,
)
current_atlas = chunk.atlas_texture
}
sdl.DrawGPUIndexedPrimitives(
render_pass,
chunk.index_count,
1,
chunk.index_start,
i32(text_vertex_gpu_base + chunk.vertex_start),
0,
)
case .SDF:
if current_mode != .SDF {
push_globals(cmd_buffer, w, h, .SDF)
current_mode = .SDF
}
if current_vbuf != unit_quad {
sdl.BindGPUVertexBuffers(
render_pass, 0,
&sdl.GPUBufferBinding{buffer = unit_quad, offset = 0}, 1,
)
current_vbuf = unit_quad
}
if current_atlas != white {
sdl.BindGPUFragmentSamplers(
render_pass, 0,
&sdl.GPUTextureSamplerBinding{texture = white, sampler = sampler}, 1,
)
current_atlas = white
}
sdl.DrawGPUPrimitives(render_pass, 6, batch.count, 0, batch.offset)
}
}
}
sdl.EndGPURenderPass(render_pass)
}
destroy_pipeline_2d_base :: proc(device: ^sdl.GPUDevice, pipeline: ^Pipeline_2D_Base) {
destroy_buffer(device, &pipeline.vertex_buffer)
destroy_buffer(device, &pipeline.index_buffer)
destroy_buffer(device, &pipeline.primitive_buffer)
if pipeline.unit_quad_buffer != nil {
sdl.ReleaseGPUBuffer(device, pipeline.unit_quad_buffer)
}
sdl.ReleaseGPUTexture(device, pipeline.white_texture)
sdl.ReleaseGPUSampler(device, pipeline.sampler)
sdl.ReleaseGPUGraphicsPipeline(device, pipeline.sdl_pipeline)
}
@@ -1,118 +0,0 @@
#pragma clang diagnostic ignored "-Wmissing-prototypes"
#include <metal_stdlib>
#include <simd/simd.h>
using namespace metal;
struct Uniforms
{
float2 inv_working_size;
uint pair_count;
uint mode;
float2 direction;
float inv_downsample_factor;
float _pad0;
float4 kernel0[32];
};
struct main0_out
{
float4 out_color [[color(0)]];
};
struct main0_in
{
float2 p_local [[user(locn0)]];
float4 f_color [[user(locn1)]];
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))
float3 blur_sample(thread const float2& uv, constant Uniforms& _108, texture2d<float> blur_input_tex, sampler blur_input_texSmplr)
{
float3 color = blur_input_tex.sample(blur_input_texSmplr, uv).xyz * _108.kernel0[0].x;
float2 axis_step = _108.direction * _108.inv_working_size;
for (uint i = 1u; i < _108.pair_count; i++)
{
float w = _108.kernel0[i].x;
float off = _108.kernel0[i].y;
float2 step_uv = axis_step * off;
color += (blur_input_tex.sample(blur_input_texSmplr, (uv - step_uv)).xyz * w);
color += (blur_input_tex.sample(blur_input_texSmplr, (uv + step_uv)).xyz * w);
}
return color;
}
static inline __attribute__((always_inline))
float sdRoundedBox(thread const float2& p, thread const float2& b, thread const float4& r)
{
float2 _36;
if (p.x > 0.0)
{
_36 = r.xy;
}
else
{
_36 = r.zw;
}
float2 rxy = _36;
float _50;
if (p.y > 0.0)
{
_50 = rxy.x;
}
else
{
_50 = rxy.y;
}
float rr = _50;
float2 q = abs(p) - b;
if (rr == 0.0)
{
return fast::max(q.x, q.y);
}
q += float2(rr);
return (fast::min(fast::max(q.x, q.y), 0.0) + length(fast::max(q, float2(0.0)))) - rr;
}
static inline __attribute__((always_inline))
float sdf_alpha(thread const float& d, thread const float& h)
{
return 1.0 - smoothstep(-h, h, d);
}
fragment main0_out main0(main0_in in [[stage_in]], constant Uniforms& _108 [[buffer(0)]], texture2d<float> blur_input_tex [[texture(0)]], sampler blur_input_texSmplr [[sampler(0)]], float4 gl_FragCoord [[position]])
{
main0_out out = {};
if (_108.mode == 0u)
{
float2 uv = gl_FragCoord.xy * _108.inv_working_size;
float2 param = uv;
float3 color = blur_sample(param, _108, blur_input_tex, blur_input_texSmplr);
out.out_color = float4(color, 1.0);
return out;
}
float2 param_1 = in.p_local;
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_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_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));
float param_4 = d_n;
float param_5 = h_n;
float coverage = sdf_alpha(param_4, param_5);
out.out_color = float4(tinted * coverage, coverage);
return out;
}
Binary file not shown.
@@ -1,123 +0,0 @@
#pragma clang diagnostic ignored "-Wmissing-prototypes"
#pragma clang diagnostic ignored "-Wmissing-braces"
#include <metal_stdlib>
#include <simd/simd.h>
using namespace metal;
template<typename T, size_t Num>
struct spvUnsafeArray
{
T elements[Num ? Num : 1];
thread T& operator [] (size_t pos) thread
{
return elements[pos];
}
constexpr const thread T& operator [] (size_t pos) const thread
{
return elements[pos];
}
device T& operator [] (size_t pos) device
{
return elements[pos];
}
constexpr const device T& operator [] (size_t pos) const device
{
return elements[pos];
}
constexpr const constant T& operator [] (size_t pos) const constant
{
return elements[pos];
}
threadgroup T& operator [] (size_t pos) threadgroup
{
return elements[pos];
}
constexpr const threadgroup T& operator [] (size_t pos) const threadgroup
{
return elements[pos];
}
};
struct Uniforms
{
float4x4 projection;
float dpi_scale;
uint mode;
float2 _pad0;
};
struct Gaussian_Blur_Primitive
{
float4 bounds;
float4 radii_ppx;
float2 half_size_ppx;
float half_feather_ppx;
uint color;
};
struct Gaussian_Blur_Primitive_1
{
float4 bounds;
float4 radii_ppx;
float2 half_size_ppx;
float half_feather_ppx;
uint color;
};
struct Gaussian_Blur_Primitives
{
Gaussian_Blur_Primitive_1 primitives[1];
};
constant spvUnsafeArray<float2, 6> _97 = spvUnsafeArray<float2, 6>({ float2(0.0), float2(1.0, 0.0), float2(0.0, 1.0), float2(0.0, 1.0), float2(1.0, 0.0), float2(1.0) });
struct main0_out
{
float2 p_local [[user(locn0)]];
float4 f_color [[user(locn1)]];
float2 f_half_size_ppx [[user(locn2)]];
float4 f_radii_ppx [[user(locn3)]];
float f_half_feather_ppx [[user(locn4)]];
float4 gl_Position [[position]];
};
vertex main0_out main0(constant Uniforms& _13 [[buffer(0)]], const device Gaussian_Blur_Primitives& _69 [[buffer(1)]], uint gl_VertexIndex [[vertex_id]], uint gl_InstanceIndex [[instance_id]])
{
main0_out out = {};
if (_13.mode == 0u)
{
float2 ndc = float2((int(gl_VertexIndex) == 1) ? 3.0 : (-1.0), (int(gl_VertexIndex) == 2) ? 3.0 : (-1.0));
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_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_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_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.
@@ -1,47 +0,0 @@
#include <metal_stdlib>
#include <simd/simd.h>
using namespace metal;
struct Uniforms
{
float2 inv_source_size;
uint downsample_factor;
uint _pad0;
};
struct main0_out
{
float4 out_color [[color(0)]];
};
fragment main0_out main0(constant Uniforms& _18 [[buffer(0)]], texture2d<float> source_tex [[texture(0)]], sampler source_texSmplr [[sampler(0)]], float4 gl_FragCoord [[position]])
{
main0_out out = {};
float2 src_block_center = gl_FragCoord.xy * float(_18.downsample_factor);
if (_18.downsample_factor == 1u)
{
float2 uv = src_block_center * _18.inv_source_size;
out.out_color = source_tex.sample(source_texSmplr, uv);
}
else
{
if (_18.downsample_factor == 2u)
{
float2 uv_1 = src_block_center * _18.inv_source_size;
out.out_color = source_tex.sample(source_texSmplr, uv_1);
}
else
{
float off = float(_18.downsample_factor) * 0.25;
float2 uv_tl = (src_block_center + float2(-off, -off)) * _18.inv_source_size;
float2 uv_tr = (src_block_center + float2(off, -off)) * _18.inv_source_size;
float2 uv_bl = (src_block_center + float2(-off, off)) * _18.inv_source_size;
float2 uv_br = (src_block_center + float2(off)) * _18.inv_source_size;
float4 c = ((source_tex.sample(source_texSmplr, uv_tl) + source_tex.sample(source_texSmplr, uv_tr)) + source_tex.sample(source_texSmplr, uv_bl)) + source_tex.sample(source_texSmplr, uv_br);
out.out_color = c * 0.25;
}
}
return out;
}
Binary file not shown.
@@ -1,18 +0,0 @@
#include <metal_stdlib>
#include <simd/simd.h>
using namespace metal;
struct main0_out
{
float4 gl_Position [[position]];
};
vertex main0_out main0(uint gl_VertexIndex [[vertex_id]])
{
main0_out out = {};
float2 ndc = float2((int(gl_VertexIndex) == 1) ? 3.0 : (-1.0), (int(gl_VertexIndex) == 2) ? 3.0 : (-1.0));
out.gl_Position = float4(ndc, 0.0, 1.0);
return out;
}
Binary file not shown.
+181 -142
View File
@@ -23,220 +23,259 @@ struct main0_in
float2 f_local_or_uv [[user(locn1)]];
float4 f_params [[user(locn2)]];
float4 f_params2 [[user(locn3)]];
uint f_flags [[user(locn4)]];
float4 f_uv_rect [[user(locn6), flat]];
uint4 f_effects [[user(locn7)]];
uint f_kind_flags [[user(locn4)]];
};
static inline __attribute__((always_inline))
float sdRoundedBox(thread const float2& p, thread const float2& b, thread const float4& r)
float sdRoundedBox(thread const float2& p, thread const float2& b, thread float4& r)
{
float2 _48;
float2 _56;
if (p.x > 0.0)
{
_48 = r.xy;
_56 = r.xy;
}
else
{
_48 = r.zw;
_56 = r.zw;
}
float2 rxy = _48;
float _62;
r.x = _56.x;
r.y = _56.y;
float _73;
if (p.y > 0.0)
{
_62 = rxy.x;
_73 = r.x;
}
else
{
_62 = rxy.y;
_73 = r.y;
}
float rr = _62;
float2 q = abs(p) - b;
if (rr == 0.0)
r.x = _73;
float2 q = (abs(p) - b) + float2(r.x);
return (fast::min(fast::max(q.x, q.y), 0.0) + length(fast::max(q, float2(0.0)))) - r.x;
}
static inline __attribute__((always_inline))
float sdf_stroke(thread const float& d, thread const float& stroke_width)
{
return abs(d) - (stroke_width * 0.5);
}
static inline __attribute__((always_inline))
float sdCircle(thread const float2& p, thread const float& r)
{
return length(p) - r;
}
static inline __attribute__((always_inline))
float sdEllipse(thread float2& p, thread float2& ab)
{
p = abs(p);
if (p.x > p.y)
{
return fast::max(q.x, q.y);
p = p.yx;
ab = ab.yx;
}
q += float2(rr);
return (fast::min(fast::max(q.x, q.y), 0.0) + length(fast::max(q, float2(0.0)))) - rr;
float l = (ab.y * ab.y) - (ab.x * ab.x);
float m = (ab.x * p.x) / l;
float m2 = m * m;
float n = (ab.y * p.y) / l;
float n2 = n * n;
float c = ((m2 + n2) - 1.0) / 3.0;
float c3 = (c * c) * c;
float q = c3 + ((m2 * n2) * 2.0);
float d = c3 + (m2 * n2);
float g = m + (m * n2);
float co;
if (d < 0.0)
{
float h = acos(q / c3) / 3.0;
float s = cos(h);
float t = sin(h) * 1.73205077648162841796875;
float rx = sqrt(((-c) * ((s + t) + 2.0)) + m2);
float ry = sqrt(((-c) * ((s - t) + 2.0)) + m2);
co = (((ry + (sign(l) * rx)) + (abs(g) / (rx * ry))) - m) / 2.0;
}
else
{
float h_1 = ((2.0 * m) * n) * sqrt(d);
float s_1 = sign(q + h_1) * powr(abs(q + h_1), 0.3333333432674407958984375);
float u = sign(q - h_1) * powr(abs(q - h_1), 0.3333333432674407958984375);
float rx_1 = (((-s_1) - u) - (c * 4.0)) + (2.0 * m2);
float ry_1 = (s_1 - u) * 1.73205077648162841796875;
float rm = sqrt((rx_1 * rx_1) + (ry_1 * ry_1));
co = (((ry_1 / sqrt(rm - rx_1)) + ((2.0 * g) / rm)) - m) / 2.0;
}
float2 r = ab * float2(co, sqrt(1.0 - (co * co)));
return length(r - p) * sign(p.y - r.y);
}
static inline __attribute__((always_inline))
float sdRegularPolygon(thread const float2& p, thread const float& r, thread const float& n)
float sdSegment(thread const float2& p, thread const float2& a, thread const float2& b)
{
float an = 3.1415927410125732421875 / n;
float bn = mod(precise::atan2(p.y, p.x), 2.0 * an) - an;
return (length(p) * cos(bn)) - r;
float2 pa = p - a;
float2 ba = b - a;
float h = fast::clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
return length(pa - (ba * h));
}
static inline __attribute__((always_inline))
float sdEllipseApprox(thread const float2& p, thread const float2& ab)
float sdf_alpha(thread const float& d, thread const float& soft)
{
float k0 = length(p / ab);
float k1 = length(p / (ab * ab));
return (k0 * (k0 - 1.0)) / k1;
}
static inline __attribute__((always_inline))
float4 gradient_2color(thread const float4& start_color, thread const float4& end_color, thread const float& t)
{
return mix(start_color, end_color, float4(fast::clamp(t, 0.0, 1.0)));
}
static inline __attribute__((always_inline))
float sdf_alpha(thread const float& d, thread const float& h)
{
return 1.0 - smoothstep(-h, h, d);
return 1.0 - smoothstep(-soft, soft, d);
}
fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[texture(0)]], sampler texSmplr [[sampler(0)]])
{
main0_out out = {};
uint kind = in.f_flags & 255u;
uint flags = (in.f_flags >> 8u) & 255u;
uint kind = in.f_kind_flags & 255u;
uint flags = (in.f_kind_flags >> 8u) & 255u;
if (kind == 0u)
{
float4 t = tex.sample(texSmplr, in.f_local_or_uv);
float _195 = t.w;
float4 _197 = t;
float3 _199 = _197.xyz * _195;
t.x = _199.x;
t.y = _199.y;
t.z = _199.z;
out.out_color = in.f_color * t;
out.out_color = in.f_color * tex.sample(texSmplr, in.f_local_or_uv);
return out;
}
float d = 1000000015047466219876688855040.0;
float h = 0.5;
float2 half_size_ppx = in.f_params.xy;
float2 p_local_ppx = in.f_local_or_uv;
float soft = 1.0;
if (kind == 1u)
{
float4 corner_radii_ppx = float4(in.f_params.zw, in.f_params2.xy);
h = in.f_params2.z;
float2 param = p_local_ppx;
float2 param_1 = half_size_ppx;
float4 param_2 = corner_radii_ppx;
d = sdRoundedBox(param, param_1, param_2);
float2 b = in.f_params.xy;
float4 r = float4(in.f_params.zw, in.f_params2.xy);
soft = fast::max(in.f_params2.z, 1.0);
float stroke_px = in.f_params2.w;
float2 param = in.f_local_or_uv;
float2 param_1 = b;
float4 param_2 = r;
float _453 = sdRoundedBox(param, param_1, param_2);
d = _453;
if ((flags & 1u) != 0u)
{
float param_3 = d;
float param_4 = stroke_px;
d = sdf_stroke(param_3, param_4);
}
}
else
{
if (kind == 2u)
{
float radius_ppx = in.f_params.x;
float sides = in.f_params.y;
h = in.f_params.z;
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_ppx = float2(radius_ppx);
float radius = in.f_params.x;
soft = fast::max(in.f_params.y, 1.0);
float stroke_px_1 = in.f_params.z;
float2 param_5 = in.f_local_or_uv;
float param_6 = radius;
d = sdCircle(param_5, param_6);
if ((flags & 1u) != 0u)
{
float param_7 = d;
float param_8 = stroke_px_1;
d = sdf_stroke(param_7, param_8);
}
}
else
{
if (kind == 3u)
{
float2 radii_ppx = in.f_params.xy;
h = in.f_params.z;
float2 param_6 = p_local_ppx;
float2 param_7 = radii_ppx;
d = sdEllipseApprox(param_6, param_7);
half_size_ppx = radii_ppx;
float2 ab = in.f_params.xy;
soft = fast::max(in.f_params.z, 1.0);
float stroke_px_2 = in.f_params.w;
float2 param_9 = in.f_local_or_uv;
float2 param_10 = ab;
float _511 = sdEllipse(param_9, param_10);
d = _511;
if ((flags & 1u) != 0u)
{
float param_11 = d;
float param_12 = stroke_px_2;
d = sdf_stroke(param_11, param_12);
}
}
else
{
if (kind == 4u)
{
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_ppx);
d = fast::max(inner_radius_ppx - r, r - outer_radius_ppx);
if (arc_bits != 0u)
{
float d_start = dot(p_local_ppx, n_start);
float d_end = dot(p_local_ppx, n_end);
float _338;
if (arc_bits == 1u)
{
_338 = fast::max(d_start, d_end);
float2 a = in.f_params.xy;
float2 b_1 = in.f_params.zw;
float width = in.f_params2.x;
soft = fast::max(in.f_params2.y, 1.0);
float2 param_13 = in.f_local_or_uv;
float2 param_14 = a;
float2 param_15 = b_1;
d = sdSegment(param_13, param_14, param_15) - (width * 0.5);
}
else
{
_338 = fast::min(d_start, d_end);
}
float d_wedge = _338;
d = fast::max(d, d_wedge);
}
half_size_ppx = float2(outer_radius_ppx);
}
}
}
}
float grad_magnitude = fast::max(fwidth(d), 9.9999999747524270787835121154785e-07);
d /= grad_magnitude;
h /= grad_magnitude;
float4 shape_color;
if ((flags & 2u) != 0u)
if (kind == 5u)
{
float4 gradient_start = in.f_color;
float4 gradient_end = unpack_unorm4x8_to_float(in.f_effects.x);
if ((flags & 4u) != 0u)
float inner = in.f_params.x;
float outer = in.f_params.y;
float start_rad = in.f_params.z;
float end_rad = in.f_params.w;
soft = fast::max(in.f_params2.x, 1.0);
float r_1 = length(in.f_local_or_uv);
float d_ring = fast::max(inner - r_1, r_1 - outer);
float angle = precise::atan2(in.f_local_or_uv.y, in.f_local_or_uv.x);
if (angle < 0.0)
{
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;
shape_color = gradient_2color(param_8, param_9, param_10);
angle += 6.283185482025146484375;
}
float ang_start = start_rad;
float ang_end = end_rad;
if (ang_start < 0.0)
{
ang_start += 6.283185482025146484375;
}
if (ang_end < 0.0)
{
ang_end += 6.283185482025146484375;
}
float _615;
if (ang_end > ang_start)
{
_615 = float((angle >= ang_start) && (angle <= ang_end));
}
else
{
float2 direction = float2(as_type<half2>(in.f_effects.z));
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;
shape_color = gradient_2color(param_11, param_12, param_13);
_615 = float((angle >= ang_start) || (angle <= ang_end));
}
float in_arc = _615;
if (abs(ang_end - ang_start) >= 6.282185077667236328125)
{
in_arc = 1.0;
}
d = (in_arc > 0.5) ? d_ring : 1000000015047466219876688855040.0;
}
else
{
if (kind == 6u)
{
float radius_1 = in.f_params.x;
float rotation = in.f_params.y;
float sides = in.f_params.z;
soft = fast::max(in.f_params.w, 1.0);
float stroke_px_3 = in.f_params2.x;
float2 p = in.f_local_or_uv;
float c = cos(rotation);
float s = sin(rotation);
p = float2x2(float2(c, -s), float2(s, c)) * p;
float an = 3.1415927410125732421875 / sides;
float bn = mod(precise::atan2(p.y, p.x), 2.0 * an) - an;
d = (length(p) * cos(bn)) - radius_1;
if ((flags & 1u) != 0u)
{
float4 uv_rect = in.f_uv_rect;
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);
}
else
{
shape_color = in.f_color;
float param_16 = d;
float param_17 = stroke_px_3;
d = sdf_stroke(param_16, param_17);
}
}
}
}
}
}
if ((flags & 8u) != 0u)
{
float4 ol_color = unpack_unorm4x8_to_float(in.f_effects.y);
float ol_width = float2(as_type<half2>(in.f_effects.w)).x / grad_magnitude;
float param_14 = d;
float param_15 = h;
float fill_cov = sdf_alpha(param_14, param_15);
float param_16 = d - ol_width;
float param_17 = h;
float total_cov = sdf_alpha(param_16, param_17);
float outline_cov = fast::max(total_cov - fill_cov, 0.0);
float3 rgb_pm = ((shape_color.xyz * shape_color.w) * fill_cov) + ((ol_color.xyz * ol_color.w) * outline_cov);
float alpha_pm = (shape_color.w * fill_cov) + (ol_color.w * outline_cov);
out.out_color = float4(rgb_pm, alpha_pm);
}
else
{
float param_18 = d;
float param_19 = h;
float param_19 = soft;
float alpha = sdf_alpha(param_18, param_19);
out.out_color = float4((shape_color.xyz * shape_color.w) * alpha, shape_color.w * alpha);
}
out.out_color = float4(in.f_color.xyz, in.f_color.w * alpha);
return out;
}
Binary file not shown.
+29 -61
View File
@@ -10,35 +10,29 @@ struct Uniforms
uint mode;
};
struct Core_2D_Primitive
struct Primitive
{
float4 bounds;
uint color;
uint flags;
uint rotation_sc;
float _pad;
uint kind_flags;
float2 _pad;
float4 params;
float4 params2;
float4 uv_rect;
uint4 effects;
};
struct Core_2D_Primitive_1
struct Primitive_1
{
float4 bounds;
uint color;
uint flags;
uint rotation_sc;
float _pad;
uint kind_flags;
float2 _pad;
float4 params;
float4 params2;
float4 uv_rect;
uint4 effects;
};
struct Core_2D_Primitives
struct Primitives
{
Core_2D_Primitive_1 primitives[1];
Primitive_1 primitives[1];
};
struct main0_out
@@ -47,9 +41,7 @@ struct main0_out
float2 f_local_or_uv [[user(locn1)]];
float4 f_params [[user(locn2)]];
float4 f_params2 [[user(locn3)]];
uint f_flags [[user(locn4)]];
float4 f_uv_rect [[user(locn6)]];
uint4 f_effects [[user(locn7)]];
uint f_kind_flags [[user(locn4)]];
float4 gl_Position [[position]];
};
@@ -60,60 +52,36 @@ 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& _31 [[buffer(1)]], uint gl_InstanceIndex [[instance_id]])
vertex main0_out main0(main0_in in [[stage_in]], constant Uniforms& _12 [[buffer(0)]], const device Primitives& _70 [[buffer(1)]], uint gl_InstanceIndex [[instance_id]])
{
main0_out out = {};
if (_12.mode == 1u)
{
Core_2D_Primitive p;
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;
float2 local = (world_pos - center) * _12.dpi_scale;
uint flags = (p.flags >> 8u) & 255u;
if ((flags & 16u) != 0u)
{
float2 sc = float2(as_type<half2>(p.rotation_sc));
local = float2((sc.y * local.x) + (sc.x * local.y), ((-sc.x) * local.x) + (sc.y * local.y));
}
out.f_color = unpack_unorm4x8_to_float(p.color);
out.f_local_or_uv = local;
out.f_params = p.params;
out.f_params2 = p.params2;
out.f_flags = p.flags;
out.f_uv_rect = p.uv_rect;
out.f_effects = p.effects;
out.gl_Position = _12.projection * float4(world_pos * _12.dpi_scale, 0.0, 1.0);
}
else
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);
float2 _199;
if (_12.mode == 2u)
{
_199 = in.v_position;
out.f_kind_flags = 0u;
out.gl_Position = _12.projection * float4(in.v_position * _12.dpi_scale, 0.0, 1.0);
}
else
{
_199 = in.v_position * _12.dpi_scale;
}
float2 pos = _199;
out.gl_Position = _12.projection * float4(pos, 0.0, 1.0);
Primitive p;
p.bounds = _70.primitives[int(gl_InstanceIndex)].bounds;
p.color = _70.primitives[int(gl_InstanceIndex)].color;
p.kind_flags = _70.primitives[int(gl_InstanceIndex)].kind_flags;
p._pad = _70.primitives[int(gl_InstanceIndex)]._pad;
p.params = _70.primitives[int(gl_InstanceIndex)].params;
p.params2 = _70.primitives[int(gl_InstanceIndex)].params2;
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;
out.f_color = unpack_unorm4x8_to_float(p.color);
out.f_local_or_uv = (world_pos - center) * _12.dpi_scale;
out.f_params = p.params;
out.f_params2 = p.params2;
out.f_kind_flags = p.kind_flags;
out.gl_Position = _12.projection * float4(world_pos * _12.dpi_scale, 0.0, 1.0);
}
return out;
}
Binary file not shown.
-155
View File
@@ -1,155 +0,0 @@
#version 450 core
// Unified backdrop blur fragment shader.
// Handles both the 1D separable blur passes (mode 0, used for BOTH the H-pass and V-pass;
// `direction` picks the axis) and the composite pass (mode 1, reads the fully-blurred
// working texture, masks via RRect SDF, applies tint, and writes to source_texture with
// premultiplied-over blending). Working textures are sized at the full swapchain resolution;
// downsampled content occupies only a sub-rect at downsample factor > 1 (set via viewport).
//
// The composite blends with source_texture via the standard premultiplied-over blend state
// (ONE, ONE_MINUS_SRC_ALPHA).
//
// Backdrop primitives are tint-only — there is no outline. A specialized edge effect
// (e.g. liquid-glass-style refraction outlines) would be implemented as a dedicated
// primitive type with its own pipeline.
//
// Two modes, structurally distinct:
//
// Mode 0: 1D separable blur. Used for BOTH the H-pass and V-pass; `direction` (set in the
// per-pass uniforms) picks (1,0) for H or (0,1) for V. Reads the previous working-
// res texture and writes the next working-res texture. Fullscreen-triangle vertex
// output; gl_FragCoord.xy is in working-res target pixel space; UV =
// gl_FragCoord.xy * inv_working_size.
//
// Mode 1: composite. Reads the fully-blurred working-res texture, applies the SDF mask and
// tint, writes to source_texture. Instanced unit-quad vertex output covering the
// per-primitive bounds; gl_FragCoord.xy is in the full-resolution render target;
// UV into the blurred working texture =
// (gl_FragCoord.xy * inv_downsample_factor) * inv_working_size.
// No kernel is applied here — the blur is already complete.
//
// V-blur is run as its own working→working pass rather than folded into the composite. The
// folded variant produced a horizontal-vs-vertical asymmetry artifact: when V-blur sampled
// the H-blur output through the bilinear-upsample/SDF-mask/tint pipeline in one shader
// invocation, horizontal source features ended up looking sharper than vertical ones.
// Matching V's structure exactly to H's restores symmetry.
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_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;
// --- Sampler ---
// Mode 0: bound to downsample_texture. Mode 1: bound to h_blur_texture.
layout(set = 2, binding = 0) uniform sampler2D blur_input_tex;
// --- Uniforms (set 3) ---
// Per-bracket-substage. `mode` matches the vertex shader's mode (0 = H, 1 = V).
// `direction` selects the kernel axis for blur offsets.
// `kernel` holds the per-sigma weight/offset pairs computed CPU-side using the
// linear-sampling pair adjustment (RAD/Rákos).
layout(set = 3, binding = 0) uniform Uniforms {
vec2 inv_working_size; // 1.0 / working-resolution texture dimensions
uint pair_count; // number of (weight, offset) pairs; pair[0] is the center
uint mode; // 0 = H-blur, 1 = V-composite
vec2 direction; // (1,0) for H, (0,1) for V — multiplied into the kernel offset
float inv_downsample_factor; // 1.0 / downsample_factor (mode 1 only; mode 0 ignores)
float _pad0;
vec4 kernel[MAX_KERNEL_PAIRS]; // .x = weight (paired-sum for idx>0), .y = offset (texels)
};
// ---------------------------------------------------------------------------------------------------------------------
// ----- SDF helper --------------------
// ---------------------------------------------------------------------------------------------------------------------
float sdRoundedBox(vec2 p, vec2 b, vec4 r) {
vec2 rxy = (p.x > 0.0) ? r.xy : r.zw;
float rr = (p.y > 0.0) ? rxy.x : rxy.y;
vec2 q = abs(p) - b;
if (rr == 0.0) {
return max(q.x, q.y);
}
q += rr;
return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - rr;
}
float sdf_alpha(float d, float h) {
return 1.0 - smoothstep(-h, h, d);
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- Blur sample loop --------------
// ---------------------------------------------------------------------------------------------------------------------
vec3 blur_sample(vec2 uv) {
vec3 color = kernel[0].x * texture(blur_input_tex, uv).rgb;
// Per-pair offset in texel space, projected onto the active axis.
vec2 axis_step = direction * inv_working_size;
for (uint i = 1u; i < pair_count; i += 1u) {
float w = kernel[i].x;
float off = kernel[i].y;
vec2 step_uv = off * axis_step;
color += w * texture(blur_input_tex, uv - step_uv).rgb;
color += w * texture(blur_input_tex, uv + step_uv).rgb;
}
return color;
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- Main --------------------------
// ---------------------------------------------------------------------------------------------------------------------
void main() {
if (mode == 0u) {
// ---- Mode 0: 1D separable blur (used for both H-pass and V-pass).
// gl_FragCoord is in working-res target pixel space; sample the previous working-res
// texture along `direction` with the kernel.
vec2 uv = gl_FragCoord.xy * inv_working_size;
vec3 color = blur_sample(uv);
out_color = vec4(color, 1.0);
return;
}
// ---- Mode 1: composite per-primitive.
// RRect SDF — early discard for fragments well outside the masked region.
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_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
// already produced the final blurred image; this is just an upsample + tint.
vec2 uv = (gl_FragCoord.xy * inv_downsample_factor) * inv_working_size;
vec3 color = texture(blur_input_tex, uv).rgb;
// Tint composition: inside the masked region the panel is fully opaque — it completely
// hides the original framebuffer content, just like real frosted glass and like iOS
// UIBlurEffect / CSS backdrop-filter. f_color.rgb specifies the tint color; f_color.a
// specifies the tint *mix strength* (NOT panel opacity). At alpha=0 we see the pure
// blur; at alpha=255 we see the blur fully multiplied by the tint color.
//
// Output is premultiplied to match the ONE, ONE_MINUS_SRC_ALPHA blend state. Coverage
// (the SDF mask's edge AA) modulates only the alpha channel, never the panel-vs-source
// blend; that way edge pixels still feather correctly while mid-panel pixels stay fully
// opaque.
mediump vec3 tinted = mix(color, color * f_color.rgb, f_color.a);
mediump float coverage = sdf_alpha(d_n, h_n);
out_color = vec4(tinted * coverage, coverage);
}
-110
View File
@@ -1,110 +0,0 @@
#version 450 core
// Unified backdrop blur vertex shader.
// Handles both the 1D separable blur passes (fullscreen triangle, mode 0; used for
// BOTH the H-pass and V-pass) and the composite pass (instanced unit-quad over
// Gaussian_Blur_Primitive storage buffer, mode 1) for the second PSO of the backdrop bracket.
// The first PSO (downsample) uses backdrop_fullscreen.vert.
//
// No vertex buffer for either mode. Mode 0 uses gl_VertexIndex 0..2 for a single
// fullscreen triangle; mode 1 uses gl_VertexIndex 0..5 for a unit-quad (two
// triangles, TRIANGLELIST topology) and gl_InstanceIndex to select the primitive.
//
// Mode 0 viewport+scissor are CPU-set per sigma group to the work region (union AABB
// of that group's backdrop primitives + halo, clamped to swapchain bounds). Mode 1
// renders into source_texture with the screen-space orthographic projection; the
// per-primitive bounds drive the quad in screen space.
//
// Backdrop primitives have NO rotation — backdrop sampling is in screen space, so
// a rotated mask over a stationary blur sample would look wrong.
// --- Outputs to fragment shader ---
// p_local: shape-local position in physical pixels (origin at shape center).
// Only meaningful in mode 1 (V-composite). Zero-init for mode 0.
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_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
// Vertex_Uniforms_2D. `mode` selects between H-blur (0) and V-composite (1).
layout(set = 1, binding = 0) uniform Uniforms {
mat4 projection;
float dpi_scale;
uint mode; // 0 = H-blur, 1 = V-composite
vec2 _pad0;
};
// --- Gaussian blur primitive storage buffer (set 0) ---
// 48 bytes, std430-natural layout (no implicit padding). vec4 members are
// front-loaded so their 16-byte alignment is satisfied without holes; the
// vec2 and scalar tail packs tight to land the struct at a clean 48-byte
// stride (a multiple of 16, so the array stride needs no rounding either).
// Field semantics match the CPU-side Gaussian_Blur_Primitive declared in
// levlib/draw/backdrop.odin; keep both in sync.
//
// Gaussian blur primitives are tint-only: outline is intentionally absent. Specialized
// 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, 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
};
layout(std430, set = 0, binding = 0) readonly buffer Gaussian_Blur_Primitives {
Gaussian_Blur_Primitive primitives[];
};
void main() {
if (mode == 0u) {
// ---- Mode 0: H-blur fullscreen triangle ----
// gl_VertexIndex 0 -> ( -1, -1)
// gl_VertexIndex 1 -> ( 3, -1)
// gl_VertexIndex 2 -> ( -1, 3)
vec2 ndc = vec2(
(gl_VertexIndex == 1) ? 3.0 : -1.0,
(gl_VertexIndex == 2) ? 3.0 : -1.0);
gl_Position = vec4(ndc, 0.0, 1.0);
// 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_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];
// Unit-quad corners for TRIANGLELIST (2 triangles, 6 vertices):
// index 0 -> (0,0) index 3 -> (0,1)
// index 1 -> (1,0) index 4 -> (1,0)
// index 2 -> (0,1) index 5 -> (1,1)
vec2 quad_corners[6] = vec2[6](
vec2(0.0, 0.0), vec2(1.0, 0.0), vec2(0.0, 1.0),
vec2(0.0, 1.0), vec2(1.0, 0.0), vec2(1.0, 1.0));
vec2 corner = quad_corners[gl_VertexIndex];
vec2 world_pos = mix(p.bounds.xy, p.bounds.zw, corner);
vec2 center = 0.5 * (p.bounds.xy + p.bounds.zw);
// Shape-local position in physical pixels (no rotation for backdrops).
p_local = (world_pos - center) * dpi_scale;
f_color = unpackUnorm4x8(p.color);
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);
}
}
@@ -1,67 +0,0 @@
#version 450 core
// Backdrop downsample fragment shader.
// Reads source_texture (full-resolution snapshot of pre-bracket framebuffer contents) and
// writes a downsampled copy at factor 1, 2, or 4. The output is the working texture (sized
// at full swapchain resolution); larger factors only fill a sub-rect of it via the CPU-set
// viewport. See backdrop.odin for the factor selection table (Flutter-style).
//
// Shader paths by factor:
//
// factor=1: identity copy. One bilinear tap aligned to the source pixel center. Useful
// when sigma is small enough that any downsample round-trip would visibly soften
// the output (Flutter does this for sigma_phys ≤ 4).
//
// factor=2: each output covers a 2×2 source block. Single bilinear tap at the shared
// corner reads all 4 source pixels with 0.25 weight.
//
// factor=4: each output covers a 4×4 source block. We use 4 bilinear taps, each at the
// shared corner of a 2×2 sub-block. Each tap reads 4 source pixels uniformly;
// combined, the 4 taps sample 16 source pixels arranged uniformly across the
// block (full coverage at factor=4). The factor>=4 path is structured so the
// same shader code would extend to factor=8 (16 pixels of 64) or factor=16 (16
// of 256) if the CPU-side cap is ever raised, though the current cap is 4.
//
// The viewport+scissor are set by the CPU to limit output to the layer's work region in
// working-texture coords (work_region_phys / factor), clamped to the texture bounds.
layout(set = 3, binding = 0) uniform Uniforms {
vec2 inv_source_size; // 1.0 / source_texture pixel dimensions
uint downsample_factor; // 1, 2, 4, 8, or 16
uint _pad0;
};
layout(set = 2, binding = 0) uniform sampler2D source_tex;
layout(location = 0) out vec4 out_color;
void main() {
// Output pixel index (i): gl_FragCoord.xy - 0.5. Source-pixel block top-left for this
// output: i * factor. Center of the block: i*factor + factor/2 = gl_FragCoord.xy * factor.
vec2 src_block_center = gl_FragCoord.xy * float(downsample_factor);
if (downsample_factor == 1u) {
// Identity copy. UV at src_block_center hits the source pixel center directly.
vec2 uv = src_block_center * inv_source_size;
out_color = texture(source_tex, uv);
} else if (downsample_factor == 2u) {
// Single tap at the shared corner of the 2×2 source block; one bilinear sample reads
// all 4 source pixels with equal 0.25 weights — uniform 2×2 box filter for free.
vec2 uv = src_block_center * inv_source_size;
out_color = texture(source_tex, uv);
} else {
// Four taps at offsets ±(factor/4) from the block center. Each tap lands on a corner
// shared by 4 source pixels of a (factor/2)×(factor/2) sub-block (equivalent at the
// bilinear level), giving a 4-tap = 16-source-pixel uniform sample of the block.
float off = float(downsample_factor) * 0.25;
vec2 uv_tl = (src_block_center + vec2(-off, -off)) * inv_source_size;
vec2 uv_tr = (src_block_center + vec2(off, -off)) * inv_source_size;
vec2 uv_bl = (src_block_center + vec2(-off, off)) * inv_source_size;
vec2 uv_br = (src_block_center + vec2(off, off)) * inv_source_size;
vec4 c = texture(source_tex, uv_tl)
+ texture(source_tex, uv_tr)
+ texture(source_tex, uv_bl)
+ texture(source_tex, uv_br);
out_color = c * 0.25;
}
}
@@ -1,21 +0,0 @@
#version 450 core
// Fullscreen-triangle vertex shader for the backdrop downsample and H-blur sub-passes.
// Emits a single triangle covering NDC [-1,1]^2; the rasterizer clips edges outside.
// No vertex buffer; uses gl_VertexIndex to pick corners.
//
// The CPU sets the viewport (and matching scissor) per layer-bracket to limit work to
// the union AABB of the layer's backdrop primitives, expanded by 3*max_sigma and
// clamped to swapchain bounds. The fragment shader uses gl_FragCoord (absolute pixel
// space in the bound target) plus an inv-size uniform to compute its own UVs — see
// each fragment shader for the per-pass sampling math.
void main() {
// gl_VertexIndex 0 -> ( -1, -1)
// gl_VertexIndex 1 -> ( 3, -1)
// gl_VertexIndex 2 -> ( -1, 3)
vec2 ndc = vec2(
(gl_VertexIndex == 1) ? 3.0 : -1.0,
(gl_VertexIndex == 2) ? 3.0 : -1.0);
gl_Position = vec4(ndc, 0.0, 1.0);
}
+141 -140
View File
@@ -1,13 +1,11 @@
#version 450 core
// --- Inputs from vertex shader ---
layout(location = 0) in mediump vec4 f_color;
layout(location = 0) in vec4 f_color;
layout(location = 1) in vec2 f_local_or_uv;
layout(location = 2) in vec4 f_params;
layout(location = 3) in vec4 f_params2;
layout(location = 4) flat in uint f_flags;
layout(location = 6) flat in vec4 f_uv_rect;
layout(location = 7) flat in uvec4 f_effects;
layout(location = 4) flat in uint f_kind_flags;
// --- Output ---
layout(location = 0) out vec4 out_color;
@@ -20,43 +18,68 @@ layout(set = 2, binding = 0) uniform sampler2D tex;
// All operate in physical pixel space — no dpi_scale needed here.
// ---------------------------------------------------------------------------
const float PI = 3.14159265358979;
float sdCircle(vec2 p, float r) {
return length(p) - r;
}
float sdRoundedBox(vec2 p, vec2 b, vec4 r) {
vec2 rxy = (p.x > 0.0) ? r.xy : r.zw;
float rr = (p.y > 0.0) ? rxy.x : rxy.y;
vec2 q = abs(p) - b;
if (rr == 0.0) {
return max(q.x, q.y);
r.xy = (p.x > 0.0) ? r.xy : r.zw;
r.x = (p.y > 0.0) ? r.x : r.y;
vec2 q = abs(p) - b + r.x;
return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - r.x;
}
float sdSegment(vec2 p, vec2 a, vec2 b) {
vec2 pa = p - a, ba = b - a;
float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
return length(pa - ba * h);
}
float sdEllipse(vec2 p, vec2 ab) {
p = abs(p);
if (p.x > p.y) {
p = p.yx;
ab = ab.yx;
}
q += rr;
return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - rr;
float l = ab.y * ab.y - ab.x * ab.x;
float m = ab.x * p.x / l;
float m2 = m * m;
float n = ab.y * p.y / l;
float n2 = n * n;
float c = (m2 + n2 - 1.0) / 3.0;
float c3 = c * c * c;
float q = c3 + m2 * n2 * 2.0;
float d = c3 + m2 * n2;
float g = m + m * n2;
float co;
if (d < 0.0) {
float h = acos(q / c3) / 3.0;
float s = cos(h);
float t = sin(h) * sqrt(3.0);
float rx = sqrt(-c * (s + t + 2.0) + m2);
float ry = sqrt(-c * (s - t + 2.0) + m2);
co = (ry + sign(l) * rx + abs(g) / (rx * ry) - m) / 2.0;
} else {
float h = 2.0 * m * n * sqrt(d);
float s = sign(q + h) * pow(abs(q + h), 1.0 / 3.0);
float u = sign(q - h) * pow(abs(q - h), 1.0 / 3.0);
float rx = -s - u - c * 4.0 + 2.0 * m2;
float ry = (s - u) * sqrt(3.0);
float rm = sqrt(rx * rx + ry * ry);
co = (ry / sqrt(rm - rx) + 2.0 * g / rm - m) / 2.0;
}
vec2 r = ab * vec2(co, sqrt(1.0 - co * co));
return length(r - p) * sign(p.y - r.y);
}
// Approximate ellipse SDF — fast, suitable for UI, NOT a true Euclidean distance.
float sdEllipseApprox(vec2 p, vec2 ab) {
float k0 = length(p / ab);
float k1 = length(p / (ab * ab));
return k0 * (k0 - 1.0) / k1;
float sdf_alpha(float d, float soft) {
return 1.0 - smoothstep(-soft, soft, d);
}
// Regular N-gon SDF (Inigo Quilez).
float sdRegularPolygon(vec2 p, float r, float n) {
float an = 3.141592653589793 / n;
float bn = mod(atan(p.y, p.x), 2.0 * an) - an;
return length(p) * cos(bn) - r;
}
// 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);
}
// ---------------------------------------------------------------------------
// Gradient helpers
// ---------------------------------------------------------------------------
mediump vec4 gradient_2color(mediump vec4 start_color, mediump vec4 end_color, mediump float t) {
return mix(start_color, end_color, clamp(t, 0.0, 1.0));
float sdf_stroke(float d, float stroke_width) {
return abs(d) - stroke_width * 0.5;
}
// ---------------------------------------------------------------------------
@@ -64,128 +87,106 @@ mediump vec4 gradient_2color(mediump vec4 start_color, mediump vec4 end_color, m
// ---------------------------------------------------------------------------
void main() {
uint kind = f_flags & 0xFFu;
uint flags = (f_flags >> 8u) & 0xFFu;
uint kind = f_kind_flags & 0xFFu;
uint flags = (f_kind_flags >> 8u) & 0xFFu;
// Kind 0: Tessellated path — vertex colors arrive premultiplied from CPU.
// Texture samples are straight-alpha (SDL_ttf glyph atlas: rgb=1, a=coverage;
// or the 1x1 white texture: rgba=1). Convert to premultiplied form so the
// blend state (ONE, ONE_MINUS_SRC_ALPHA) composites correctly.
// -----------------------------------------------------------------------
// Kind 0: Tessellated path. Texture multiply for text atlas,
// white pixel for solid shapes.
// -----------------------------------------------------------------------
if (kind == 0u) {
vec4 t = texture(tex, f_local_or_uv);
t.rgb *= t.a;
out_color = f_color * t;
out_color = f_color * texture(tex, f_local_or_uv);
return;
}
// SDF path — dispatch on kind
// -----------------------------------------------------------------------
// SDF path. f_local_or_uv = shape-centered position in physical pixels.
// All dimensional params are already in physical pixels (CPU pre-scaled).
// -----------------------------------------------------------------------
float d = 1e30;
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_ppx = f_local_or_uv; // arrives rotated; vertex shader handled .Rotated
float soft = 1.0;
if (kind == 1u) {
// 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_ppx, half_size_ppx, corner_radii_ppx);
// RRect: rounded box
vec2 b = f_params.xy; // half_size (phys px)
vec4 r = vec4(f_params.zw, f_params2.xy); // corner radii: tr, br, tl, bl
soft = max(f_params2.z, 1.0);
float stroke_px = f_params2.w;
d = sdRoundedBox(f_local_or_uv, b, r);
if ((flags & 1u) != 0u) d = sdf_stroke(d, stroke_px);
}
else if (kind == 2u) {
// 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_ppx, radius_ppx, sides);
half_size_ppx = vec2(radius_ppx); // for gradient UV computation
// Circle
float radius = f_params.x;
soft = max(f_params.y, 1.0);
float stroke_px = f_params.z;
d = sdCircle(f_local_or_uv, radius);
if ((flags & 1u) != 0u) d = sdf_stroke(d, stroke_px);
}
else if (kind == 3u) {
// Ellipse — half_feather_ppx in params.z
vec2 radii_ppx = f_params.xy;
h = f_params.z;
d = sdEllipseApprox(p_local_ppx, radii_ppx);
half_size_ppx = radii_ppx; // for gradient UV computation
// Ellipse
vec2 ab = f_params.xy;
soft = max(f_params.z, 1.0);
float stroke_px = f_params.w;
d = sdEllipse(f_local_or_uv, ab);
if ((flags & 1u) != 0u) d = sdf_stroke(d, stroke_px);
}
else if (kind == 4u) {
// Ring_Arc — half_feather_ppx in params2.z
// Arc mode from flag bits 5-6: 0 = full, 1 = narrow (≤π), 2 = wide (>π)
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;
// Segment (capsule line)
vec2 a = f_params.xy; // already in local physical pixels
vec2 b = f_params.zw;
float width = f_params2.x;
soft = max(f_params2.y, 1.0);
h = f_params2.z;
d = sdSegment(f_local_or_uv, a, b) - width * 0.5;
}
else if (kind == 5u) {
// Ring / Arc
float inner = f_params.x;
float outer = f_params.y;
float start_rad = f_params.z;
float end_rad = f_params.w;
soft = max(f_params2.x, 1.0);
float r = length(p_local_ppx);
d = max(inner_radius_ppx - r, r - outer_radius_ppx);
float r = length(f_local_or_uv);
float d_ring = max(inner - r, r - outer);
if (arc_bits != 0u) {
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);
// Angular clip
float angle = atan(f_local_or_uv.y, f_local_or_uv.x);
if (angle < 0.0) angle += 2.0 * PI;
float ang_start = start_rad;
float ang_end = end_rad;
if (ang_start < 0.0) ang_start += 2.0 * PI;
if (ang_end < 0.0) ang_end += 2.0 * PI;
float in_arc = (ang_end > ang_start)
? ((angle >= ang_start && angle <= ang_end) ? 1.0 : 0.0) : ((angle >= ang_start || angle <= ang_end) ? 1.0 : 0.0);
if (abs(ang_end - ang_start) >= 2.0 * PI - 0.001) in_arc = 1.0;
d = in_arc > 0.5 ? d_ring : 1e30;
}
else if (kind == 6u) {
// Regular N-gon
float radius = f_params.x;
float rotation = f_params.y;
float sides = f_params.z;
soft = max(f_params.w, 1.0);
float stroke_px = f_params2.x;
vec2 p = f_local_or_uv;
float c = cos(rotation), s = sin(rotation);
p = mat2(c, -s, s, c) * p;
float an = PI / sides;
float bn = mod(atan(p.y, p.x), 2.0 * an) - an;
d = length(p) * cos(bn) - radius;
if ((flags & 1u) != 0u) d = sdf_stroke(d, stroke_px);
}
half_size_ppx = vec2(outer_radius_ppx); // for gradient UV computation
}
// --- fwidth-based normalization for correct AA and stroke width ---
float grad_magnitude = max(fwidth(d), 1e-6);
d = d / grad_magnitude;
h = h / grad_magnitude;
// --- Determine shape color based on flags ---
mediump vec4 shape_color;
if ((flags & 2u) != 0u) {
// Gradient active (bit 1)
mediump vec4 gradient_start = f_color;
mediump vec4 gradient_end = unpackUnorm4x8(f_effects.x);
if ((flags & 4u) != 0u) {
// Radial gradient (bit 2): t from distance to center
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_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_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 {
// Solid color
shape_color = f_color;
}
// --- Outline (bit 3) — outer outline via premultiplied compositing ---
// The outline band sits OUTSIDE the original shape boundary (d=0 to d=+ol_width).
// fill_cov covers the interior with AA at d=0; total_cov covers interior+outline with
// AA at d=ol_width. The outline band's coverage is total_cov - fill_cov.
// Output is premultiplied: blend state is ONE, ONE_MINUS_SRC_ALPHA.
if ((flags & 8u) != 0u) {
mediump vec4 ol_color = unpackUnorm4x8(f_effects.y);
// Outline width in f_effects.w (low f16 half)
float ol_width = unpackHalf2x16(f_effects.w).x / grad_magnitude;
float fill_cov = sdf_alpha(d, h);
float total_cov = sdf_alpha(d - ol_width, h);
float outline_cov = max(total_cov - fill_cov, 0.0);
// Premultiplied output — no divide, no threshold check
vec3 rgb_pm = shape_color.rgb * shape_color.a * fill_cov
+ ol_color.rgb * ol_color.a * outline_cov;
float alpha_pm = shape_color.a * fill_cov + ol_color.a * outline_cov;
out_color = vec4(rgb_pm, alpha_pm);
} else {
mediump float alpha = sdf_alpha(d, h);
out_color = vec4(shape_color.rgb * shape_color.a * alpha, shape_color.a * alpha);
}
float alpha = sdf_alpha(d, soft);
out_color = vec4(f_color.rgb, f_color.a * alpha);
}
+26 -70
View File
@@ -1,107 +1,63 @@
#version 450 core
// ---------- Vertex attributes (used in all modes) ----------
// ---------- Vertex attributes (used in both modes) ----------
layout(location = 0) in vec2 v_position;
layout(location = 1) in vec2 v_uv;
layout(location = 2) in vec4 v_color;
// ---------- Outputs to fragment shader ----------
layout(location = 0) out mediump vec4 f_color;
layout(location = 0) out vec4 f_color;
layout(location = 1) out vec2 f_local_or_uv;
layout(location = 2) out vec4 f_params;
layout(location = 3) out vec4 f_params2;
layout(location = 4) flat out uint f_flags;
layout(location = 6) flat out vec4 f_uv_rect;
layout(location = 7) flat out uvec4 f_effects;
layout(location = 4) flat out uint f_kind_flags;
// ---------- 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;
uint mode; // 0 = tessellated, 1 = SDF
};
// ---------- SDF primitive storage buffer ----------
// Mirrors the CPU-side Core_2D_Primitive in core_2d.odin. Named with the
// subsystem prefix so a project-wide grep on the type name matches both the GLSL
// declaration and the Odin declaration.
struct Core_2D_Primitive {
vec4 bounds; // 0-15
uint color; // 16-19
uint flags; // 20-23
uint rotation_sc; // 24-27: packed f16 pair (sin, cos)
float _pad; // 28-31
vec4 params; // 32-47
vec4 params2; // 48-63
vec4 uv_rect; // 64-79: texture UV coordinates (read when .Textured)
uvec4 effects; // 80-95: gradient/outline parameters (read when .Gradient/.Outline)
struct Primitive {
vec4 bounds; // 0-15: min_x, min_y, max_x, max_y
uint color; // 16-19: packed u8x4 (unpack with unpackUnorm4x8)
uint kind_flags; // 20-23: kind | (flags << 8)
vec2 _pad; // 24-31: padding
vec4 params; // 32-47: shape params part 1
vec4 params2; // 48-63: shape params part 2
};
layout(std430, set = 0, binding = 0) readonly buffer Core_2D_Primitives {
Core_2D_Primitive primitives[];
layout(std430, set = 0, binding = 0) readonly buffer Primitives {
Primitive primitives[];
};
// ---------- Entry point ----------
void main() {
if (mode == 1u) {
if (mode == 0u) {
// ---- Mode 0: Tessellated (legacy) ----
f_color = v_color;
f_local_or_uv = v_uv;
f_params = vec4(0.0);
f_params2 = vec4(0.0);
f_kind_flags = 0u;
gl_Position = projection * vec4(v_position * dpi_scale, 0.0, 1.0);
} else {
// ---- Mode 1: SDF instanced quads ----
Core_2D_Primitive p = primitives[gl_InstanceIndex];
Primitive p = primitives[gl_InstanceIndex];
vec2 corner = v_position; // unit quad corners: (0,0)-(1,1)
vec2 world_pos = mix(p.bounds.xy, p.bounds.zw, corner);
vec2 center = 0.5 * (p.bounds.xy + p.bounds.zw);
// Compute shape-local position. Apply inverse rotation here in the vertex
// shader; the rasterizer interpolates the rotated values across the quad,
// which is mathematically equivalent to per-fragment rotation under 2D ortho
// projection. Frees one fragment-shader varying and per-pixel rotation math.
vec2 local = (world_pos - center) * dpi_scale;
uint flags = (p.flags >> 8u) & 0xFFu;
if ((flags & 16u) != 0u) {
// Rotated flag (bit 4); rotation_sc holds packed f16 (sin, cos).
// Inverse rotation matrix R(-angle) = [[cos, sin], [-sin, cos]].
vec2 sc = unpackHalf2x16(p.rotation_sc);
local = vec2(sc.y * local.x + sc.x * local.y,
-sc.x * local.x + sc.y * local.y);
}
f_color = unpackUnorm4x8(p.color);
f_local_or_uv = local; // shape-local physical pixels (rotated if .Rotated set)
f_local_or_uv = (world_pos - center) * dpi_scale; // shape-centered physical pixels
f_params = p.params;
f_params2 = p.params2;
f_flags = p.flags;
f_uv_rect = p.uv_rect;
f_effects = p.effects;
f_kind_flags = p.kind_flags;
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);
}
}
+669
View File
@@ -0,0 +1,669 @@
package draw
import "core:math"
SMOOTH_CIRCLE_ERROR_RATE :: 0.1
// ----- Adaptive tessellation ----
auto_segments :: proc(radius: f32, arc_degrees: f32) -> int {
if radius <= 0 do return 4
phys_radius := radius * GLOB.dpi_scaling
acos_arg := clamp(2 * math.pow(1 - SMOOTH_CIRCLE_ERROR_RATE / phys_radius, 2) - 1, -1, 1)
th := math.acos(acos_arg)
if th <= 0 do return 4
full_circle_segs := int(math.ceil(2 * math.PI / th))
segs := int(f32(full_circle_segs) * arc_degrees / 360.0)
min_segs := max(int(math.ceil(f64(arc_degrees / 90.0))), 4)
return max(segs, min_segs)
}
// ----- Internal helpers ----
@(private = "file")
extrude_line :: proc(
start, end_pos: [2]f32,
thick: f32,
color: Color,
vertices: []Vertex,
offset: int,
) -> int {
direction := end_pos - start
dx := direction[0]
dy := direction[1]
length := math.sqrt(dx * dx + dy * dy)
if length < 0.0001 do return 0
scale := thick / (2 * length)
perpendicular := [2]f32{-dy * scale, dx * scale}
p0 := start + perpendicular
p1 := start - perpendicular
p2 := end_pos - perpendicular
p3 := end_pos + perpendicular
vertices[offset + 0] = sv(p0, color)
vertices[offset + 1] = sv(p1, color)
vertices[offset + 2] = sv(p2, color)
vertices[offset + 3] = sv(p0, color)
vertices[offset + 4] = sv(p2, color)
vertices[offset + 5] = sv(p3, color)
return 6
}
// Create a vertex for solid-color shape drawing (no texture, UV defaults to zero).
@(private = "file")
sv :: proc(pos: [2]f32, color: Color) -> Vertex {
return Vertex{position = pos, color = color}
}
@(private = "file")
emit_rect :: proc(x, y, w, h: f32, color: Color, vertices: []Vertex, offset: int) {
vertices[offset + 0] = sv({x, y}, color)
vertices[offset + 1] = sv({x + w, y}, color)
vertices[offset + 2] = sv({x + w, y + h}, color)
vertices[offset + 3] = sv({x, y}, color)
vertices[offset + 4] = sv({x + w, y + h}, color)
vertices[offset + 5] = sv({x, y + h}, color)
}
// ----- Drawing functions ----
pixel :: proc(layer: ^Layer, pos: [2]f32, color: Color) {
vertices: [6]Vertex
emit_rect(pos[0], pos[1], 1, 1, color, vertices[:], 0)
prepare_shape(layer, vertices[:])
}
rectangle :: proc(
layer: ^Layer,
rect: Rectangle,
color: Color,
origin: [2]f32 = {0, 0},
rotation: f32 = 0,
temp_allocator := context.temp_allocator,
) {
vertices := make([]Vertex, 6, temp_allocator)
if rotation == 0 {
emit_rect(rect.x, rect.y, rect.w, rect.h, color, vertices, 0)
} else {
rad := math.to_radians(rotation)
cos_rotation := math.cos(rad)
sin_rotation := math.sin(rad)
// Corners relative to origin
top_left := [2]f32{-origin[0], -origin[1]}
top_right := [2]f32{rect.w - origin[0], -origin[1]}
bottom_right := [2]f32{rect.w - origin[0], rect.h - origin[1]}
bottom_left := [2]f32{-origin[0], rect.h - origin[1]}
// Translation to final position
translate := [2]f32{rect.x + origin[0], rect.y + origin[1]}
// Rotate and translate each corner
tl :=
[2]f32 {
cos_rotation * top_left[0] - sin_rotation * top_left[1],
sin_rotation * top_left[0] + cos_rotation * top_left[1],
} +
translate
tr :=
[2]f32 {
cos_rotation * top_right[0] - sin_rotation * top_right[1],
sin_rotation * top_right[0] + cos_rotation * top_right[1],
} +
translate
br :=
[2]f32 {
cos_rotation * bottom_right[0] - sin_rotation * bottom_right[1],
sin_rotation * bottom_right[0] + cos_rotation * bottom_right[1],
} +
translate
bl :=
[2]f32 {
cos_rotation * bottom_left[0] - sin_rotation * bottom_left[1],
sin_rotation * bottom_left[0] + cos_rotation * bottom_left[1],
} +
translate
vertices[0] = sv(tl, color)
vertices[1] = sv(tr, color)
vertices[2] = sv(br, color)
vertices[3] = sv(tl, color)
vertices[4] = sv(br, color)
vertices[5] = sv(bl, color)
}
prepare_shape(layer, vertices)
}
rectangle_lines :: proc(
layer: ^Layer,
rect: Rectangle,
color: Color,
thick: f32 = 1,
temp_allocator := context.temp_allocator,
) {
vertices := make([]Vertex, 24, temp_allocator)
// Top edge
emit_rect(rect.x, rect.y, rect.w, thick, color, vertices, 0)
// Bottom edge
emit_rect(rect.x, rect.y + rect.h - thick, rect.w, thick, color, vertices, 6)
// Left edge
emit_rect(rect.x, rect.y + thick, thick, rect.h - thick * 2, color, vertices, 12)
// Right edge
emit_rect(rect.x + rect.w - thick, rect.y + thick, thick, rect.h - thick * 2, color, vertices, 18)
prepare_shape(layer, vertices)
}
rectangle_gradient :: proc(
layer: ^Layer,
rect: Rectangle,
top_left, top_right, bottom_left, bottom_right: Color,
temp_allocator := context.temp_allocator,
) {
vertices := make([]Vertex, 6, temp_allocator)
tl := [2]f32{rect.x, rect.y}
tr := [2]f32{rect.x + rect.w, rect.y}
br := [2]f32{rect.x + rect.w, rect.y + rect.h}
bl := [2]f32{rect.x, rect.y + rect.h}
vertices[0] = sv(tl, top_left)
vertices[1] = sv(tr, top_right)
vertices[2] = sv(br, bottom_right)
vertices[3] = sv(tl, top_left)
vertices[4] = sv(br, bottom_right)
vertices[5] = sv(bl, bottom_left)
prepare_shape(layer, vertices)
}
circle_sector :: proc(
layer: ^Layer,
center: [2]f32,
radius: f32,
start_angle, end_angle: f32,
color: Color,
segments: int = 0,
temp_allocator := context.temp_allocator,
) {
arc_length := abs(end_angle - start_angle)
segs := segments > 0 ? segments : auto_segments(radius, arc_length)
vertex_count := segs * 3
vertices := make([]Vertex, vertex_count, temp_allocator)
start_rad := math.to_radians(start_angle)
end_rad := math.to_radians(end_angle)
step_angle := (end_rad - start_rad) / f32(segs)
for i in 0 ..< segs {
current_angle := start_rad + step_angle * f32(i)
next_angle := start_rad + step_angle * f32(i + 1)
edge_current := center + [2]f32{math.cos(current_angle) * radius, math.sin(current_angle) * radius}
edge_next := center + [2]f32{math.cos(next_angle) * radius, math.sin(next_angle) * radius}
idx := i * 3
vertices[idx + 0] = sv(center, color)
vertices[idx + 1] = sv(edge_next, color)
vertices[idx + 2] = sv(edge_current, color)
}
prepare_shape(layer, vertices)
}
circle_gradient :: proc(
layer: ^Layer,
center: [2]f32,
radius: f32,
inner, outer: Color,
segments: int = 0,
temp_allocator := context.temp_allocator,
) {
segs := segments > 0 ? segments : auto_segments(radius, 360)
vertex_count := segs * 3
vertices := make([]Vertex, vertex_count, temp_allocator)
step_angle := math.TAU / f32(segs)
for i in 0 ..< segs {
current_angle := step_angle * f32(i)
next_angle := step_angle * f32(i + 1)
edge_current := center + [2]f32{math.cos(current_angle) * radius, math.sin(current_angle) * radius}
edge_next := center + [2]f32{math.cos(next_angle) * radius, math.sin(next_angle) * radius}
idx := i * 3
vertices[idx + 0] = sv(center, inner)
vertices[idx + 1] = sv(edge_next, outer)
vertices[idx + 2] = sv(edge_current, outer)
}
prepare_shape(layer, vertices)
}
triangle :: proc(layer: ^Layer, v1, v2, v3: [2]f32, color: Color) {
vertices := [3]Vertex{sv(v1, color), sv(v2, color), sv(v3, color)}
prepare_shape(layer, vertices[:])
}
triangle_lines :: proc(
layer: ^Layer,
v1, v2, v3: [2]f32,
color: Color,
thick: f32 = 1,
temp_allocator := context.temp_allocator,
) {
vertices := make([]Vertex, 18, temp_allocator)
write_offset := 0
write_offset += extrude_line(v1, v2, thick, color, vertices, write_offset)
write_offset += extrude_line(v2, v3, thick, color, vertices, write_offset)
write_offset += extrude_line(v3, v1, thick, color, vertices, write_offset)
if write_offset > 0 {
prepare_shape(layer, vertices[:write_offset])
}
}
triangle_fan :: proc(
layer: ^Layer,
points: [][2]f32,
color: Color,
temp_allocator := context.temp_allocator,
) {
if len(points) < 3 do return
triangle_count := len(points) - 2
vertex_count := triangle_count * 3
vertices := make([]Vertex, vertex_count, temp_allocator)
for i in 1 ..< len(points) - 1 {
idx := (i - 1) * 3
vertices[idx + 0] = sv(points[0], color)
vertices[idx + 1] = sv(points[i], color)
vertices[idx + 2] = sv(points[i + 1], color)
}
prepare_shape(layer, vertices)
}
triangle_strip :: proc(
layer: ^Layer,
points: [][2]f32,
color: Color,
temp_allocator := context.temp_allocator,
) {
if len(points) < 3 do return
triangle_count := len(points) - 2
vertex_count := triangle_count * 3
vertices := make([]Vertex, vertex_count, temp_allocator)
for i in 0 ..< triangle_count {
idx := i * 3
if i % 2 == 0 {
vertices[idx + 0] = sv(points[i], color)
vertices[idx + 1] = sv(points[i + 1], color)
vertices[idx + 2] = sv(points[i + 2], color)
} else {
vertices[idx + 0] = sv(points[i + 1], color)
vertices[idx + 1] = sv(points[i], color)
vertices[idx + 2] = sv(points[i + 2], color)
}
}
prepare_shape(layer, vertices)
}
// ----- SDF drawing functions ----
// Draw a rectangle with per-corner rounding radii via SDF.
rectangle_corners :: proc(
layer: ^Layer,
rect: Rectangle,
radii: [4]f32,
color: Color,
soft_px: f32 = 1.0,
) {
max_radius := min(rect.w, rect.h) * 0.5
tl := clamp(radii[0], 0, max_radius)
tr := clamp(radii[1], 0, max_radius)
br := clamp(radii[2], 0, max_radius)
bl := clamp(radii[3], 0, max_radius)
pad := soft_px / GLOB.dpi_scaling
dpi := GLOB.dpi_scaling
prim := Primitive {
bounds = {rect.x - pad, rect.y - pad, rect.x + rect.w + pad, rect.y + rect.h + pad},
color = color,
kind_flags = pack_kind_flags(.RRect, {}),
}
prim.params.rrect = RRect_Params {
half_size = {rect.w * 0.5 * dpi, rect.h * 0.5 * dpi},
radii = {tr * dpi, br * dpi, tl * dpi, bl * dpi},
soft_px = soft_px,
stroke_px = 0,
}
prepare_sdf_primitive(layer, prim)
}
// Draw a stroked rectangle with per-corner rounding radii via SDF.
rectangle_corners_lines :: proc(
layer: ^Layer,
rect: Rectangle,
radii: [4]f32,
color: Color,
thick: f32 = 1,
soft_px: f32 = 1.0,
) {
max_radius := min(rect.w, rect.h) * 0.5
tl := clamp(radii[0], 0, max_radius)
tr := clamp(radii[1], 0, max_radius)
br := clamp(radii[2], 0, max_radius)
bl := clamp(radii[3], 0, max_radius)
pad := (thick * 0.5 + soft_px) / GLOB.dpi_scaling
dpi := GLOB.dpi_scaling
prim := Primitive {
bounds = {rect.x - pad, rect.y - pad, rect.x + rect.w + pad, rect.y + rect.h + pad},
color = color,
kind_flags = pack_kind_flags(.RRect, {.Stroke}),
}
prim.params.rrect = RRect_Params {
half_size = {rect.w * 0.5 * dpi, rect.h * 0.5 * dpi},
radii = {tr * dpi, br * dpi, tl * dpi, bl * dpi},
soft_px = soft_px,
stroke_px = thick * dpi,
}
prepare_sdf_primitive(layer, prim)
}
// Draw a rectangle with uniform corner rounding via SDF.
rectangle_rounded :: proc(
layer: ^Layer,
rect: Rectangle,
roundness: f32,
color: Color,
soft_px: f32 = 1.0,
) {
cr := min(rect.w, rect.h) * clamp(roundness, 0, 1) * 0.5
if cr < 1 {
rectangle(layer, rect, color)
return
}
rectangle_corners(layer, rect, {cr, cr, cr, cr}, color, soft_px)
}
// Draw a stroked rectangle with uniform corner rounding via SDF.
rectangle_rounded_lines :: proc(
layer: ^Layer,
rect: Rectangle,
roundness: f32,
color: Color,
thick: f32 = 1,
soft_px: f32 = 1.0,
) {
cr := min(rect.w, rect.h) * clamp(roundness, 0, 1) * 0.5
if cr < 1 {
rectangle_lines(layer, rect, color, thick)
return
}
rectangle_corners_lines(layer, rect, {cr, cr, cr, cr}, color, thick, soft_px)
}
// Draw a filled circle via SDF.
circle :: proc(layer: ^Layer, center: [2]f32, radius: f32, color: Color, soft_px: f32 = 1.0) {
pad := soft_px / GLOB.dpi_scaling
dpi := GLOB.dpi_scaling
prim := Primitive {
bounds = {center.x - radius - pad, center.y - radius - pad,
center.x + radius + pad, center.y + radius + pad},
color = color,
kind_flags = pack_kind_flags(.Circle, {}),
}
prim.params.circle = Circle_Params{radius = radius * dpi, soft_px = soft_px}
prepare_sdf_primitive(layer, prim)
}
// Draw a stroked circle via SDF.
circle_lines :: proc(
layer: ^Layer,
center: [2]f32,
radius: f32,
color: Color,
thick: f32 = 1,
soft_px: f32 = 1.0,
) {
pad := (thick * 0.5 + soft_px) / GLOB.dpi_scaling
dpi := GLOB.dpi_scaling
prim := Primitive {
bounds = {center.x - radius - pad, center.y - radius - pad,
center.x + radius + pad, center.y + radius + pad},
color = color,
kind_flags = pack_kind_flags(.Circle, {.Stroke}),
}
prim.params.circle = Circle_Params{
radius = radius * dpi, soft_px = soft_px, stroke_px = thick * dpi,
}
prepare_sdf_primitive(layer, prim)
}
// Draw a filled ellipse via SDF.
ellipse :: proc(
layer: ^Layer,
center: [2]f32,
radius_h, radius_v: f32,
color: Color,
soft_px: f32 = 1.0,
) {
pad := soft_px / GLOB.dpi_scaling
dpi := GLOB.dpi_scaling
prim := Primitive {
bounds = {center.x - radius_h - pad, center.y - radius_v - pad,
center.x + radius_h + pad, center.y + radius_v + pad},
color = color,
kind_flags = pack_kind_flags(.Ellipse, {}),
}
prim.params.ellipse = Ellipse_Params{radii = {radius_h * dpi, radius_v * dpi}, soft_px = soft_px}
prepare_sdf_primitive(layer, prim)
}
// Draw a stroked ellipse via SDF.
ellipse_lines :: proc(
layer: ^Layer,
center: [2]f32,
radius_h, radius_v: f32,
color: Color,
thick: f32 = 1,
soft_px: f32 = 1.0,
) {
// Extra 10% padding: iq's sdEllipse has precision degradation near the tips of highly
// eccentric ellipses, so the quad needs additional breathing room beyond the stroke width.
pad := (max(radius_h, radius_v) * 0.1 + thick * 0.5 + soft_px) / GLOB.dpi_scaling
dpi := GLOB.dpi_scaling
prim := Primitive {
bounds = {center.x - radius_h - pad, center.y - radius_v - pad,
center.x + radius_h + pad, center.y + radius_v + pad},
color = color,
kind_flags = pack_kind_flags(.Ellipse, {.Stroke}),
}
prim.params.ellipse = Ellipse_Params{
radii = {radius_h * dpi, radius_v * dpi}, soft_px = soft_px, stroke_px = thick * dpi,
}
prepare_sdf_primitive(layer, prim)
}
// Draw a filled ring arc via SDF.
ring :: proc(
layer: ^Layer,
center: [2]f32,
inner_radius, outer_radius: f32,
start_angle, end_angle: f32,
color: Color,
soft_px: f32 = 1.0,
) {
pad := soft_px / GLOB.dpi_scaling
dpi := GLOB.dpi_scaling
prim := Primitive {
bounds = {center.x - outer_radius - pad, center.y - outer_radius - pad,
center.x + outer_radius + pad, center.y + outer_radius + pad},
color = color,
kind_flags = pack_kind_flags(.Ring_Arc, {}),
}
prim.params.ring_arc = Ring_Arc_Params {
inner_radius = inner_radius * dpi,
outer_radius = outer_radius * dpi,
start_rad = math.to_radians(start_angle),
end_rad = math.to_radians(end_angle),
soft_px = soft_px,
}
prepare_sdf_primitive(layer, prim)
}
// Draw stroked ring arc outlines via SDF.
ring_lines :: proc(
layer: ^Layer,
center: [2]f32,
inner_radius, outer_radius: f32,
start_angle, end_angle: f32,
color: Color,
thick: f32 = 1,
soft_px: f32 = 1.0,
) {
// Inner arc outline
ring(layer, center, max(0, inner_radius - thick * 0.5), inner_radius + thick * 0.5,
start_angle, end_angle, color, soft_px)
// Outer arc outline
ring(layer, center, max(0, outer_radius - thick * 0.5), outer_radius + thick * 0.5,
start_angle, end_angle, color, soft_px)
// Start cap
start_rad := math.to_radians(start_angle)
end_rad := math.to_radians(end_angle)
inner_start := center + {math.cos(start_rad) * inner_radius, math.sin(start_rad) * inner_radius}
outer_start := center + {math.cos(start_rad) * outer_radius, math.sin(start_rad) * outer_radius}
line(layer, inner_start, outer_start, color, thick, soft_px)
// End cap
inner_end := center + {math.cos(end_rad) * inner_radius, math.sin(end_rad) * inner_radius}
outer_end := center + {math.cos(end_rad) * outer_radius, math.sin(end_rad) * outer_radius}
line(layer, inner_end, outer_end, color, thick, soft_px)
}
// Draw a line segment via SDF.
line :: proc(
layer: ^Layer,
start, end_pos: [2]f32,
color: Color,
thick: f32 = 1,
soft_px: f32 = 1.0,
) {
cap := thick * 0.5 + soft_px / GLOB.dpi_scaling
min_x := min(start.x, end_pos.x) - cap
max_x := max(start.x, end_pos.x) + cap
min_y := min(start.y, end_pos.y) - cap
max_y := max(start.y, end_pos.y) + cap
dpi := GLOB.dpi_scaling
center := [2]f32{(min_x + max_x) * 0.5, (min_y + max_y) * 0.5}
local_a := (start - center) * dpi
local_b := (end_pos - center) * dpi
prim := Primitive {
bounds = {min_x, min_y, max_x, max_y},
color = color,
kind_flags = pack_kind_flags(.Segment, {}),
}
prim.params.segment = Segment_Params {
a = local_a,
b = local_b,
width = thick * dpi,
soft_px = soft_px,
}
prepare_sdf_primitive(layer, prim)
}
// Draw a line strip via decomposed SDF segments.
line_strip :: proc(
layer: ^Layer,
points: [][2]f32,
color: Color,
thick: f32 = 1,
soft_px: f32 = 1.0,
) {
if len(points) < 2 do return
for i in 0 ..< len(points) - 1 {
line(layer, points[i], points[i + 1], color, thick, soft_px)
}
}
// Draw a filled regular polygon via SDF.
poly :: proc(
layer: ^Layer,
center: [2]f32,
sides: int,
radius: f32,
color: Color,
rotation: f32 = 0,
soft_px: f32 = 1.0,
) {
if sides < 3 do return
pad := soft_px / GLOB.dpi_scaling
dpi := GLOB.dpi_scaling
prim := Primitive {
bounds = {center.x - radius - pad, center.y - radius - pad,
center.x + radius + pad, center.y + radius + pad},
color = color,
kind_flags = pack_kind_flags(.NGon, {}),
}
prim.params.ngon = NGon_Params {
radius = radius * math.cos(math.PI / f32(sides)) * dpi,
rotation = math.to_radians(rotation),
sides = f32(sides),
soft_px = soft_px,
}
prepare_sdf_primitive(layer, prim)
}
// Draw a stroked regular polygon via SDF.
poly_lines :: proc(
layer: ^Layer,
center: [2]f32,
sides: int,
radius: f32,
color: Color,
rotation: f32 = 0,
thick: f32 = 1,
soft_px: f32 = 1.0,
) {
if sides < 3 do return
pad := (thick * 0.5 + soft_px) / GLOB.dpi_scaling
dpi := GLOB.dpi_scaling
prim := Primitive {
bounds = {center.x - radius - pad, center.y - radius - pad,
center.x + radius + pad, center.y + radius + pad},
color = color,
kind_flags = pack_kind_flags(.NGon, {.Stroke}),
}
prim.params.ngon = NGon_Params {
radius = radius * math.cos(math.PI / f32(sides)) * dpi,
rotation = math.to_radians(rotation),
sides = f32(sides),
soft_px = soft_px,
stroke_px = thick * dpi,
}
prepare_sdf_primitive(layer, prim)
}
-369
View File
@@ -1,369 +0,0 @@
package tess
import "core:math"
import draw ".."
//INTERNAL
SMOOTH_CIRCLE_ERROR_RATE :: 0.1
auto_segments :: proc(radius: f32, arc_degrees: f32) -> int {
if radius <= 0 do return 4
phys_radius := radius * draw.GLOB.dpi_scaling
acos_arg := clamp(2 * math.pow(1 - SMOOTH_CIRCLE_ERROR_RATE / phys_radius, 2) - 1, -1, 1)
theta := math.acos(acos_arg)
if theta <= 0 do return 4
full_circle_segments := int(math.ceil(2 * math.PI / theta))
segments := int(f32(full_circle_segments) * arc_degrees / 360.0)
min_segments := max(int(math.ceil(f64(arc_degrees / 90.0))), 4)
return max(segments, min_segments)
}
// ----- Internal helpers -----
// 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)}
}
//INTERNAL
emit_rectangle :: proc(
x, y, width, height: f32,
color: draw.Color,
vertices: []draw.Vertex_2D,
offset: int,
) {
vertices[offset + 0] = solid_vertex({x, y}, color)
vertices[offset + 1] = solid_vertex({x + width, y}, color)
vertices[offset + 2] = solid_vertex({x + width, y + height}, color)
vertices[offset + 3] = solid_vertex({x, y}, color)
vertices[offset + 4] = solid_vertex({x + width, y + height}, color)
vertices[offset + 5] = solid_vertex({x, y + height}, color)
}
//INTERNAL
extrude_line :: proc(
start, end_pos: draw.Vec2,
thickness: f32,
color: draw.Color,
vertices: []draw.Vertex_2D,
offset: int,
) -> int {
direction := end_pos - start
delta_x := direction[0]
delta_y := direction[1]
length := math.sqrt(delta_x * delta_x + delta_y * delta_y)
if length < 0.0001 do return 0
scale := thickness / (2 * length)
perpendicular := draw.Vec2{-delta_y * scale, delta_x * scale}
p0 := start + perpendicular
p1 := start - perpendicular
p2 := end_pos - perpendicular
p3 := end_pos + perpendicular
vertices[offset + 0] = solid_vertex(p0, color)
vertices[offset + 1] = solid_vertex(p1, color)
vertices[offset + 2] = solid_vertex(p2, color)
vertices[offset + 3] = solid_vertex(p0, color)
vertices[offset + 4] = solid_vertex(p2, color)
vertices[offset + 5] = solid_vertex(p3, color)
return 6
}
// ----- Public draw -----
pixel :: proc(layer: ^draw.Layer, pos: draw.Vec2, color: draw.Color) {
vertices: [6]draw.Vertex_2D
emit_rectangle(pos[0], pos[1], 1, 1, color, vertices[:], 0)
draw.prepare_shape(layer, vertices[:])
}
triangle :: proc(
layer: ^draw.Layer,
v1, v2, v3: draw.Vec2,
color: draw.Color,
origin: draw.Vec2 = {},
rotation: f32 = 0,
) {
if !draw.needs_transform(origin, rotation) {
vertices := [3]draw.Vertex_2D{solid_vertex(v1, color), solid_vertex(v2, color), solid_vertex(v3, color)}
draw.prepare_shape(layer, vertices[:])
return
}
bounds_min := draw.Vec2{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
transform := draw.build_pivot_rotation(bounds_min, origin, rotation)
local_v1 := v1 - bounds_min
local_v2 := v2 - bounds_min
local_v3 := v3 - bounds_min
vertices := [3]draw.Vertex_2D {
solid_vertex(draw.apply_transform(transform, local_v1), color),
solid_vertex(draw.apply_transform(transform, local_v2), color),
solid_vertex(draw.apply_transform(transform, local_v3), color),
}
draw.prepare_shape(layer, vertices[:])
}
// 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-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_ppx: f32 = draw.DFT_FEATHER_PPX,
origin: draw.Vec2 = {},
rotation: f32 = 0,
) {
// Apply rotation if needed, then work in world space.
p0, p1, p2: draw.Vec2
if !draw.needs_transform(origin, rotation) {
p0 = v1
p1 = v2
p2 = v3
} else {
bounds_min := draw.Vec2{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
transform := draw.build_pivot_rotation(bounds_min, origin, rotation)
p0 = draw.apply_transform(transform, v1 - bounds_min)
p1 = draw.apply_transform(transform, v2 - bounds_min)
p2 = draw.apply_transform(transform, v3 - bounds_min)
}
// Compute outward edge normals (unit length, pointing away from triangle interior).
// Winding-independent: we check against the centroid to ensure normals point outward.
centroid_x := (p0.x + p1.x + p2.x) / 3.0
centroid_y := (p0.y + p1.y + p2.y) / 3.0
edge_normal :: proc(edge_start, edge_end: draw.Vec2, centroid_x, centroid_y: f32) -> draw.Vec2 {
delta_x := edge_end.x - edge_start.x
delta_y := edge_end.y - edge_start.y
length := math.sqrt(delta_x * delta_x + delta_y * delta_y)
if length < 0.0001 do return {0, 0}
inverse_length := 1.0 / length
// Perpendicular: (-delta_y, delta_x) normalized
normal_x := -delta_y * inverse_length
normal_y := delta_x * inverse_length
// Midpoint of the edge
midpoint_x := (edge_start.x + edge_end.x) * 0.5
midpoint_y := (edge_start.y + edge_end.y) * 0.5
// If normal points toward centroid, flip it
if normal_x * (centroid_x - midpoint_x) + normal_y * (centroid_y - midpoint_y) > 0 {
normal_x = -normal_x
normal_y = -normal_y
}
return {normal_x, normal_y}
}
normal_01 := edge_normal(p0, p1, centroid_x, centroid_y)
normal_12 := edge_normal(p1, p2, centroid_x, centroid_y)
normal_20 := edge_normal(p2, p0, centroid_x, centroid_y)
// 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
outer_1_01 := p1 + normal_01 * extrude_distance
outer_1_12 := p1 + normal_12 * extrude_distance
outer_2_12 := p2 + normal_12 * extrude_distance
outer_2_20 := p2 + normal_20 * extrude_distance
outer_0_20 := p0 + normal_20 * extrude_distance
// Premultiplied interior color (solid_vertex does premul internally).
// Outer fringe is BLANK = {0,0,0,0} which is already premul.
transparent := draw.BLANK
// 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)
vertices[1] = solid_vertex(p1, color)
vertices[2] = solid_vertex(p2, color)
// Edge quad: p0→p1 (2 triangles)
vertices[3] = solid_vertex(p0, color)
vertices[4] = solid_vertex(p1, color)
vertices[5] = solid_vertex(outer_1_01, transparent)
vertices[6] = solid_vertex(p0, color)
vertices[7] = solid_vertex(outer_1_01, transparent)
vertices[8] = solid_vertex(outer_0_01, transparent)
// Edge quad: p1→p2 (2 triangles)
vertices[9] = solid_vertex(p1, color)
vertices[10] = solid_vertex(p2, color)
vertices[11] = solid_vertex(outer_2_12, transparent)
vertices[12] = solid_vertex(p1, color)
vertices[13] = solid_vertex(outer_2_12, transparent)
vertices[14] = solid_vertex(outer_1_12, transparent)
// Edge quad: p2→p0 (2 triangles)
vertices[15] = solid_vertex(p2, color)
vertices[16] = solid_vertex(p0, color)
vertices[17] = solid_vertex(outer_0_20, transparent)
vertices[18] = solid_vertex(p2, color)
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[:])
}
triangle_lines :: proc(
layer: ^draw.Layer,
v1, v2, v3: draw.Vec2,
color: draw.Color,
thickness: f32 = draw.DFT_STROKE_THICKNESS,
origin: draw.Vec2 = {},
rotation: f32 = 0,
temp_allocator := context.temp_allocator,
) {
vertices := make([]draw.Vertex_2D, 18, temp_allocator)
defer delete(vertices, temp_allocator)
write_offset := 0
if !draw.needs_transform(origin, rotation) {
write_offset += extrude_line(v1, v2, thickness, color, vertices, write_offset)
write_offset += extrude_line(v2, v3, thickness, color, vertices, write_offset)
write_offset += extrude_line(v3, v1, thickness, color, vertices, write_offset)
} else {
bounds_min := draw.Vec2{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
transform := draw.build_pivot_rotation(bounds_min, origin, rotation)
transformed_v1 := draw.apply_transform(transform, v1 - bounds_min)
transformed_v2 := draw.apply_transform(transform, v2 - bounds_min)
transformed_v3 := draw.apply_transform(transform, v3 - bounds_min)
write_offset += extrude_line(transformed_v1, transformed_v2, thickness, color, vertices, write_offset)
write_offset += extrude_line(transformed_v2, transformed_v3, thickness, color, vertices, write_offset)
write_offset += extrude_line(transformed_v3, transformed_v1, thickness, color, vertices, write_offset)
}
if write_offset > 0 {
draw.prepare_shape(layer, vertices[:write_offset])
}
}
triangle_fan :: proc(
layer: ^draw.Layer,
points: []draw.Vec2,
color: draw.Color,
origin: draw.Vec2 = {},
rotation: f32 = 0,
temp_allocator := context.temp_allocator,
) {
if len(points) < 3 do return
triangle_count := len(points) - 2
vertex_count := triangle_count * 3
vertices := make([]draw.Vertex_2D, vertex_count, temp_allocator)
defer delete(vertices, temp_allocator)
if !draw.needs_transform(origin, rotation) {
for i in 1 ..< len(points) - 1 {
idx := (i - 1) * 3
vertices[idx + 0] = solid_vertex(points[0], color)
vertices[idx + 1] = solid_vertex(points[i], color)
vertices[idx + 2] = solid_vertex(points[i + 1], color)
}
} else {
bounds_min := draw.Vec2{max(f32), max(f32)}
for point in points {
bounds_min.x = min(bounds_min.x, point.x)
bounds_min.y = min(bounds_min.y, point.y)
}
transform := draw.build_pivot_rotation(bounds_min, origin, rotation)
for i in 1 ..< len(points) - 1 {
idx := (i - 1) * 3
vertices[idx + 0] = solid_vertex(draw.apply_transform(transform, points[0] - bounds_min), color)
vertices[idx + 1] = solid_vertex(draw.apply_transform(transform, points[i] - bounds_min), color)
vertices[idx + 2] = solid_vertex(draw.apply_transform(transform, points[i + 1] - bounds_min), color)
}
}
draw.prepare_shape(layer, vertices)
}
triangle_strip :: proc(
layer: ^draw.Layer,
points: []draw.Vec2,
color: draw.Color,
origin: draw.Vec2 = {},
rotation: f32 = 0,
temp_allocator := context.temp_allocator,
) {
if len(points) < 3 do return
triangle_count := len(points) - 2
vertex_count := triangle_count * 3
vertices := make([]draw.Vertex_2D, vertex_count, temp_allocator)
defer delete(vertices, temp_allocator)
if !draw.needs_transform(origin, rotation) {
for i in 0 ..< triangle_count {
idx := i * 3
if i % 2 == 0 {
vertices[idx + 0] = solid_vertex(points[i], color)
vertices[idx + 1] = solid_vertex(points[i + 1], color)
vertices[idx + 2] = solid_vertex(points[i + 2], color)
} else {
vertices[idx + 0] = solid_vertex(points[i + 1], color)
vertices[idx + 1] = solid_vertex(points[i], color)
vertices[idx + 2] = solid_vertex(points[i + 2], color)
}
}
} else {
bounds_min := draw.Vec2{max(f32), max(f32)}
for point in points {
bounds_min.x = min(bounds_min.x, point.x)
bounds_min.y = min(bounds_min.y, point.y)
}
transform := draw.build_pivot_rotation(bounds_min, origin, rotation)
for i in 0 ..< triangle_count {
idx := i * 3
if i % 2 == 0 {
vertices[idx + 0] = solid_vertex(draw.apply_transform(transform, points[i] - bounds_min), color)
vertices[idx + 1] = solid_vertex(draw.apply_transform(transform, points[i + 1] - bounds_min), color)
vertices[idx + 2] = solid_vertex(draw.apply_transform(transform, points[i + 2] - bounds_min), color)
} else {
vertices[idx + 0] = solid_vertex(draw.apply_transform(transform, points[i + 1] - bounds_min), color)
vertices[idx + 1] = solid_vertex(draw.apply_transform(transform, points[i] - bounds_min), color)
vertices[idx + 2] = solid_vertex(draw.apply_transform(transform, points[i + 2] - bounds_min), color)
}
}
}
draw.prepare_shape(layer, vertices)
}
+22 -204
View File
@@ -1,41 +1,24 @@
package draw
import "core:c"
import "core:log"
import "core:strings"
import sdl "vendor:sdl3"
import sdl_ttf "vendor:sdl3/ttf"
Font_Id :: u16
//INTERNAL
Font_Key :: struct {
id: Font_Id,
size: u16,
}
//INTERNAL
Cache_Source :: enum u8 {
Custom,
Clay,
}
//INTERNAL
Cache_Key :: struct {
id: u32,
source: Cache_Source,
}
//INTERNAL
Text_Cache :: struct {
engine: ^sdl_ttf.TextEngine,
font_bytes: [dynamic][]u8,
sdl_fonts: map[Font_Key]^sdl_ttf.Font,
cache: map[Cache_Key]^sdl_ttf.Text,
cache: map[u32]^sdl_ttf.Text,
}
// Fetch SDL TTF font pointer for rendering.
//INTERNAL
// Internal for fetching SDL TTF font pointer for rendering
get_font :: proc(id: Font_Id, size: u16) -> ^sdl_ttf.Font {
assert(int(id) < len(GLOB.text_cache.font_bytes), "Invalid font ID.")
key := Font_Key{id, size}
@@ -82,200 +65,39 @@ register_font :: proc(bytes: []u8) -> (id: Font_Id, ok: bool) #optional_ok {
return Font_Id(len(GLOB.text_cache.font_bytes) - 1), true
}
//INTERNAL
Text :: struct {
sdl_text: ^sdl_ttf.Text,
position: Vec2,
ref: ^sdl_ttf.Text,
position: [2]f32,
color: Color,
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- Text cache lookup -------------
// ---------------------------------------------------------------------------------------------------------------------
// Shared cache lookup/create/update logic used by both the `text` proc and the Clay render path.
// Returns the cached (or newly created) TTF_Text pointer.
//INTERNAL
cache_get_or_update :: proc(key: Cache_Key, c_str: cstring, font: ^sdl_ttf.Font) -> ^sdl_ttf.Text {
existing, found := GLOB.text_cache.cache[key]
if !found {
sdl_text := sdl_ttf.CreateText(GLOB.text_cache.engine, font, c_str, 0)
text :: proc(
id: u32,
txt: cstring,
pos: [2]f32,
font_id: Font_Id,
font_size: u16 = 44,
color: Color = {0, 0, 0, 255},
) -> Text {
sdl_text := GLOB.text_cache.cache[id]
if sdl_text == nil {
sdl_text = sdl_ttf.CreateText(GLOB.text_cache.engine, get_font(font_id, font_size), txt, 0)
if sdl_text == nil {
log.panicf("Failed to create SDL text: %s", sdl.GetError())
}
GLOB.text_cache.cache[key] = sdl_text
return sdl_text
GLOB.text_cache.cache[id] = sdl_text
} else {
if !sdl_ttf.SetTextString(existing, c_str, 0) {
//TODO if IDs are always unique and never change the underlying text
// can get rid of this
if !sdl_ttf.SetTextString(sdl_text, txt, 0) {
log.panicf("Failed to update SDL text string: %s", sdl.GetError())
}
return existing
}
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- Text drawing ------------------
// ---------------------------------------------------------------------------------------------------------------------
// Draw text at a position with optional rotation and origin.
//
// When `id` is nil (the default), the text is created and destroyed each frame — simple and
// leak-free, appropriate for HUDs and moderate UI (up to ~50 text elements per frame).
//
// When `id` is set, the TTF_Text object is cached across frames keyed by the provided u32.
// This avoids per-frame HarfBuzz shaping and allocation, which matters for text-heavy apps
// (editors, terminals, chat). The user is responsible for choosing unique IDs per logical text
// element and calling `clear_text_cache` or `clear_text_cache_entry` when cached entries are
// no longer needed. Custom text IDs occupy a separate namespace from Clay text IDs, so
// collisions between the two are impossible.
//
// `origin` is in pixels from the text block's top-left corner (raylib convention).
// The point whose local coords equal `origin` lands at `pos` in world space.
// `rotation` is in degrees, counter-clockwise.
text :: proc(
layer: ^Layer,
text_string: string,
position: Vec2,
font_id: Font_Id,
font_size: u16 = DFT_FONT_SIZE,
color: Color = DFT_TEXT_COLOR,
origin: Vec2 = {},
rotation: f32 = 0,
id: Maybe(u32) = nil,
temp_allocator := context.temp_allocator,
) {
c_str := strings.clone_to_cstring(text_string, temp_allocator)
defer delete(c_str, temp_allocator)
sdl_text: ^sdl_ttf.Text
cached := false
if cache_id, ok := id.?; ok {
cached = true
sdl_text = cache_get_or_update(Cache_Key{cache_id, .Custom}, c_str, get_font(font_id, font_size))
} else {
sdl_text = sdl_ttf.CreateText(GLOB.text_cache.engine, get_font(font_id, font_size), c_str, 0)
if sdl_text == nil {
log.panicf("Failed to create SDL text: %s", sdl.GetError())
}
}
if needs_transform(origin, rotation) {
dpi_scale := GLOB.dpi_scaling
transform := build_pivot_rotation(position * dpi_scale, origin * dpi_scale, rotation)
prepare_text_transformed(layer, Text{sdl_text, {0, 0}, color}, transform)
} else {
prepare_text(layer, Text{sdl_text, position, color})
}
if !cached {
// Don't destroy now — the draw data (atlas texture, vertices) is still referenced
// by the batch buffers until end() submits to the GPU. Deferred to clear_global().
append(&GLOB.tmp_uncached_text, sdl_text)
}
return Text{sdl_text, pos, color}
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- Public text measurement -------
// ---------------------------------------------------------------------------------------------------------------------
// Measure a string in logical pixels (pre-DPI-scaling) using the same font backend as the renderer.
measure_text :: proc(
text_string: string,
font_id: Font_Id,
font_size: u16 = DFT_FONT_SIZE,
allocator := context.temp_allocator,
) -> Vec2 {
c_str := strings.clone_to_cstring(text_string, allocator)
defer delete(c_str, allocator)
width, height: c.int
if !sdl_ttf.GetStringSize(get_font(font_id, font_size), c_str, 0, &width, &height) {
log.panicf("Failed to measure text: %s", sdl.GetError())
}
return {f32(width) / GLOB.dpi_scaling, f32(height) / GLOB.dpi_scaling}
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- Text anchor helpers -----------
// ---------------------------------------------------------------------------------------------------------------------
center_of_text :: proc(text_string: string, font_id: Font_Id, font_size: u16 = DFT_FONT_SIZE) -> Vec2 {
size := measure_text(text_string, font_id, font_size)
return size * 0.5
}
top_left_of_text :: proc(text_string: string, font_id: Font_Id, font_size: u16 = DFT_FONT_SIZE) -> Vec2 {
return {0, 0}
}
top_of_text :: proc(text_string: string, font_id: Font_Id, font_size: u16 = DFT_FONT_SIZE) -> Vec2 {
size := measure_text(text_string, font_id, font_size)
return {size.x * 0.5, 0}
}
top_right_of_text :: proc(text_string: string, font_id: Font_Id, font_size: u16 = DFT_FONT_SIZE) -> Vec2 {
size := measure_text(text_string, font_id, font_size)
return {size.x, 0}
}
left_of_text :: proc(text_string: string, font_id: Font_Id, font_size: u16 = DFT_FONT_SIZE) -> Vec2 {
size := measure_text(text_string, font_id, font_size)
return {0, size.y * 0.5}
}
right_of_text :: proc(text_string: string, font_id: Font_Id, font_size: u16 = DFT_FONT_SIZE) -> Vec2 {
size := measure_text(text_string, font_id, font_size)
return {size.x, size.y * 0.5}
}
bottom_left_of_text :: proc(text_string: string, font_id: Font_Id, font_size: u16 = DFT_FONT_SIZE) -> Vec2 {
size := measure_text(text_string, font_id, font_size)
return {0, size.y}
}
bottom_of_text :: proc(text_string: string, font_id: Font_Id, font_size: u16 = DFT_FONT_SIZE) -> Vec2 {
size := measure_text(text_string, font_id, font_size)
return {size.x * 0.5, size.y}
}
bottom_right_of_text :: proc(text_string: string, font_id: Font_Id, font_size: u16 = DFT_FONT_SIZE) -> Vec2 {
size := measure_text(text_string, font_id, font_size)
return size
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- Cache management --------------
// ---------------------------------------------------------------------------------------------------------------------
// Destroy all cached text objects (both custom and Clay entries). Call on scene transitions,
// view changes, or periodically in apps that produce many distinct cached text entries over time.
// After calling this, subsequent text draws with an `id` will re-create their cache entries.
clear_text_cache :: proc() {
for _, sdl_text in GLOB.text_cache.cache {
append(&GLOB.pending_text_releases, sdl_text)
}
clear(&GLOB.text_cache.cache)
}
// Destroy a specific cached custom text entry by its u32 id (the same value passed to the
// `text` proc's `id` parameter). This only affects custom text entries — Clay text entries
// are managed internally and are not addressable by the user.
// No-op if the id is not in the cache.
clear_text_cache_entry :: proc(id: u32) {
key := Cache_Key{id, .Custom}
sdl_text, ok := GLOB.text_cache.cache[key]
if ok {
append(&GLOB.pending_text_releases, sdl_text)
delete_key(&GLOB.text_cache.cache, key)
}
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- Internal cache lifecycle ------
// ---------------------------------------------------------------------------------------------------------------------
//INTERNAL
@(require_results)
@(private, require_results)
init_text_cache :: proc(
device: ^sdl.GPUDevice,
allocator := context.allocator,
@@ -299,21 +121,17 @@ init_text_cache :: proc(
text_cache = Text_Cache {
engine = engine,
cache = make(map[Cache_Key]^sdl_ttf.Text, allocator = allocator),
cache = make(map[u32]^sdl_ttf.Text, allocator = allocator),
}
log.debug("Done initializing text cache")
return text_cache, true
}
//INTERNAL
destroy_text_cache :: proc() {
for _, font in GLOB.text_cache.sdl_fonts {
sdl_ttf.CloseFont(font)
}
for _, sdl_text in GLOB.text_cache.cache {
sdl_ttf.DestroyText(sdl_text)
}
delete(GLOB.text_cache.sdl_fonts)
delete(GLOB.text_cache.font_bytes)
delete(GLOB.text_cache.cache)
-403
View File
@@ -1,403 +0,0 @@
package draw
import "core:log"
import "core:mem"
import sdl "vendor:sdl3"
Texture_Id :: distinct u32
INVALID_TEXTURE :: Texture_Id(0) // Slot 0 is reserved/unused
Texture_Kind :: enum u8 {
Static, // Uploaded once, never changes (QR codes, decoded PNGs, icons)
Dynamic, // Updatable via update_texture_region
Stream, // Frequent full re-uploads (video, procedural)
}
Sampler_Preset :: enum u8 {
Linear_Clamp,
Nearest_Clamp,
Nearest_Repeat,
Linear_Repeat,
}
SAMPLER_PRESET_COUNT :: 4
Fit_Mode :: enum u8 {
Stretch, // Fill rect, may distort aspect ratio (default)
Fit, // Preserve aspect, letterbox (may leave margins)
Fill, // Preserve aspect, center-crop (may crop edges)
Tile, // Repeat at native texture size
Center, // 1:1 pixel size, centered, no scaling
}
Texture_Desc :: struct {
width: u32,
height: u32,
depth_or_layers: u32,
type: sdl.GPUTextureType,
format: sdl.GPUTextureFormat,
usage: sdl.GPUTextureUsageFlags,
mip_levels: u32,
kind: Texture_Kind,
}
//INTERNAL
Texture_Slot :: struct {
gpu_texture: ^sdl.GPUTexture,
desc: Texture_Desc,
generation: u32,
}
// State stored in GLOB
// This file references:
// GLOB.device : ^sdl.GPUDevice
// GLOB.texture_slots : [dynamic]Texture_Slot
// GLOB.texture_free_list : [dynamic]u32
// GLOB.pending_texture_releases : [dynamic]Texture_Id
// GLOB.samplers : [SAMPLER_PRESET_COUNT]^sdl.GPUSampler
// ---------------------------------------------------------------------------------------------------------------------
// ----- Registration -------------
// ---------------------------------------------------------------------------------------------------------------------
// Register a texture. Draw owns the GPU resource and releases it on unregister.
// `data` is tightly-packed row-major bytes matching desc.format.
// The caller may free `data` immediately after this proc returns.
@(require_results)
register_texture :: proc(desc: Texture_Desc, data: []u8) -> (id: Texture_Id, ok: bool) {
device := GLOB.device
if device == nil {
log.error("register_texture called before draw.init()")
return INVALID_TEXTURE, false
}
assert(desc.width > 0, "Texture_Desc.width must be > 0")
assert(desc.height > 0, "Texture_Desc.height must be > 0")
assert(desc.depth_or_layers > 0, "Texture_Desc.depth_or_layers must be > 0")
assert(desc.mip_levels > 0, "Texture_Desc.mip_levels must be > 0")
assert(desc.usage != {}, "Texture_Desc.usage must not be empty (e.g. {.SAMPLER})")
// Create the GPU texture
gpu_texture := sdl.CreateGPUTexture(
device,
sdl.GPUTextureCreateInfo {
type = desc.type,
format = desc.format,
usage = desc.usage,
width = desc.width,
height = desc.height,
layer_count_or_depth = desc.depth_or_layers,
num_levels = desc.mip_levels,
sample_count = ._1,
},
)
if gpu_texture == nil {
log.errorf("Failed to create GPU texture (%dx%d): %s", desc.width, desc.height, sdl.GetError())
return INVALID_TEXTURE, false
}
// Upload pixel data via a transfer buffer
if len(data) > 0 {
data_size := u32(len(data))
transfer := sdl.CreateGPUTransferBuffer(
device,
sdl.GPUTransferBufferCreateInfo{usage = .UPLOAD, size = data_size},
)
if transfer == nil {
log.errorf("Failed to create texture transfer buffer: %s", sdl.GetError())
sdl.ReleaseGPUTexture(device, gpu_texture)
return INVALID_TEXTURE, false
}
defer sdl.ReleaseGPUTransferBuffer(device, transfer)
mapped := sdl.MapGPUTransferBuffer(device, transfer, false)
if mapped == nil {
log.errorf("Failed to map texture transfer buffer: %s", sdl.GetError())
sdl.ReleaseGPUTexture(device, gpu_texture)
return INVALID_TEXTURE, false
}
mem.copy(mapped, raw_data(data), int(data_size))
sdl.UnmapGPUTransferBuffer(device, transfer)
cmd_buffer := sdl.AcquireGPUCommandBuffer(device)
if cmd_buffer == nil {
log.errorf("Failed to acquire command buffer for texture upload: %s", sdl.GetError())
sdl.ReleaseGPUTexture(device, gpu_texture)
return INVALID_TEXTURE, false
}
copy_pass := sdl.BeginGPUCopyPass(cmd_buffer)
sdl.UploadToGPUTexture(
copy_pass,
sdl.GPUTextureTransferInfo{transfer_buffer = transfer},
sdl.GPUTextureRegion{texture = gpu_texture, w = desc.width, h = desc.height, d = desc.depth_or_layers},
false,
)
sdl.EndGPUCopyPass(copy_pass)
if !sdl.SubmitGPUCommandBuffer(cmd_buffer) {
log.errorf("Failed to submit texture upload: %s", sdl.GetError())
sdl.ReleaseGPUTexture(device, gpu_texture)
return INVALID_TEXTURE, false
}
}
// Allocate a slot (reuse from free list or append)
slot_index: u32
if len(GLOB.texture_free_list) > 0 {
slot_index = pop(&GLOB.texture_free_list)
GLOB.texture_slots[slot_index] = Texture_Slot {
gpu_texture = gpu_texture,
desc = desc,
generation = GLOB.texture_slots[slot_index].generation + 1,
}
} else {
slot_index = u32(len(GLOB.texture_slots))
append(&GLOB.texture_slots, Texture_Slot{gpu_texture = gpu_texture, desc = desc, generation = 1})
}
return Texture_Id(slot_index), true
}
// Queue a texture for release at the end of the current frame.
// The GPU resource is not freed immediately — see "Deferred release" in the README.
unregister_texture :: proc(id: Texture_Id) {
if id == INVALID_TEXTURE do return
append(&GLOB.pending_texture_releases, id)
}
// Re-upload a sub-region of a Dynamic texture.
update_texture_region :: proc(id: Texture_Id, region: Rectangle, data: []u8) {
if id == INVALID_TEXTURE do return
slot := &GLOB.texture_slots[u32(id)]
if slot.gpu_texture == nil do return
device := GLOB.device
data_size := u32(len(data))
if data_size == 0 do return
transfer := sdl.CreateGPUTransferBuffer(
device,
sdl.GPUTransferBufferCreateInfo{usage = .UPLOAD, size = data_size},
)
if transfer == nil {
log.errorf("Failed to create transfer buffer for texture region update: %s", sdl.GetError())
return
}
defer sdl.ReleaseGPUTransferBuffer(device, transfer)
mapped := sdl.MapGPUTransferBuffer(device, transfer, false)
if mapped == nil {
log.errorf("Failed to map transfer buffer for texture region update: %s", sdl.GetError())
return
}
mem.copy(mapped, raw_data(data), int(data_size))
sdl.UnmapGPUTransferBuffer(device, transfer)
cmd_buffer := sdl.AcquireGPUCommandBuffer(device)
if cmd_buffer == nil {
log.errorf("Failed to acquire command buffer for texture region update: %s", sdl.GetError())
return
}
copy_pass := sdl.BeginGPUCopyPass(cmd_buffer)
sdl.UploadToGPUTexture(
copy_pass,
sdl.GPUTextureTransferInfo{transfer_buffer = transfer},
sdl.GPUTextureRegion {
texture = slot.gpu_texture,
x = u32(region.x),
y = u32(region.y),
w = u32(region.width),
h = u32(region.height),
d = 1,
},
false,
)
sdl.EndGPUCopyPass(copy_pass)
if !sdl.SubmitGPUCommandBuffer(cmd_buffer) {
log.errorf("Failed to submit texture region update: %s", sdl.GetError())
}
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- Helpers -------------
// ---------------------------------------------------------------------------------------------------------------------
// Compute UV rect, recommended sampler, and inner rect for a given fit mode.
// `rect` is the target drawing area; `texture_id` identifies the texture whose
// pixel dimensions are looked up via texture_size().
// For Fit mode, `inner_rect` is smaller than `rect` (centered). For all other modes, `inner_rect == rect`.
fit_params :: proc(
fit: Fit_Mode,
rect: Rectangle,
texture_id: Texture_Id,
) -> (
uv_rect: Rectangle,
sampler: Sampler_Preset,
inner_rect: Rectangle,
) {
size := texture_size(texture_id)
texture_width := f32(size.x)
texture_height := f32(size.y)
rect_width := rect.width
rect_height := rect.height
inner_rect = rect
if texture_width == 0 || texture_height == 0 || rect_width == 0 || rect_height == 0 {
return {0, 0, 1, 1}, .Linear_Clamp, inner_rect
}
texture_aspect := texture_width / texture_height
rect_aspect := rect_width / rect_height
switch fit {
case .Stretch: return {0, 0, 1, 1}, .Linear_Clamp, inner_rect
case .Fill: if texture_aspect > rect_aspect {
// Texture wider than rect — crop sides
scale := rect_aspect / texture_aspect
margin := (1 - scale) * 0.5
return {margin, 0, 1 - margin, 1}, .Linear_Clamp, inner_rect
} else {
// Texture taller than rect — crop top/bottom
scale := texture_aspect / rect_aspect
margin := (1 - scale) * 0.5
return {0, margin, 1, 1 - margin}, .Linear_Clamp, inner_rect
}
case .Fit:
// Preserve aspect, fit inside rect. Returns a shrunken inner_rect.
if texture_aspect > rect_aspect {
// Image wider — letterbox top/bottom
fit_height := rect_width / texture_aspect
padding := (rect_height - fit_height) * 0.5
inner_rect = Rectangle{rect.x, rect.y + padding, rect_width, fit_height}
} else {
// Image taller — letterbox left/right
fit_width := rect_height * texture_aspect
padding := (rect_width - fit_width) * 0.5
inner_rect = Rectangle{rect.x + padding, rect.y, fit_width, rect_height}
}
return {0, 0, 1, 1}, .Linear_Clamp, inner_rect
case .Tile:
uv_width := rect_width / texture_width
uv_height := rect_height / texture_height
return {0, 0, uv_width, uv_height}, .Linear_Repeat, inner_rect
case .Center:
u_half := rect_width / (2 * texture_width)
v_half := rect_height / (2 * texture_height)
return {0.5 - u_half, 0.5 - v_half, 0.5 + u_half, 0.5 + v_half}, .Nearest_Clamp, inner_rect
}
return {0, 0, 1, 1}, .Linear_Clamp, inner_rect
}
texture_size :: proc(id: Texture_Id) -> [2]u32 {
if id == INVALID_TEXTURE do return {0, 0}
slot := &GLOB.texture_slots[u32(id)]
return {slot.desc.width, slot.desc.height}
}
texture_format :: proc(id: Texture_Id) -> sdl.GPUTextureFormat {
if id == INVALID_TEXTURE do return .INVALID
return GLOB.texture_slots[u32(id)].desc.format
}
texture_kind :: proc(id: Texture_Id) -> Texture_Kind {
if id == INVALID_TEXTURE do return .Static
return GLOB.texture_slots[u32(id)].desc.kind
}
// Get the raw GPU texture pointer for binding during draw.
//INTERNAL
texture_gpu_handle :: proc(id: Texture_Id) -> ^sdl.GPUTexture {
if id == INVALID_TEXTURE do return nil
idx := u32(id)
if idx >= u32(len(GLOB.texture_slots)) do return nil
return GLOB.texture_slots[idx].gpu_texture
}
// Deferred release (called from end / clear_global).
//INTERNAL
process_pending_texture_releases :: proc() {
device := GLOB.device
for id in GLOB.pending_texture_releases {
idx := u32(id)
if idx >= u32(len(GLOB.texture_slots)) do continue
slot := &GLOB.texture_slots[idx]
if slot.gpu_texture != nil {
sdl.ReleaseGPUTexture(device, slot.gpu_texture)
slot.gpu_texture = nil
}
slot.generation += 1
append(&GLOB.texture_free_list, idx)
}
clear(&GLOB.pending_texture_releases)
}
//INTERNAL
get_sampler :: proc(preset: Sampler_Preset) -> ^sdl.GPUSampler {
idx := int(preset)
if GLOB.samplers[idx] != nil do return GLOB.samplers[idx]
// Lazily create
min_filter, mag_filter: sdl.GPUFilter
address_mode: sdl.GPUSamplerAddressMode
switch preset {
case .Nearest_Clamp:
min_filter = .NEAREST; mag_filter = .NEAREST; address_mode = .CLAMP_TO_EDGE
case .Linear_Clamp:
min_filter = .LINEAR; mag_filter = .LINEAR; address_mode = .CLAMP_TO_EDGE
case .Nearest_Repeat:
min_filter = .NEAREST; mag_filter = .NEAREST; address_mode = .REPEAT
case .Linear_Repeat:
min_filter = .LINEAR; mag_filter = .LINEAR; address_mode = .REPEAT
}
sampler := sdl.CreateGPUSampler(
GLOB.device,
sdl.GPUSamplerCreateInfo {
min_filter = min_filter,
mag_filter = mag_filter,
mipmap_mode = .LINEAR,
address_mode_u = address_mode,
address_mode_v = address_mode,
address_mode_w = address_mode,
},
)
if sampler == nil {
log.errorf("Failed to create sampler preset %v: %s", preset, sdl.GetError())
return GLOB.core_2d.sampler // fallback to existing default sampler
}
GLOB.samplers[idx] = sampler
return sampler
}
// Destroy all sampler pool entries. Called from destroy().
//INTERNAL
destroy_sampler_pool :: proc() {
device := GLOB.device
for &s in GLOB.samplers {
if s != nil {
sdl.ReleaseGPUSampler(device, s)
s = nil
}
}
}
// Destroy all registered textures. Called from destroy().
//INTERNAL
destroy_all_textures :: proc() {
device := GLOB.device
for &slot in GLOB.texture_slots {
if slot.gpu_texture != nil {
sdl.ReleaseGPUTexture(device, slot.gpu_texture)
slot.gpu_texture = nil
}
}
delete(GLOB.texture_slots)
delete(GLOB.texture_free_list)
delete(GLOB.pending_texture_releases)
}
+44 -119
View File
@@ -120,52 +120,10 @@ 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,
}
@@ -174,14 +132,6 @@ unlock :: proc {
spinlock_unlock,
}
guard :: proc {
spinlock_guard,
}
try_guard :: proc {
spinlock_try_guard,
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- Tests ------------------------
// ---------------------------------------------------------------------------------------------------------------------
@@ -189,10 +139,10 @@ import "core:sync"
import "core:testing"
import "core:thread"
// 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) {
// 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.
NUM_THREADS :: 8
ITERATIONS_PER_THREAD :: 10_000
@@ -234,10 +184,10 @@ test_concurrent_atomic_add_no_lost_updates :: proc(t: ^testing.T) {
testing.expect_value(t, shared_value, expected)
}
// 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) {
// Start with a known value, multiple threads subtract.
// If any updates are lost due to race conditions, the final result will be wrong.
NUM_THREADS :: 8
ITERATIONS_PER_THREAD :: 10_000
@@ -278,11 +228,11 @@ test_concurrent_atomic_sub_no_lost_updates :: proc(t: ^testing.T) {
testing.expect_value(t, shared_value, 0.0)
}
// 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) {
// 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.
NUM_THREADS :: 8
ITERATIONS_PER_THREAD :: 10_000
@@ -324,10 +274,10 @@ test_concurrent_atomic_mul_div_round_trip :: proc(t: ^testing.T) {
testing.expect_value(t, shared_value, 1000.0)
}
// 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) {
// Verify the f32 type dispatch works correctly under contention.
// Same approach as the f64 add test but with f32.
NUM_THREADS :: 8
ITERATIONS_PER_THREAD :: 10_000
@@ -369,17 +319,17 @@ test_atomic_add_with_f32 :: proc(t: ^testing.T) {
testing.expect_value(t, shared_value, expected)
}
// Tests that the memory order passed to atomic_float_op's CAS success condition
// provides full ordering guarantees for the entire float operation.
//
// Both sides use atomic_add_float (not raw intrinsics) to verify:
// - Release on CAS success publishes prior non-atomic writes
// - Acquire on CAS success makes those writes visible to the reader
//
// 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) {
// Tests that the memory order passed to atomic_float_op's CAS success condition
// provides full ordering guarantees for the entire float operation.
//
// Both sides use atomic_add_float (not raw intrinsics) to verify:
// - Release on CAS success publishes prior non-atomic writes
// - Acquire on CAS success makes those writes visible to the reader
//
// 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.
NUM_READERS :: 4
Shared_State :: struct {
@@ -476,20 +426,17 @@ test_atomic_release_acquire_publish_visibility :: proc(t: ^testing.T) {
}
}
// 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
// - `concurrent_holders` never exceeds 1
//
// 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) {
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.
//
// If mutual exclusion holds:
// - `counter` ends at exactly NUM_THREADS * ITERATIONS_PER_THREAD
// - `concurrent_holders` never exceeds 1
//
// A multi-step RMW (read → relax → write) widens the critical section so
// any failure to exclude is virtually guaranteed to corrupt the counter.
NUM_THREADS :: 8
ITERATIONS_PER_THREAD :: 50_000
@@ -514,9 +461,21 @@ test_spinlock_mutual_exclusion :: proc(t: ^testing.T) {
barrier: sync.Barrier
sync.barrier_init(&barrier, NUM_THREADS)
// 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) {
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 ---
// Atomically bump the holder count so we can detect overlapping holders.
holders := intrinsics.atomic_add_explicit(&s.concurrent_holders, 1, .Relaxed)
@@ -535,44 +494,10 @@ test_spinlock_mutual_exclusion :: proc(t: ^testing.T) {
s.counter = val + 1
intrinsics.atomic_sub_explicit(&s.concurrent_holders, 1, .Relaxed)
}
thread_proc :: proc(th: ^thread.Thread) {
ctx := cast(^Thread_Data)th.data
s := ctx.shared
// --- critical section end ---
// 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()
}
}
}
}
+28 -34
View File
@@ -2,7 +2,6 @@ package many_bits
import "base:builtin"
import "base:intrinsics"
import "base:runtime"
import "core:fmt"
import "core:slice"
@@ -26,20 +25,15 @@ Bits :: struct {
length: int, // Total number of bits being stored
}
destroy :: proc(bits: Bits, allocator := context.allocator) -> runtime.Allocator_Error {
return delete_slice(bits.int_array, allocator)
delete :: proc(bits: Bits, allocator := context.allocator) {
delete_slice(bits.int_array, allocator)
}
create :: proc(
#any_int length: int,
allocator := context.allocator,
) -> (
bits: Bits,
err: runtime.Allocator_Error,
) #optional_allocator_error {
bits.int_array, err = make_slice([]Int_Bits, ((length - 1) >> INDEX_SHIFT) + 1, allocator)
bits.length = length
return bits, err
make :: proc(#any_int length: int, allocator := context.allocator) -> Bits {
return Bits {
int_array = make_slice([]Int_Bits, ((length - 1) >> INDEX_SHIFT) + 1, allocator),
length = length,
}
}
// Sets all bits to 0 (false)
@@ -513,8 +507,8 @@ import "core:testing"
@(test)
test_set :: proc(t: ^testing.T) {
bits := create(128)
defer destroy(bits)
bits := make(128)
defer delete(bits)
set(bits, 0, true)
testing.expect_value(t, bits.int_array[0], Int_Bits{0})
@@ -530,8 +524,8 @@ test_set :: proc(t: ^testing.T) {
@(test)
test_get :: proc(t: ^testing.T) {
bits := create(128)
defer destroy(bits)
bits := make(128)
defer delete(bits)
// Default is false
testing.expect(t, !get(bits, 0))
@@ -566,8 +560,8 @@ test_get :: proc(t: ^testing.T) {
@(test)
test_set_true_set_false :: proc(t: ^testing.T) {
bits := create(128)
defer destroy(bits)
bits := make(128)
defer delete(bits)
// set_true within first uint
set_true(bits, 0)
@@ -611,8 +605,8 @@ all_true_test :: proc(t: ^testing.T) {
uint_max := UINT_MAX
all_ones := transmute(Int_Bits)uint_max
bits := create(132)
defer destroy(bits)
bits := make(132)
defer delete(bits)
bits.int_array[0] = all_ones
bits.int_array[1] = all_ones
@@ -622,8 +616,8 @@ all_true_test :: proc(t: ^testing.T) {
bits.int_array[2] = {0, 1, 2}
testing.expect(t, !all_true(bits))
bits2 := create(1)
defer destroy(bits2)
bits2 := make(1)
defer delete(bits2)
bits2.int_array[0] = {0}
testing.expect(t, all_true(bits2))
@@ -634,8 +628,8 @@ test_range_true :: proc(t: ^testing.T) {
uint_max := UINT_MAX
all_ones := transmute(Int_Bits)uint_max
bits := create(192)
defer destroy(bits)
bits := make(192)
defer delete(bits)
// Empty range is vacuously true
testing.expect(t, range_true(bits, 0, 0))
@@ -682,7 +676,7 @@ test_range_true :: proc(t: ^testing.T) {
@(test)
nearest_true_handles_same_word_and_boundaries :: proc(t: ^testing.T) {
bits := create(128, context.temp_allocator)
bits := make(128, context.temp_allocator)
set_true(bits, 0)
set_true(bits, 10)
@@ -716,7 +710,7 @@ nearest_true_handles_same_word_and_boundaries :: proc(t: ^testing.T) {
@(test)
nearest_false_handles_same_word_and_boundaries :: proc(t: ^testing.T) {
bits := create(128, context.temp_allocator)
bits := make(128, context.temp_allocator)
// Start with all bits true, then clear a few to false.
for i := 0; i < bits.length; i += 1 {
@@ -755,7 +749,7 @@ nearest_false_handles_same_word_and_boundaries :: proc(t: ^testing.T) {
@(test)
nearest_false_scans_across_words_and_returns_false_when_all_true :: proc(t: ^testing.T) {
bits := create(192, context.temp_allocator)
bits := make(192, context.temp_allocator)
// Start with all bits true, then clear a couple far apart.
for i := 0; i < bits.length; i += 1 {
@@ -779,7 +773,7 @@ nearest_false_scans_across_words_and_returns_false_when_all_true :: proc(t: ^tes
@(test)
nearest_true_scans_across_words_and_returns_false_when_empty :: proc(t: ^testing.T) {
bits := create(192, context.temp_allocator)
bits := make(192, context.temp_allocator)
set_true(bits, 5)
set_true(bits, 130)
@@ -796,7 +790,7 @@ nearest_true_scans_across_words_and_returns_false_when_empty :: proc(t: ^testing
@(test)
nearest_false_handles_last_word_partial_length :: proc(t: ^testing.T) {
bits := create(130, context.temp_allocator)
bits := make(130, context.temp_allocator)
// Start with all bits true, then clear the first and last valid bits.
for i := 0; i < bits.length; i += 1 {
@@ -817,7 +811,7 @@ nearest_false_handles_last_word_partial_length :: proc(t: ^testing.T) {
@(test)
nearest_true_handles_last_word_partial_length :: proc(t: ^testing.T) {
bits := create(130, context.temp_allocator)
bits := make(130, context.temp_allocator)
set_true(bits, 0)
set_true(bits, 129)
@@ -834,7 +828,7 @@ nearest_true_handles_last_word_partial_length :: proc(t: ^testing.T) {
@(test)
iterator_basic_mixed_bits :: proc(t: ^testing.T) {
// Use non-word-aligned length to test partial last word handling
bits := create(100, context.temp_allocator)
bits := make(100, context.temp_allocator)
// Set specific bits: 0, 3, 64, 99 (last valid index)
set_true(bits, 0)
@@ -909,7 +903,7 @@ iterator_basic_mixed_bits :: proc(t: ^testing.T) {
@(test)
iterator_all_false_bits :: proc(t: ^testing.T) {
// Use non-word-aligned length
bits := create(100, context.temp_allocator)
bits := make(100, context.temp_allocator)
// All bits default to false, no need to set anything
// Test iterate - should return all 100 bits as false
@@ -950,7 +944,7 @@ iterator_all_false_bits :: proc(t: ^testing.T) {
@(test)
iterator_all_true_bits :: proc(t: ^testing.T) {
// Use non-word-aligned length
bits := create(100, context.temp_allocator)
bits := make(100, context.temp_allocator)
// Set all bits to true
for i := 0; i < bits.length; i += 1 {
set_true(bits, i)
-44
View File
@@ -1,8 +1,6 @@
package meta
import "core:fmt"
import "core:log"
import "core:mem"
import "core:os"
Command :: struct {
@@ -22,48 +20,6 @@ COMMANDS :: []Command {
}
main :: proc() {
//----- General setup ----------------------------------
when ODIN_DEBUG {
// Temp
track_temp: mem.Tracking_Allocator
mem.tracking_allocator_init(&track_temp, context.temp_allocator)
context.temp_allocator = mem.tracking_allocator(&track_temp)
// Default
track: mem.Tracking_Allocator
mem.tracking_allocator_init(&track, context.allocator)
context.allocator = mem.tracking_allocator(&track)
// Log a warning about any memory that was not freed by the end of the program.
// This could be fine for some global state or it could be a memory leak.
defer {
// Temp allocator
if len(track_temp.bad_free_array) > 0 {
fmt.eprintf("=== %v incorrect frees - temp allocator: ===\n", len(track_temp.bad_free_array))
for entry in track_temp.bad_free_array {
fmt.eprintf("- %p @ %v\n", entry.memory, entry.location)
}
mem.tracking_allocator_destroy(&track_temp)
}
// Default allocator
if len(track.allocation_map) > 0 {
fmt.eprintf("=== %v allocations not freed - main allocator: ===\n", len(track.allocation_map))
for _, entry in track.allocation_map {
fmt.eprintf("- %v bytes @ %v\n", entry.size, entry.location)
}
}
if len(track.bad_free_array) > 0 {
fmt.eprintf("=== %v incorrect frees - main allocator: ===\n", len(track.bad_free_array))
for entry in track.bad_free_array {
fmt.eprintf("- %p @ %v\n", entry.memory, entry.location)
}
}
mem.tracking_allocator_destroy(&track)
}
// Logger
context.logger = log.create_console_logger()
defer log.destroy_console_logger(context.logger)
}
args := os.args[1:]
if len(args) == 0 {
+19 -22
View File
@@ -4,8 +4,7 @@
package phased_executor
import "base:intrinsics"
import "base:runtime"
import que "core:container/queue"
import q "core:container/queue"
import "core:prof/spall"
import "core:sync"
import "core:thread"
@@ -19,7 +18,7 @@ DEFT_SPIN_LIMIT :: 2_500_000
Harness :: struct($T: typeid) where intrinsics.type_has_nil(T) {
mutex: sync.Mutex,
condition: sync.Cond,
cmd_queue: que.Queue(T),
cmd_queue: q.Queue(T),
spin: bool,
lock: levsync.Spinlock,
_pad: [64 - size_of(uint)]u8, // We want join_count to have its own cache line
@@ -43,13 +42,13 @@ Executor :: struct($T: typeid) where intrinsics.type_has_nil(T) {
}
//TODO: Provide a way to set some aspects of context for the executor threads. Namely a logger.
init :: proc(
init_executor :: proc(
executor: ^Executor($T),
#any_int num_threads: int,
$on_command_received: proc(command: T),
#any_int spin_limit: uint = DEFT_SPIN_LIMIT,
allocator := context.allocator,
) -> runtime.Allocator_Error {
) {
was_initialized, _ := intrinsics.atomic_compare_exchange_strong_explicit(
&executor.initialized,
false,
@@ -61,9 +60,9 @@ init :: proc(
slave_task := build_task(on_command_received)
executor.spin_limit = spin_limit
executor.harnesses = make([]Harness(T), num_threads, allocator) or_return
executor.harnesses = make([]Harness(T), num_threads, allocator)
for &harness in executor.harnesses {
que.init(&harness.cmd_queue, allocator = allocator) or_return
q.init(&harness.cmd_queue, allocator = allocator)
harness.spin = true
}
@@ -73,11 +72,11 @@ init :: proc(
}
thread.pool_start(&executor.thread_pool)
return nil
return
}
// Cleanly shuts down all executor tasks then destroys the executor
destroy :: proc(executor: ^Executor($T), allocator := context.allocator) -> runtime.Allocator_Error {
destroy_executor :: proc(executor: ^Executor($T), allocator := context.allocator) {
was_initialized, _ := intrinsics.atomic_compare_exchange_strong_explicit(
&executor.initialized,
true,
@@ -91,7 +90,7 @@ destroy :: proc(executor: ^Executor($T), allocator := context.allocator) -> runt
for &harness in executor.harnesses {
for {
if levsync.try_lock(&harness.lock) {
que.push_back(&harness.cmd_queue, nil)
q.push_back(&harness.cmd_queue, nil)
if !harness.spin {
sync.mutex_lock(&harness.mutex)
sync.cond_signal(&harness.condition)
@@ -106,11 +105,9 @@ destroy :: proc(executor: ^Executor($T), allocator := context.allocator) -> runt
thread.pool_join(&executor.thread_pool)
thread.pool_destroy(&executor.thread_pool)
for &harness in executor.harnesses {
que.destroy(&harness.cmd_queue)
q.destroy(&harness.cmd_queue)
}
delete(executor.harnesses, allocator) or_return
return nil
delete(executor.harnesses, allocator)
}
build_task :: proc(
@@ -134,10 +131,10 @@ build_task :: proc(
spin_count: uint = 0
spin_loop: for {
if levsync.try_lock(&harness.lock) {
if que.len(harness.cmd_queue) > 0 {
if q.len(harness.cmd_queue) > 0 {
// Execute command
command := que.pop_front(&harness.cmd_queue)
command := q.pop_front(&harness.cmd_queue)
levsync.unlock(&harness.lock)
if command == nil do return
on_command_received(command)
@@ -166,7 +163,7 @@ build_task :: proc(
defer intrinsics.cpu_relax()
if levsync.try_lock(&harness.lock) {
defer levsync.unlock(&harness.lock)
if que.len(harness.cmd_queue) > 0 {
if q.len(harness.cmd_queue) > 0 {
harness.spin = true
break cond_loop
} else {
@@ -193,9 +190,9 @@ exec_command :: proc(executor: ^Executor($T), command: T) {
}
harness := &executor.harnesses[executor.harness_index]
if levsync.try_lock(&harness.lock) {
if que.len(harness.cmd_queue) <= executor.cmd_queue_floor {
que.push_back(&harness.cmd_queue, command)
executor.cmd_queue_floor = que.len(harness.cmd_queue)
if q.len(harness.cmd_queue) <= executor.cmd_queue_floor {
q.push_back(&harness.cmd_queue, command)
executor.cmd_queue_floor = q.len(harness.cmd_queue)
slave_sleeping := !harness.spin
// Must release lock before signalling to avoid race from slave spurious wakeup
levsync.unlock(&harness.lock)
@@ -261,7 +258,7 @@ stress_test_executor :: proc(t: ^testing.T) {
defer free(exec_counts)
executor: Executor(Stress_Cmd)
init(&executor, STRESS_NUM_THREADS, stress_handler, spin_limit = 500)
init_executor(&executor, STRESS_NUM_THREADS, stress_handler, spin_limit = 500)
for round in 0 ..< STRESS_NUM_ROUNDS {
base := round * STRESS_CMDS_PER_ROUND
@@ -284,6 +281,6 @@ stress_test_executor :: proc(t: ^testing.T) {
// Explicitly destroy to verify clean shutdown.
// If destroy_executor returns, all threads received the nil sentinel and exited,
// and thread.pool_join completed without deadlock.
destroy(&executor)
destroy_executor(&executor)
testing.expect(t, !executor.initialized, "Executor still marked initialized after destroy")
}
-280
View File
@@ -1,280 +0,0 @@
package examples
import "core:fmt"
import "core:log"
import "core:mem"
import "core:os"
import qr ".."
main :: proc() {
//----- General setup ----------------------------------
// Temp
track_temp: mem.Tracking_Allocator
mem.tracking_allocator_init(&track_temp, context.temp_allocator)
context.temp_allocator = mem.tracking_allocator(&track_temp)
// Default
track: mem.Tracking_Allocator
mem.tracking_allocator_init(&track, context.allocator)
context.allocator = mem.tracking_allocator(&track)
// Log a warning about any memory that was not freed by the end of the program.
// This could be fine for some global state or it could be a memory leak.
defer {
// Temp allocator
if len(track_temp.bad_free_array) > 0 {
fmt.eprintf("=== %v incorrect frees - temp allocator: ===\n", len(track_temp.bad_free_array))
for entry in track_temp.bad_free_array {
fmt.eprintf("- %p @ %v\n", entry.memory, entry.location)
}
mem.tracking_allocator_destroy(&track_temp)
}
// Default allocator
if len(track.allocation_map) > 0 {
fmt.eprintf("=== %v allocations not freed - main allocator: ===\n", len(track.allocation_map))
for _, entry in track.allocation_map {
fmt.eprintf("- %v bytes @ %v\n", entry.size, entry.location)
}
}
if len(track.bad_free_array) > 0 {
fmt.eprintf("=== %v incorrect frees - main allocator: ===\n", len(track.bad_free_array))
for entry in track.bad_free_array {
fmt.eprintf("- %p @ %v\n", entry.memory, entry.location)
}
}
mem.tracking_allocator_destroy(&track)
}
// Logger
context.logger = log.create_console_logger()
defer log.destroy_console_logger(context.logger)
args := os.args
if len(args) < 2 {
fmt.eprintln("Usage: examples <example_name>")
fmt.eprintln("Available examples: basic, variety, segment, mask")
os.exit(1)
}
switch args[1] {
case "basic": basic()
case "variety": variety()
case "segment": segment()
case "mask": mask()
case:
fmt.eprintf("Unknown example: %v\n", args[1])
fmt.eprintln("Available examples: basic, variety, segment, mask")
os.exit(1)
}
}
// Creates a single QR Code, then prints it to the console.
basic :: proc() {
text :: "Hello, world!"
ecl :: qr.Ecc.Low
qrcode: [qr.BUFFER_LEN_MAX]u8
ok := qr.encode_auto(text, qrcode[:], ecl)
if ok do print_qr(qrcode[:])
}
// Creates a variety of QR Codes that exercise different features of the library.
variety :: proc() {
qrcode: [qr.BUFFER_LEN_MAX]u8
{ // Numeric mode encoding (3.33 bits per digit)
ok := qr.encode_auto("314159265358979323846264338327950288419716939937510", qrcode[:], qr.Ecc.Medium)
if ok do print_qr(qrcode[:])
}
{ // Alphanumeric mode encoding (5.5 bits per character)
ok := qr.encode_auto("DOLLAR-AMOUNT:$39.87 PERCENTAGE:100.00% OPERATIONS:+-*/", qrcode[:], qr.Ecc.High)
if ok do print_qr(qrcode[:])
}
{ // Unicode text as UTF-8
ok := qr.encode_auto(
"\xE3\x81\x93\xE3\x82\x93\xE3\x81\xAB\xE3\x81\xA1wa\xE3\x80\x81" +
"\xE4\xB8\x96\xE7\x95\x8C\xEF\xBC\x81\x20\xCE\xB1\xCE\xB2\xCE\xB3\xCE\xB4",
qrcode[:],
qr.Ecc.Quartile,
)
if ok do print_qr(qrcode[:])
}
{ // Moderately large QR Code using longer text (from Lewis Carroll's Alice in Wonderland)
ok := qr.encode_auto(
"Alice was beginning to get very tired of sitting by her sister on the bank, " +
"and of having nothing to do: once or twice she had peeped into the book her sister was reading, " +
"but it had no pictures or conversations in it, 'and what is the use of a book,' thought Alice " +
"'without pictures or conversations?' So she was considering in her own mind (as well as she could, " +
"for the hot day made her feel very sleepy and stupid), whether the pleasure of making a " +
"daisy-chain would be worth the trouble of getting up and picking the daisies, when suddenly " +
"a White Rabbit with pink eyes ran close by her.",
qrcode[:],
qr.Ecc.High,
)
if ok do print_qr(qrcode[:])
}
}
// Creates QR Codes with manually specified segments for better compactness.
segment :: proc() {
qrcode: [qr.BUFFER_LEN_MAX]u8
{ // Illustration "silver"
silver0 :: "THE SQUARE ROOT OF 2 IS 1."
silver1 :: "41421356237309504880168872420969807856967187537694807317667973799"
// Encode as single text (auto mode selection)
{
concat :: silver0 + silver1
ok := qr.encode_auto(concat, qrcode[:], qr.Ecc.Low)
if ok do print_qr(qrcode[:])
}
// Encode as two manual segments (alphanumeric + numeric) for better compactness
{
seg_buf0: [qr.BUFFER_LEN_MAX]u8
seg_buf1: [qr.BUFFER_LEN_MAX]u8
segs := [2]qr.Segment{qr.make_alphanumeric(silver0, seg_buf0[:]), qr.make_numeric(silver1, seg_buf1[:])}
ok := qr.encode_auto(segs[:], qr.Ecc.Low, qrcode[:])
if ok do print_qr(qrcode[:])
}
}
{ // Illustration "golden"
golden0 :: "Golden ratio \xCF\x86 = 1."
golden1 :: "6180339887498948482045868343656381177203091798057628621354486227052604628189024497072072041893911374"
golden2 :: "......"
// Encode as single text (auto mode selection)
{
concat :: golden0 + golden1 + golden2
ok := qr.encode_auto(concat, qrcode[:], qr.Ecc.Low)
if ok do print_qr(qrcode[:])
}
// Encode as three manual segments (byte + numeric + alphanumeric) for better compactness
{
golden0_str: string = golden0
golden0_bytes := transmute([]u8)golden0_str
seg_buf0: [qr.BUFFER_LEN_MAX]u8
seg_buf1: [qr.BUFFER_LEN_MAX]u8
seg_buf2: [qr.BUFFER_LEN_MAX]u8
segs := [3]qr.Segment {
qr.make_bytes(golden0_bytes, seg_buf0[:]),
qr.make_numeric(golden1, seg_buf1[:]),
qr.make_alphanumeric(golden2, seg_buf2[:]),
}
ok := qr.encode_auto(segs[:], qr.Ecc.Low, qrcode[:])
if ok do print_qr(qrcode[:])
}
}
{ // Illustration "Madoka": kanji, kana, Cyrillic, full-width Latin, Greek characters
// Encode as text (auto mode — byte mode)
{
madoka ::
"\xE3\x80\x8C\xE9\xAD\x94\xE6\xB3\x95\xE5" +
"\xB0\x91\xE5\xA5\xB3\xE3\x81\xBE\xE3\x81" +
"\xA9\xE3\x81\x8B\xE2\x98\x86\xE3\x83\x9E" +
"\xE3\x82\xAE\xE3\x82\xAB\xE3\x80\x8D\xE3" +
"\x81\xA3\xE3\x81\xA6\xE3\x80\x81\xE3\x80" +
"\x80\xD0\x98\xD0\x90\xD0\x98\xE3\x80\x80" +
"\xEF\xBD\x84\xEF\xBD\x85\xEF\xBD\x93\xEF" +
"\xBD\x95\xE3\x80\x80\xCE\xBA\xCE\xB1\xEF" +
"\xBC\x9F"
ok := qr.encode_auto(madoka, qrcode[:], qr.Ecc.Low)
if ok do print_qr(qrcode[:])
}
// Encode with manual kanji mode (13 bits per character)
{
//odinfmt: disable
kanji_chars :: [29]int{
0x0035, 0x1002, 0x0FC0, 0x0AED, 0x0AD7,
0x015C, 0x0147, 0x0129, 0x0059, 0x01BD,
0x018D, 0x018A, 0x0036, 0x0141, 0x0144,
0x0001, 0x0000, 0x0249, 0x0240, 0x0249,
0x0000, 0x0104, 0x0105, 0x0113, 0x0115,
0x0000, 0x0208, 0x01FF, 0x0008,
}
//odinfmt: enable
seg_buf: [qr.BUFFER_LEN_MAX]u8
for &b in seg_buf {
b = 0
}
seg: qr.Segment
seg.mode = .Kanji
seg.num_chars = len(kanji_chars)
seg.bit_length = 0
for ch in kanji_chars {
for j := 12; j >= 0; j -= 1 {
seg_buf[seg.bit_length >> 3] |= u8(((ch >> uint(j)) & 1)) << uint(7 - (seg.bit_length & 7))
seg.bit_length += 1
}
}
seg.data = seg_buf[:(seg.bit_length + 7) / 8]
segs := [1]qr.Segment{seg}
ok := qr.encode_auto(segs[:], qr.Ecc.Low, qrcode[:])
if ok do print_qr(qrcode[:])
}
}
}
// Creates QR Codes with the same size and contents but different mask patterns.
mask :: proc() {
qrcode: [qr.BUFFER_LEN_MAX]u8
{ // Project Nayuki URL
ok: bool
ok = qr.encode_auto("https://www.nayuki.io/", qrcode[:], qr.Ecc.High)
if ok do print_qr(qrcode[:])
ok = qr.encode_auto("https://www.nayuki.io/", qrcode[:], qr.Ecc.High, mask = qr.Mask.M3)
if ok do print_qr(qrcode[:])
}
{ // Chinese text as UTF-8
text ::
"\xE7\xB6\xAD\xE5\x9F\xBA\xE7\x99\xBE\xE7\xA7\x91\xEF\xBC\x88\x57\x69\x6B\x69\x70" +
"\x65\x64\x69\x61\xEF\xBC\x8C\xE8\x81\x86\xE8\x81\xBD\x69\x2F\xCB\x8C\x77\xC9\xAA" +
"\x6B\xE1\xB5\xBB\xCB\x88\x70\x69\xCB\x90\x64\x69\x2E\xC9\x99\x2F\xEF\xBC\x89\xE6" +
"\x98\xAF\xE4\xB8\x80\xE5\x80\x8B\xE8\x87\xAA\xE7\x94\xB1\xE5\x85\xA7\xE5\xAE\xB9" +
"\xE3\x80\x81\xE5\x85\xAC\xE9\x96\x8B\xE7\xB7\xA8\xE8\xBC\xAF\xE4\xB8\x94\xE5\xA4" +
"\x9A\xE8\xAA\x9E\xE8\xA8\x80\xE7\x9A\x84\xE7\xB6\xB2\xE8\xB7\xAF\xE7\x99\xBE\xE7" +
"\xA7\x91\xE5\x85\xA8\xE6\x9B\xB8\xE5\x8D\x94\xE4\xBD\x9C\xE8\xA8\x88\xE7\x95\xAB"
ok: bool
ok = qr.encode_auto(text, qrcode[:], qr.Ecc.Medium, mask = qr.Mask.M0)
if ok do print_qr(qrcode[:])
ok = qr.encode_auto(text, qrcode[:], qr.Ecc.Medium, mask = qr.Mask.M1)
if ok do print_qr(qrcode[:])
ok = qr.encode_auto(text, qrcode[:], qr.Ecc.Medium, mask = qr.Mask.M5)
if ok do print_qr(qrcode[:])
ok = qr.encode_auto(text, qrcode[:], qr.Ecc.Medium, mask = qr.Mask.M7)
if ok do print_qr(qrcode[:])
}
}
// Prints the given QR Code to the console.
print_qr :: proc(qrcode: []u8) {
size := qr.get_size(qrcode)
border :: 4
for y in -border ..< size + border {
for x in -border ..< size + border {
fmt.print("##" if qr.get_module(qrcode, x, y) else " ")
}
fmt.println()
}
fmt.println()
}
-2845
View File
File diff suppressed because it is too large Load Diff
-42
View File
@@ -2,8 +2,6 @@ package quantity
import "base:intrinsics"
LITERS_PER_GALLON :: 3.785411784
//----- Liters ----------------------------------
Liters :: struct($V: typeid) where intrinsics.type_is_numeric(V) {
v: V,
@@ -16,13 +14,6 @@ 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,
@@ -35,34 +26,17 @@ 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 ------------------------
// ---------------------------------------------------------------------------------------------------------------------
@@ -83,19 +57,3 @@ 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)
}
-52
View File
@@ -2,58 +2,6 @@ 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)
}
+99 -269
View File
@@ -1,139 +1,103 @@
package ring
import "base:runtime"
import "core:fmt"
@(private)
ODIN_BOUNDS_CHECK :: !ODIN_NO_BOUNDS_CHECK
Ring :: struct($E: typeid) {
data: []E,
next_write_index, len: int,
Ring :: struct($T: typeid) {
data: []T,
_end_index, len: int,
}
Ring_Soa :: struct($E: typeid) {
data: #soa[]E,
next_write_index, len: int,
Ring_Soa :: struct($T: typeid) {
data: #soa[]T,
_end_index, len: int,
}
destroy_aos :: #force_inline proc(
ring: ^Ring($E),
allocator := context.allocator,
) -> runtime.Allocator_Error {
return delete(ring.data)
from_slice_raos :: #force_inline proc(data: $T/[]$E) -> Ring(E) {
return {data = data, _end_index = -1}
}
destroy_soa :: #force_inline proc(
ring: ^Ring_Soa($E),
allocator := context.allocator,
) -> runtime.Allocator_Error {
return delete(ring.data)
from_slice_rsoa :: #force_inline proc(data: $T/#soa[]$E) -> Ring_Soa(E) {
return {data = data, _end_index = -1}
}
destroy :: proc {
destroy_aos,
destroy_soa,
from_slice :: proc {
from_slice_raos,
from_slice_rsoa,
}
create_aos :: #force_inline proc(
$E: typeid,
capacity: int,
allocator := context.allocator,
) -> (
ring: Ring(E),
err: runtime.Allocator_Error,
) #optional_allocator_error {
ring.data, err = make([]E, capacity, allocator)
return ring, err
}
create_soa :: #force_inline proc(
$E: typeid,
capacity: int,
allocator := context.allocator,
) -> (
ring: Ring_Soa(E),
err: runtime.Allocator_Error,
) #optional_allocator_error {
ring.data, err = make(#soa[]E, capacity, allocator)
return ring, err
}
// All contents of `data` will be completely ignored, `data` is treated as an empty slice.
init_from_slice_aos :: #force_inline proc(ring: ^Ring($E), data: $T/[]E) {
ring.data = data
ring.len = 0
ring.next_write_index = 0
return
}
// All contents of `data` will be completely ignored, `data` is treated as an empty slice.
init_from_slice_soa :: #force_inline proc(ring: ^Ring_Soa($E), data: $T/#soa[]E) {
ring.data = data
ring.len = 0
ring.next_write_index = 0
return
}
init_from_slice :: proc {
init_from_slice_aos,
init_from_slice_soa,
}
// Internal
// Index in the backing array where the ring starts
start_index_aos :: #force_inline proc(ring: Ring($E)) -> int {
return ring.len < len(ring.data) ? 0 : ring.next_write_index
_start_index_raos :: proc(ring: Ring($T)) -> int {
if ring.len < len(ring.data) {
return 0
} else {
start_index := ring._end_index + 1
return 0 if start_index == len(ring.data) else start_index
}
}
// Internal
// Index in the backing array where the ring starts
start_index_soa :: #force_inline proc(ring: Ring_Soa($E)) -> int {
return ring.len < len(ring.data) ? 0 : ring.next_write_index
_start_index_rsoa :: proc(ring: Ring_Soa($T)) -> int {
if ring.len < len(ring.data) {
return 0
} else {
start_index := ring._end_index + 1
return 0 if start_index == len(ring.data) else start_index
}
}
advance_aos :: #force_inline proc(ring: ^Ring($E)) {
advance_raos :: proc(ring: ^Ring($T)) {
// Length
if ring.len != len(ring.data) do ring.len += 1
// Write index
ring.next_write_index += 1
if ring.next_write_index == len(ring.data) do ring.next_write_index = 0
// End index
if ring._end_index == len(ring.data) - 1 { // If we are at the end of the backing array
ring._end_index = 0 // Overflow end to 0
} else {
ring._end_index += 1
}
}
advance_soa :: #force_inline proc(ring: ^Ring_Soa($E)) {
advance_rsoa :: proc(ring: ^Ring_Soa($T)) {
// Length
if ring.len != len(ring.data) do ring.len += 1
// Write index
ring.next_write_index += 1
if ring.next_write_index == len(ring.data) do ring.next_write_index = 0
// End index
if ring._end_index == len(ring.data) - 1 { // If we are at the end of the backing array
ring._end_index = 0 // Overflow end to 0
} else {
ring._end_index += 1
}
}
advance :: proc {
advance_aos,
advance_soa,
advance_raos,
advance_rsoa,
}
append_aos :: #force_inline proc(ring: ^Ring($E), element: E) {
ring.data[ring.next_write_index] = element
append_raos :: proc(ring: ^Ring($T), element: T) {
advance(ring)
ring.data[ring._end_index] = element
}
append_soa :: #force_inline proc(ring: ^Ring_Soa($E), element: E) {
ring.data[ring.next_write_index] = element
append_rsoa :: proc(ring: ^Ring_Soa($T), element: T) {
advance(ring)
ring.data[ring._end_index] = element
}
append :: proc {
append_aos,
append_soa,
append_raos,
append_rsoa,
}
get_aos :: #force_inline proc(ring: Ring($E), index: int) -> ^E {
get_raos :: proc(ring: Ring($T), index: int) -> ^T {
when ODIN_BOUNDS_CHECK {
fmt.assertf(index < ring.len, "Ring index %i out of bounds for length %i", index, ring.len)
if index >= ring.len {
panic(fmt.tprintf("Ring index %i out of bounds for length %i", index, ring.len))
}
}
array_index := start_index_aos(ring) + index
array_index := _start_index_raos(ring) + index
if array_index < len(ring.data) {
return &ring.data[array_index]
} else {
@@ -143,12 +107,14 @@ get_aos :: #force_inline proc(ring: Ring($E), index: int) -> ^E {
}
// SOA can't return soa pointer to parapoly T.
get_soa :: #force_inline proc(ring: Ring_Soa($E), index: int) -> E {
get_rsoa :: proc(ring: Ring_Soa($T), index: int) -> T {
when ODIN_BOUNDS_CHECK {
fmt.assertf(index < ring.len, "Ring index %i out of bounds for length %i", index, ring.len)
if index >= ring.len {
panic(fmt.tprintf("Ring index %i out of bounds for length %i", index, ring.len))
}
}
array_index := start_index_soa(ring) + index
array_index := _start_index_rsoa(ring) + index
if array_index < len(ring.data) {
return ring.data[array_index]
} else {
@@ -158,36 +124,36 @@ get_soa :: #force_inline proc(ring: Ring_Soa($E), index: int) -> E {
}
get :: proc {
get_aos,
get_soa,
get_raos,
get_rsoa,
}
get_last_aos :: #force_inline proc(ring: Ring($E)) -> ^E {
get_last_raos :: #force_inline proc(ring: Ring($T)) -> ^T {
return get(ring, ring.len - 1)
}
get_last_soa :: #force_inline proc(ring: Ring_Soa($E)) -> E {
get_last_rsoa :: #force_inline proc(ring: Ring_Soa($T)) -> T {
return get(ring, ring.len - 1)
}
get_last :: proc {
get_last_aos,
get_last_soa,
get_last_raos,
get_last_rsoa,
}
clear_aos :: #force_inline proc "contextless" (ring: ^Ring($E)) {
clear_raos :: #force_inline proc "contextless" (ring: ^Ring($T)) {
ring.len = 0
ring.next_write_index = 0
ring._end_index = -1
}
clear_soa :: #force_inline proc "contextless" (ring: ^Ring_Soa($E)) {
clear_rsoa :: #force_inline proc "contextless" (ring: ^Ring_Soa($T)) {
ring.len = 0
ring.next_write_index = 0
ring._end_index = -1
}
clear :: proc {
clear_aos,
clear_soa,
clear_raos,
clear_rsoa,
}
// ---------------------------------------------------------------------------------------------------------------------
@@ -198,27 +164,28 @@ import "core:testing"
@(test)
test_ring_aos :: proc(t: ^testing.T) {
ring := create_aos(int, 10)
defer destroy(&ring)
data := make_slice([]int, 10)
ring := from_slice(data)
defer delete(ring.data)
for i in 1 ..= 5 {
append(&ring, i)
log.debug("Length:", ring.len)
log.debug("Start index:", start_index_aos(ring))
log.debug("Next write index:", ring.next_write_index)
log.debug("Start index:", _start_index_raos(ring))
log.debug("End index:", ring._end_index)
log.debug(ring.data)
}
testing.expect_value(t, get(ring, 0)^, 1)
testing.expect_value(t, get(ring, 4)^, 5)
testing.expect_value(t, ring.len, 5)
testing.expect_value(t, ring.next_write_index, 5)
testing.expect_value(t, start_index_aos(ring), 0)
testing.expect_value(t, ring._end_index, 4)
testing.expect_value(t, _start_index_raos(ring), 0)
for i in 6 ..= 15 {
append(&ring, i)
log.debug("Length:", ring.len)
log.debug("Start index:", start_index_aos(ring))
log.debug("Next write index:", ring.next_write_index)
log.debug("Start index:", _start_index_raos(ring))
log.debug("End index:", ring._end_index)
log.debug(ring.data)
}
testing.expect_value(t, get(ring, 0)^, 6)
@@ -226,18 +193,18 @@ test_ring_aos :: proc(t: ^testing.T) {
testing.expect_value(t, get(ring, 9)^, 15)
testing.expect_value(t, get_last(ring)^, 15)
testing.expect_value(t, ring.len, 10)
testing.expect_value(t, ring.next_write_index, 5)
testing.expect_value(t, start_index_aos(ring), 5)
testing.expect_value(t, ring._end_index, 4)
testing.expect_value(t, _start_index_raos(ring), 5)
for i in 15 ..= 25 {
append(&ring, i)
log.debug("Length:", ring.len)
log.debug("Start index:", start_index_aos(ring))
log.debug("Next write index:", ring.next_write_index)
log.debug("Start index:", _start_index_raos(ring))
log.debug("End index:", ring._end_index)
log.debug(ring.data)
}
testing.expect_value(t, get(ring, 0)^, 16)
testing.expect_value(t, ring.next_write_index, 6)
testing.expect_value(t, ring._end_index, 5)
testing.expect_value(t, get_last(ring)^, 25)
clear(&ring)
@@ -252,27 +219,28 @@ test_ring_soa :: proc(t: ^testing.T) {
x, y: int,
}
ring := create_soa(Ints, 10)
defer destroy(&ring)
data := make_soa_slice(#soa[]Ints, 10)
ring := from_slice(data)
defer delete(ring.data)
for i in 1 ..= 5 {
append(&ring, Ints{i, i})
log.debug("Length:", ring.len)
log.debug("Start index:", start_index_soa(ring))
log.debug("Next write index:", ring.next_write_index)
log.debug("Start index:", _start_index_rsoa(ring))
log.debug("End index:", ring._end_index)
log.debug(ring.data)
}
testing.expect_value(t, get(ring, 0), Ints{1, 1})
testing.expect_value(t, get(ring, 4), Ints{5, 5})
testing.expect_value(t, ring.len, 5)
testing.expect_value(t, ring.next_write_index, 5)
testing.expect_value(t, start_index_soa(ring), 0)
testing.expect_value(t, ring._end_index, 4)
testing.expect_value(t, _start_index_rsoa(ring), 0)
for i in 6 ..= 15 {
append(&ring, Ints{i, i})
log.debug("Length:", ring.len)
log.debug("Start index:", start_index_soa(ring))
log.debug("Next write index:", ring.next_write_index)
log.debug("Start index:", _start_index_rsoa(ring))
log.debug("End index:", ring._end_index)
log.debug(ring.data)
}
testing.expect_value(t, get(ring, 0), Ints{6, 6})
@@ -280,18 +248,18 @@ test_ring_soa :: proc(t: ^testing.T) {
testing.expect_value(t, get(ring, 9), Ints{15, 15})
testing.expect_value(t, get_last(ring), Ints{15, 15})
testing.expect_value(t, ring.len, 10)
testing.expect_value(t, ring.next_write_index, 5)
testing.expect_value(t, start_index_soa(ring), 5)
testing.expect_value(t, ring._end_index, 4)
testing.expect_value(t, _start_index_rsoa(ring), 5)
for i in 15 ..= 25 {
append(&ring, Ints{i, i})
log.debug("Length:", ring.len)
log.debug("Start index:", start_index_soa(ring))
log.debug("Next write index:", ring.next_write_index)
log.debug("Start index:", _start_index_rsoa(ring))
log.debug("End index:", ring._end_index)
log.debug(ring.data)
}
testing.expect_value(t, get(ring, 0), Ints{16, 16})
testing.expect_value(t, ring.next_write_index, 6)
testing.expect_value(t, ring._end_index, 5)
testing.expect_value(t, get_last(ring), Ints{25, 25})
clear(&ring)
@@ -299,141 +267,3 @@ test_ring_soa :: proc(t: ^testing.T) {
testing.expect_value(t, ring.len, 1)
testing.expect_value(t, get(ring, 0), Ints{1, 1})
}
@(test)
test_ring_aos_init_from_slice :: proc(t: ^testing.T) {
// Stack-allocated backing with pre-existing garbage and odd capacity.
backing: [7]int = {99, 99, 99, 99, 99, 99, 99}
ring: Ring(int)
init_from_slice(&ring, backing[:])
// Empty ring invariants after init_from_slice.
testing.expect_value(t, ring.len, 0)
testing.expect_value(t, ring.next_write_index, 0)
testing.expect_value(t, start_index_aos(ring), 0)
// Partial fill (3 / 7).
for i in 1 ..= 3 do append(&ring, i)
testing.expect_value(t, ring.len, 3)
testing.expect_value(t, ring.next_write_index, 3)
testing.expect_value(t, start_index_aos(ring), 0)
testing.expect_value(t, get(ring, 0)^, 1)
testing.expect_value(t, get(ring, 2)^, 3)
testing.expect_value(t, get_last(ring)^, 3)
// Fill exactly to capacity. Pushing element 7 must make len == cap
// AND wrap next_write_index from 6 back to 0 in the same step.
for i in 4 ..= 7 do append(&ring, i)
testing.expect_value(t, ring.len, 7)
testing.expect_value(t, ring.next_write_index, 0)
testing.expect_value(t, start_index_aos(ring), 0)
testing.expect_value(t, get(ring, 0)^, 1)
testing.expect_value(t, get(ring, 6)^, 7)
testing.expect_value(t, get_last(ring)^, 7)
// First overwrite — oldest element shifts by one.
append(&ring, 8)
testing.expect_value(t, ring.len, 7)
testing.expect_value(t, ring.next_write_index, 1)
testing.expect_value(t, start_index_aos(ring), 1)
testing.expect_value(t, get(ring, 0)^, 2)
testing.expect_value(t, get(ring, 6)^, 8)
testing.expect_value(t, get_last(ring)^, 8)
// Stress: 3 more complete wrap cycles (21 more pushes).
// After 29 total pushes, ring contains the last 7 (23..=29),
// and next_write_index = 29 mod 7 = 1.
for i in 9 ..= 29 do append(&ring, i)
testing.expect_value(t, ring.len, 7)
testing.expect_value(t, ring.next_write_index, 1)
testing.expect_value(t, start_index_aos(ring), 1)
testing.expect_value(t, get(ring, 0)^, 23)
testing.expect_value(t, get(ring, 3)^, 26)
testing.expect_value(t, get(ring, 6)^, 29)
testing.expect_value(t, get_last(ring)^, 29)
// Clear returns ring to empty-equivalent state.
clear(&ring)
testing.expect_value(t, ring.len, 0)
testing.expect_value(t, ring.next_write_index, 0)
testing.expect_value(t, start_index_aos(ring), 0)
// Single-element edge case: get_last(len==1) routes through get(ring, 0).
append(&ring, 42)
testing.expect_value(t, ring.len, 1)
testing.expect_value(t, ring.next_write_index, 1)
testing.expect_value(t, get(ring, 0)^, 42)
testing.expect_value(t, get_last(ring)^, 42)
}
@(test)
test_ring_soa_init_from_slice :: proc(t: ^testing.T) {
Ints :: struct {
x, y: int,
}
// Stack-allocated backing with pre-existing garbage and odd capacity.
backing: #soa[7]Ints = {{99, 99}, {99, 99}, {99, 99}, {99, 99}, {99, 99}, {99, 99}, {99, 99}}
ring: Ring_Soa(Ints)
init_from_slice(&ring, backing[:])
// Empty ring invariants after init_from_slice.
testing.expect_value(t, ring.len, 0)
testing.expect_value(t, ring.next_write_index, 0)
testing.expect_value(t, start_index_soa(ring), 0)
// Partial fill (3 / 7).
for i in 1 ..= 3 do append(&ring, Ints{i, i})
testing.expect_value(t, ring.len, 3)
testing.expect_value(t, ring.next_write_index, 3)
testing.expect_value(t, start_index_soa(ring), 0)
testing.expect_value(t, get(ring, 0), Ints{1, 1})
testing.expect_value(t, get(ring, 2), Ints{3, 3})
testing.expect_value(t, get_last(ring), Ints{3, 3})
// Fill exactly to capacity. Pushing element 7 must make len == cap
// AND wrap next_write_index from 6 back to 0 in the same step.
for i in 4 ..= 7 do append(&ring, Ints{i, i})
testing.expect_value(t, ring.len, 7)
testing.expect_value(t, ring.next_write_index, 0)
testing.expect_value(t, start_index_soa(ring), 0)
testing.expect_value(t, get(ring, 0), Ints{1, 1})
testing.expect_value(t, get(ring, 6), Ints{7, 7})
testing.expect_value(t, get_last(ring), Ints{7, 7})
// First overwrite — oldest element shifts by one.
append(&ring, Ints{8, 8})
testing.expect_value(t, ring.len, 7)
testing.expect_value(t, ring.next_write_index, 1)
testing.expect_value(t, start_index_soa(ring), 1)
testing.expect_value(t, get(ring, 0), Ints{2, 2})
testing.expect_value(t, get(ring, 6), Ints{8, 8})
testing.expect_value(t, get_last(ring), Ints{8, 8})
// Stress: 3 more complete wrap cycles (21 more pushes).
// After 29 total pushes, ring contains the last 7 (23..=29),
// and next_write_index = 29 mod 7 = 1.
for i in 9 ..= 29 do append(&ring, Ints{i, i})
testing.expect_value(t, ring.len, 7)
testing.expect_value(t, ring.next_write_index, 1)
testing.expect_value(t, start_index_soa(ring), 1)
testing.expect_value(t, get(ring, 0), Ints{23, 23})
testing.expect_value(t, get(ring, 3), Ints{26, 26})
testing.expect_value(t, get(ring, 6), Ints{29, 29})
testing.expect_value(t, get_last(ring), Ints{29, 29})
// Clear returns ring to empty-equivalent state.
clear(&ring)
testing.expect_value(t, ring.len, 0)
testing.expect_value(t, ring.next_write_index, 0)
testing.expect_value(t, start_index_soa(ring), 0)
// Single-element edge case: get_last(len==1) routes through get(ring, 0).
append(&ring, Ints{42, 42})
testing.expect_value(t, ring.len, 1)
testing.expect_value(t, ring.next_write_index, 1)
testing.expect_value(t, get(ring, 0), Ints{42, 42})
testing.expect_value(t, get_last(ring), Ints{42, 42})
}
+23 -141
View File
@@ -57,6 +57,11 @@ CornerRadius :: struct {
bottomRight: c.float,
}
BorderData :: struct {
width: u32,
color: Color,
}
ElementId :: struct {
id: u32,
offset: u32,
@@ -64,12 +69,6 @@ ElementId :: struct {
stringId: String,
}
ElementIdArray :: struct {
capacity: i32,
length: i32,
internalArray: [^]ElementId,
}
when ODIN_OS == .Windows {
EnumBackingType :: u32
} else {
@@ -84,8 +83,6 @@ RenderCommandType :: enum EnumBackingType {
Image,
ScissorStart,
ScissorEnd,
OverlayColorStart,
OverlayColorEnd,
Custom,
}
@@ -141,92 +138,6 @@ 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
@@ -304,15 +215,6 @@ CustomRenderData :: struct {
customData: rawptr,
}
ClipRenderData :: struct {
horizontal: bool,
vertical: bool,
}
OverlayColorRenderData :: struct {
color: Color,
}
BorderRenderData :: struct {
color: Color,
cornerRadius: CornerRadius,
@@ -325,8 +227,6 @@ RenderCommandData :: struct #raw_union {
image: ImageRenderData,
custom: CustomRenderData,
border: BorderRenderData,
clip: ClipRenderData,
overlayColor: OverlayColorRenderData,
}
RenderCommand :: struct {
@@ -438,9 +338,9 @@ ClayArray :: struct($type: typeid) {
}
ElementDeclaration :: struct {
id: ElementId,
layout: LayoutConfig,
backgroundColor: Color,
overlayColor: Color,
cornerRadius: CornerRadius,
aspectRatio: AspectRatioElementConfig,
image: ImageElementConfig,
@@ -448,7 +348,6 @@ ElementDeclaration :: struct {
custom: CustomElementConfig,
clip: ClipElementConfig,
border: BorderElementConfig,
transition: TransitionElementConfig,
userData: rawptr,
}
@@ -461,7 +360,6 @@ ErrorType :: enum EnumBackingType {
FloatingContainerParentNotFound,
PercentageOver1,
InternalError,
UnbalancedOpenClose,
}
ErrorData :: struct {
@@ -480,27 +378,23 @@ 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(deltaTime: c.float) -> ClayArray(RenderCommand) ---
GetOpenElementId :: proc() -> u32 ---
EndLayout :: proc() -> ClayArray(RenderCommand) ---
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) ---
@@ -514,15 +408,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, seed: u32) -> ElementId ---
_HashStringWithOffset :: proc(key: String, index: u32, seed: u32) -> ElementId ---
_OpenTextElement :: proc(text: String, textConfig: TextElementConfig) ---
_HashString :: proc(key: String, offset: u32, seed: u32) -> ElementId ---
_OpenTextElement :: proc(text: String, textConfig: ^TextElementConfig) ---
_StoreTextElementConfig :: proc(config: TextElementConfig) -> ^TextElementConfig ---
_GetParentElementId :: proc() -> u32 ---
}
ConfigureOpenElement :: proc(config: ElementDeclaration) -> bool {
@@ -531,39 +425,27 @@ ConfigureOpenElement :: proc(config: ElementDeclaration) -> bool {
}
@(deferred_none = _CloseElement)
UI_WithId :: proc(id: ElementId) -> proc(config: ElementDeclaration) -> bool {
_OpenElementWithId(id)
return ConfigureOpenElement
}
@(deferred_none = _CloseElement)
UI_AutoId :: proc() -> proc(config: ElementDeclaration) -> bool {
UI :: proc() -> proc (config: ElementDeclaration) -> bool {
_OpenElement()
return ConfigureOpenElement
}
UI :: proc {
UI_WithId,
UI_AutoId,
}
Text :: proc {
TextStatic,
TextDynamic,
}
TextStatic :: proc($text: string, config: TextElementConfig) {
Text :: 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}
return { left = allPadding, right = allPadding, top = allPadding, bottom = allPadding }
}
BorderOutside :: proc(width: u16) -> BorderWidth {
@@ -578,11 +460,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}}
}
@@ -599,9 +481,9 @@ MakeString :: proc(label: string) -> String {
}
ID :: proc(label: string, index: u32 = 0) -> ElementId {
return _HashString(MakeString(label), index)
return _HashString(MakeString(label), index, 0)
}
ID_LOCAL :: proc(label: string, index: u32 = 0) -> ElementId {
return _HashStringWithOffset(MakeString(label), index, GetOpenElementId())
return _HashString(MakeString(label), index, _GetParentElementId())
}
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+6
View File
@@ -0,0 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/DanielGavin/ols/master/misc/odinfmt.schema.json",
"character_width": 180,
"sort_imports": true,
"tabs": false
}
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+906 -812
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+13 -56
View File
@@ -1,11 +1,8 @@
package examples
import "core:fmt"
import "core:log"
import "core:mem"
import "core:os"
import "core:sys/posix"
import mdb "../../lmdb"
// 0o660
@@ -13,74 +10,34 @@ DB_MODE :: posix.mode_t{.IWGRP, .IRGRP, .IWUSR, .IRUSR}
DB_PATH :: "out/debug/lmdb_example_db"
main :: proc() {
//----- General setup ----------------------------------
// Temp
track_temp: mem.Tracking_Allocator
mem.tracking_allocator_init(&track_temp, context.temp_allocator)
context.temp_allocator = mem.tracking_allocator(&track_temp)
// Default
track: mem.Tracking_Allocator
mem.tracking_allocator_init(&track, context.allocator)
context.allocator = mem.tracking_allocator(&track)
// Log a warning about any memory that was not freed by the end of the program.
// This could be fine for some global state or it could be a memory leak.
defer {
// Temp allocator
if len(track_temp.bad_free_array) > 0 {
fmt.eprintf("=== %v incorrect frees - temp allocator: ===\n", len(track_temp.bad_free_array))
for entry in track_temp.bad_free_array {
fmt.eprintf("- %p @ %v\n", entry.memory, entry.location)
}
mem.tracking_allocator_destroy(&track_temp)
}
// Default allocator
if len(track.allocation_map) > 0 {
fmt.eprintf("=== %v allocations not freed - main allocator: ===\n", len(track.allocation_map))
for _, entry in track.allocation_map {
fmt.eprintf("- %v bytes @ %v\n", entry.size, entry.location)
}
}
if len(track.bad_free_array) > 0 {
fmt.eprintf("=== %v incorrect frees - main allocator: ===\n", len(track.bad_free_array))
for entry in track.bad_free_array {
fmt.eprintf("- %p @ %v\n", entry.memory, entry.location)
}
}
mem.tracking_allocator_destroy(&track)
}
// Logger
context.logger = log.create_console_logger()
defer log.destroy_console_logger(context.logger)
environment: ^mdb.Env
// Create environment for lmdb
mdb.panic_on_err(mdb.env_create(&environment))
// Create directory for databases. Won't do anything if it already exists.
os.make_directory(DB_PATH)
// 0o774 gives all permissions for owner and group, read for everyone else.
os.make_directory(DB_PATH, 0o774)
// Open the database files (creates them if they don't already exist)
mdb.panic_on_err(mdb.env_open(environment, DB_PATH, {}, DB_MODE))
mdb.panic_on_err(mdb.env_open(environment, DB_PATH, 0, DB_MODE))
// Transactions
txn_handle: ^mdb.Txn
db_handle: mdb.Dbi
// Put transaction
key := 7
key_val := mdb.pod_val(&key)
key_val := mdb.autoval(&key)
put_data := 12
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, {}))
put_data_val := mdb.autoval(&put_data)
mdb.panic_on_err(mdb.txn_begin(environment, nil, 0, &txn_handle))
mdb.panic_on_err(mdb.dbi_open(txn_handle, nil, 0, &db_handle))
mdb.panic_on_err(mdb.put(txn_handle, db_handle, &key_val.raw, &put_data_val.raw, 0))
mdb.panic_on_err(mdb.txn_commit(txn_handle))
// Get transaction
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.pod_copy(data_val, int)
mdb.txn_abort(txn_handle)
get_data_val := mdb.nil_autoval(int)
mdb.panic_on_err(mdb.txn_begin(environment, nil, 0, &txn_handle))
mdb.panic_on_err(mdb.get(txn_handle, db_handle, &key_val.raw, &get_data_val.raw))
mdb.panic_on_err(mdb.txn_commit(txn_handle))
data_cpy := mdb.autoval_get_data(&get_data_val)^
fmt.println("Get result:", data_cpy)
}
+150 -229
View File
@@ -164,160 +164,24 @@
*/
package lmdb
foreign import lib "system:lmdb"
import "core:c"
import "core:fmt"
import "core:reflect"
import "core:sys/posix"
import b "../../basic"
// ---------------------------------------------------------------------------------------------------------------------
// ----- Added Odin Helpers ------------------------
// ---------------------------------------------------------------------------------------------------------------------
// 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.).
pod_val :: #force_inline proc(val_ptr: ^$T) -> Val {
when ODIN_DEBUG {
fmt.assertf(
reflect.has_no_indirections(type_info_of(T)),
"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 POD T out of the LMDB memory map by copying it into caller
// storage. The returned T has no lifetime tie to the transaction.
pod_copy :: #force_inline proc(val: Val, $T: typeid) -> T {
when ODIN_DEBUG {
fmt.assertf(
reflect.has_no_indirections(type_info_of(T)),
"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 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.
pod_view :: #force_inline proc(val: Val, $T: typeid) -> ^T {
when ODIN_DEBUG {
fmt.assertf(
reflect.has_no_indirections(type_info_of(T)),
"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 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.
pod_slice_val :: #force_inline proc(s: []$T) -> Val {
when ODIN_DEBUG {
fmt.assertf(
reflect.has_no_indirections(type_info_of(T)),
"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)}
}
// Zero-copy slice view into the LMDB memory map.
// T must match the element type that was originally stored.
// MUST NOT be modified — writes through this slice either segfault (default
// env mode) or silently corrupt the database (ENV_WRITEMAP).
// 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.
pod_slice_view :: #force_inline proc(val: Val, $T: typeid) -> []T {
when ODIN_DEBUG {
fmt.assertf(
reflect.has_no_indirections(type_info_of(T)),
"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)]
}
// Wrap a string's bytes as an LMDB Val for use with put/get.
// The caller's string must remain valid (backing memory not freed) for the
// duration of the put call that consumes this Val.
string_val :: #force_inline proc(s: string) -> Val {
return Val{uint(len(s)), raw_data(s)}
}
// Zero-copy string view into the LMDB memory map.
// MUST NOT be modified — writes through the underlying bytes either segfault
// (default env mode) or silently corrupt the database (ENV_WRITEMAP).
// 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 {
return string((cast([^]u8)val.data)[:val.size])
}
// Panic if there is an error
panic_on_err :: #force_inline proc(error: Error, loc := #caller_location) {
if error != .NONE {
fmt.panicf("LMDB error %v: %s", error, strerror(i32(error)), loc = loc)
}
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- Bindings ------------------------
// ---------------------------------------------------------------------------------------------------------------------
_ :: c
when ODIN_OS == .Windows {
#panic("TODO: Compile windows .lib for lmdb")
mode_t :: c.int
filehandle_t :: rawptr
} else when ODIN_OS ==
.Linux || ODIN_OS == .Darwin || ODIN_OS == .FreeBSD || ODIN_OS == .OpenBSD || ODIN_OS == .NetBSD {
foreign import lib "system:lmdb"
mode_t :: posix.mode_t
filehandle_t :: c.int
} else {
#panic("levlib/vendor/lmdb: unsupported OS target")
mode_t :: posix.mode_t
}
when ODIN_OS == .Windows {
filehandle_t :: rawptr
} else {
filehandle_t :: c.int
}
Env :: struct {}
@@ -325,7 +189,7 @@ Env :: struct {}
Txn :: struct {}
/** @brief A handle for an individual database in the DB environment. */
Dbi :: c.uint
Dbi :: u32
Cursor :: struct {}
@@ -341,8 +205,33 @@ Cursor :: struct {}
* Other data items can in theory be from 0 to 0xffffffff bytes long.
*/
Val :: struct {
size: uint, /**< size of the data item */
data: rawptr, /**< address of the data item */
mv_size: uint, /**< size of the data item */
mv_data: rawptr, /**< address of the data item */
}
// Automatic `Val` handling for a given type 'T'.
// Will not traverse pointers. If `T` stores pointers, you probably don't want to use this.
Auto_Val :: struct($T: typeid) {
raw: Val,
}
autoval :: #force_inline proc "contextless" (val_ptr: ^$T) -> Auto_Val(T) {
return Auto_Val(T){Val{size_of(T), val_ptr}}
}
nil_autoval :: #force_inline proc "contextless" ($T: typeid) -> Auto_Val(T) {
return Auto_Val(T){Val{size_of(T), nil}}
}
autoval_get_data :: #force_inline proc "contextless" (val: ^Auto_Val($T)) -> ^T {
return cast(^T)val.raw.mv_data
}
// Panic if there is an error
panic_on_err :: #force_inline proc(error: Error) {
if error != .NONE {
fmt.panicf("Irrecoverable LMDB error", strerror(i32(error)))
}
}
/** @brief A callback function used to compare two keys in a database */
@@ -367,62 +256,82 @@ Rel_Func :: #type proc "c" (item: ^Val, oldptr, newptr, relctx: rawptr)
/** @defgroup mdb_env Environment Flags
* @{
*/
Env_Flag :: enum u32 {
FIXEDMAP = 0, /**< mmap at a fixed address (experimental) */
NOSUBDIR = 14, /**< no environment directory */
NOSYNC = 16, /**< don't fsync after commit */
RDONLY = 17, /**< read only */
NOMETASYNC = 18, /**< don't fsync metapage after commit */
WRITEMAP = 19, /**< use writable mmap */
MAPASYNC = 20, /**< use asynchronous msync when WRITEMAP is used */
NOTLS = 21, /**< tie reader locktable slots to Txn objects instead of to threads */
NOLOCK = 22, /**< don't do any locking, caller must manage their own locks */
NORDAHEAD = 23, /**< don't do readahead (no effect on Windows) */
NOMEMINIT = 24, /**< don't initialize malloc'd memory before writing to datafile */
PREVSNAPSHOT = 25, /**< use the previous snapshot rather than the latest one */
}
Env_Flags :: distinct bit_set[Env_Flag;c.uint]
/** mmap at a fixed address (experimental) */
ENV_FIXEDMAP :: 0x01
/** no environment directory */
ENV_NOSUBDIR :: 0x4000
/** don't fsync after commit */
ENV_NOSYNC :: 0x10000
/** read only */
ENV_RDONLY :: 0x20000
/** don't fsync metapage after commit */
ENV_NOMETASYNC :: 0x40000
/** use writable mmap */
ENV_WRITEMAP :: 0x80000
/** use asynchronous msync when #MDB_WRITEMAP is used */
ENV_MAPASYNC :: 0x100000
/** tie reader locktable slots to #MDB_txn objects instead of to threads */
ENV_NOTLS :: 0x200000
/** don't do any locking, caller must manage their own locks */
ENV_NOLOCK :: 0x400000
/** don't do readahead (no effect on Windows) */
ENV_NORDAHEAD :: 0x800000
/** don't initialize malloc'd memory before writing to datafile */
ENV_NOMEMINIT :: 0x1000000
/** @} */
/** @defgroup mdb_dbi_open Database Flags
* @{
*/
Db_Flag :: enum u32 {
REVERSEKEY = 1, /**< use reverse string keys */
DUPSORT = 2, /**< use sorted duplicates */
INTEGERKEY = 3, /**< numeric keys in native byte order */
DUPFIXED = 4, /**< with DUPSORT, sorted dup items have fixed size */
INTEGERDUP = 5, /**< with DUPSORT, dups are INTEGERKEY-style integers */
REVERSEDUP = 6, /**< with DUPSORT, use reverse string dups */
CREATE = 18, /**< create DB if not already existing */
}
Db_Flags :: distinct bit_set[Db_Flag;c.uint]
/** use reverse string keys */
DB_REVERSEKEY :: 0x02
/** use sorted duplicates */
DB_DUPSORT :: 0x04
/** numeric keys in native byte order: either unsigned int or size_t.
* The keys must all be of the same size. */
DB_INTEGERKEY :: 0x08
/** with #MDB_DUPSORT, sorted dup items have fixed size */
DB_DUPFIXED :: 0x10
/** with #MDB_DUPSORT, dups are #MDB_INTEGERKEY-style integers */
DB_INTEGERDUP :: 0x20
/** with #MDB_DUPSORT, use reverse string dups */
DB_REVERSEDUP :: 0x40
/** create DB if not already existing */
DB_CREATE :: 0x40000
/** @} */
/** @defgroup mdb_put Write Flags
* @{
*/
Write_Flag :: enum u32 {
NOOVERWRITE = 4, /**< For put: Don't write if the key already exists */
NODUPDATA = 5, /**< For DUPSORT: don't write if the key and data pair already exist.
For mdb_cursor_del: remove all duplicate data items. */
CURRENT = 6, /**< For mdb_cursor_put: overwrite the current key/data pair */
RESERVE = 16, /**< For put: Just reserve space for data, don't copy it */
APPEND = 17, /**< Data is being appended, don't split full pages */
APPENDDUP = 18, /**< Duplicate data is being appended, don't split full pages */
MULTIPLE = 19, /**< Store multiple data items in one call. Only for DUPFIXED. */
}
Write_Flags :: distinct bit_set[Write_Flag;c.uint]
/** @} */
/** For put: Don't write if the key already exists. */
WRITE_NOOVERWRITE :: 0x10
/** Only for #MDB_DUPSORT<br>
* For put: don't write if the key and data pair already exist.<br>
* For mdb_cursor_del: remove all duplicate data items.
*/
WRITE_NODUPDATA :: 0x20
/** For mdb_cursor_put: overwrite the current key/data pair */
WRITE_CURRENT :: 0x40
/** For put: Just reserve space for data, don't copy it. Return a
* pointer to the reserved space.
*/
WRITE_RESERVE :: 0x10000
/** Data is being appended, don't split full pages. */
WRITE_APPEND :: 0x20000
/** Duplicate data is being appended, don't split full pages. */
WRITE_APPENDDUP :: 0x40000
/** Store multiple data items in one call. Only for #MDB_DUPFIXED. */
WRITE_MULTIPLE :: 0x80000
/* @} */
/** @defgroup mdb_copy Copy Flags
* @{
*/
Copy_Flag :: enum u32 {
COMPACT = 0, /**< Compacting copy: Omit free space from copy, and renumber all pages sequentially. */
}
Copy_Flags :: distinct bit_set[Copy_Flag;c.uint]
/** @} */
/** Compacting copy: Omit free space from copy, and renumber all
* pages sequentially.
*/
CP_COMPACT :: 0x01
/* @} */
/** @brief Cursor Get operations.
*
@@ -431,24 +340,33 @@ Copy_Flags :: distinct bit_set[Copy_Flag;c.uint]
*/
Cursor_Op :: enum c.int {
FIRST, /**< Position at first key/data item */
FIRST_DUP, /**< Position at first data item of current key. Only for DUPSORT */
GET_BOTH, /**< Position at key/data pair. Only for DUPSORT */
GET_BOTH_RANGE, /**< Position at key, nearest data. Only for DUPSORT */
FIRST_DUP, /**< Position at first data item of current key.
Only for #MDB_DUPSORT */
GET_BOTH, /**< Position at key/data pair. Only for #MDB_DUPSORT */
GET_BOTH_RANGE, /**< position at key, nearest data. Only for #MDB_DUPSORT */
GET_CURRENT, /**< Return key/data at current cursor position */
GET_MULTIPLE, /**< Return up to a page of duplicate data items from current cursor position. Only for DUPFIXED */
GET_MULTIPLE, /**< Return up to a page of duplicate data items
from current cursor position. Move cursor to prepare
for #MDB_NEXT_MULTIPLE. Only for #MDB_DUPFIXED */
LAST, /**< Position at last key/data item */
LAST_DUP, /**< Position at last data item of current key. Only for DUPSORT */
LAST_DUP, /**< Position at last data item of current key.
Only for #MDB_DUPSORT */
NEXT, /**< Position at next data item */
NEXT_DUP, /**< Position at next data item of current key. Only for DUPSORT */
NEXT_MULTIPLE, /**< Return up to a page of duplicate data items from next cursor position. Only for DUPFIXED */
NEXT_DUP, /**< Position at next data item of current key.
Only for #MDB_DUPSORT */
NEXT_MULTIPLE, /**< Return up to a page of duplicate data items
from next cursor position. Move cursor to prepare
for #MDB_NEXT_MULTIPLE. Only for #MDB_DUPFIXED */
NEXT_NODUP, /**< Position at first data item of next key */
PREV, /**< Position at previous data item */
PREV_DUP, /**< Position at previous data item of current key. Only for DUPSORT */
PREV_DUP, /**< Position at previous data item of current key.
Only for #MDB_DUPSORT */
PREV_NODUP, /**< Position at last data item of previous key */
SET, /**< Position at specified key */
SET_KEY, /**< Position at specified key, return key + data */
SET_RANGE, /**< Position at first key greater than or equal to specified key */
PREV_MULTIPLE, /**< Position at previous page and return up to a page of duplicate data items. Only for DUPFIXED */
SET_RANGE, /**< Position at first key greater than or equal to specified key. */
PREV_MULTIPLE, /**< Position at previous page and return up to
a page of duplicate data items. Only for #MDB_DUPFIXED */
}
Error :: enum c.int {
@@ -501,28 +419,33 @@ Error :: enum c.int {
BAD_VALSIZE = -30781,
/** The specified DBI was changed unexpectedly */
BAD_DBI = -30780,
/** Unexpected problem - txn should abort */
PROBLEM = -30779,
}
/** @brief Statistics for a database in the environment */
Stat :: struct {
psize: u32, /**< Size of a database page. This is currently the same for all databases. */
depth: u32, /**< Depth (height) of the B-tree */
branch_pages: uint, /**< Number of internal (non-leaf) pages */
leaf_pages: uint, /**< Number of leaf pages */
overflow_pages: uint, /**< Number of overflow pages */
entries: uint, /**< Number of data items */
ms_psize: u32,
/**< Size of a database page.
This is currently the same for all databases. */
ms_depth: u32,
/**< Depth (height) of the B-tree */
ms_branch_pages: uint,
/**< Number of internal (non-leaf) pages */
ms_leaf_pages: uint,
/**< Number of leaf pages */
ms_overflow_pages: uint,
/**< Number of overflow pages */
ms_entries: uint,
/**< Number of data items */
}
/** @brief Information about the environment */
Env_Info :: struct {
mapaddr: rawptr, /**< Address of map, if fixed */
mapsize: uint, /**< Size of the data memory map */
last_pgno: uint, /**< ID of the last used page */
last_txnid: uint, /**< ID of the last committed transaction */
maxreaders: u32, /**< max reader slots in the environment */
numreaders: u32, /**< max reader slots used in the environment */
me_mapaddr: rawptr, /**< Address of map, if fixed */
me_mapsize: uint, /**< Size of the data memory map */
me_last_pgno: uint, /**< ID of the last used page */
me_last_txnid: uint, /**< ID of the last committed transaction */
me_maxreaders: u32, /**< max reader slots in the environment */
me_numreaders: u32, /**< max reader slots used in the environment */
}
/** @brief A callback function for most LMDB assert() failures,
@@ -531,7 +454,7 @@ Env_Info :: struct {
* @param[in] env An environment handle returned by #mdb_env_create().
* @param[in] msg The assertion message, not including newline.
*/
Assert_Func :: #type proc "c" (_: ^Env, _: cstring)
Assert_Func :: proc "c" (_: ^Env, _: cstring)
/** @brief A callback function used to print a message from the library.
*
@@ -539,7 +462,7 @@ Assert_Func :: #type proc "c" (_: ^Env, _: cstring)
* @param[in] ctx An arbitrary context pointer for the callback.
* @return < 0 on failure, >= 0 on success.
*/
Msg_Func :: #type proc "c" (_: cstring, _: rawptr) -> i32
Msg_Func :: proc "c" (_: cstring, _: rawptr) -> i32
@(default_calling_convention = "c", link_prefix = "mdb_")
foreign lib {
@@ -700,7 +623,7 @@ foreign lib {
* </ul>
*/
@(require_results)
env_open :: proc(env: ^Env, path: cstring, flags: Env_Flags, mode: mode_t) -> Error ---
env_open :: proc(env: ^Env, path: cstring, flags: u32, mode: mode_t) -> Error ---
/** @brief Copy an LMDB environment to the specified path.
*
@@ -759,7 +682,7 @@ foreign lib {
* @return A non-zero error value on failure and 0 on success.
*/
@(require_results)
env_copy2 :: proc(env: ^Env, path: cstring, flags: Copy_Flags) -> Error ---
env_copy2 :: proc(env: ^Env, path: cstring, flags: u32) -> Error ---
/** @brief Copy an LMDB environment to the specified file descriptor,
* with options.
@@ -779,7 +702,7 @@ foreign lib {
* @return A non-zero error value on failure and 0 on success.
*/
@(require_results)
env_copyfd2 :: proc(env: ^Env, fd: filehandle_t, flags: Copy_Flags) -> Error ---
env_copyfd2 :: proc(env: ^Env, fd: filehandle_t, flags: u32) -> Error ---
/** @brief Return statistics about the LMDB environment.
*
@@ -844,7 +767,7 @@ foreign lib {
* </ul>
*/
@(require_results)
env_set_flags :: proc(env: ^Env, flags: Env_Flags, onoff: i32) -> Error ---
env_set_flags :: proc(env: ^Env, flags: u32, onoff: i32) -> Error ---
/** @brief Get environment flags.
*
@@ -857,7 +780,7 @@ foreign lib {
* </ul>
*/
@(require_results)
env_get_flags :: proc(env: ^Env, flags: ^Env_Flags) -> Error ---
env_get_flags :: proc(env: ^Env, flags: ^u32) -> Error ---
/** @brief Return the path that was used in #mdb_env_open().
*
@@ -1050,7 +973,7 @@ foreign lib {
* </ul>
*/
@(require_results)
txn_begin :: proc(env: ^Env, parent: ^Txn, flags: Env_Flags, txn: ^^Txn) -> Error ---
txn_begin :: proc(env: ^Env, parent: ^Txn, flags: u32, txn: ^^Txn) -> Error ---
/** @brief Returns the transaction's #MDB_env
*
@@ -1203,7 +1126,7 @@ foreign lib {
* </ul>
*/
@(require_results)
dbi_open :: proc(txn: ^Txn, name: cstring, flags: Db_Flags, dbi: ^Dbi) -> Error ---
dbi_open :: proc(txn: ^Txn, name: cstring, flags: u32, dbi: ^Dbi) -> Error ---
/** @brief Retrieve statistics for a database.
*
@@ -1228,7 +1151,7 @@ foreign lib {
* @return A non-zero error value on failure and 0 on success.
*/
@(require_results)
dbi_flags :: proc(txn: ^Txn, dbi: Dbi, flags: ^Db_Flags) -> Error ---
dbi_flags :: proc(txn: ^Txn, dbi: Dbi, flags: ^u32) -> Error ---
/** @brief Close a database handle. Normally unnecessary. Use with care:
*
@@ -1306,7 +1229,6 @@ foreign lib {
@(require_results)
set_dupsort :: proc(txn: ^Txn, dbi: Dbi, cmp: Cmp_Func) -> Error ---
// NOTE: Unimplemented in current LMDB — this function has no effect.
/** @brief Set a relocation function for a #MDB_FIXEDMAP database.
*
* @todo The relocation function is called whenever it is necessary to move the data
@@ -1328,7 +1250,6 @@ foreign lib {
@(require_results)
set_relfunc :: proc(txn: ^Txn, dbi: Dbi, rel: Rel_Func) -> Error ---
// NOTE: Unimplemented in current LMDB — this function has no effect.
/** @brief Set a context pointer for a #MDB_FIXEDMAP database's relocation function.
*
* See #mdb_set_relfunc and #MDB_rel_func for more details.
@@ -1423,7 +1344,7 @@ foreign lib {
* </ul>
*/
@(require_results)
put :: proc(txn: ^Txn, dbi: Dbi, key: ^Val, data: ^Val, flags: Write_Flags) -> Error ---
put :: proc(txn: ^Txn, dbi: Dbi, key: ^Val, data: ^Val, flags: u32) -> Error ---
/** @brief Delete items from a database.
*
@@ -1596,7 +1517,7 @@ foreign lib {
* </ul>
*/
@(require_results)
cursor_put :: proc(cursor: ^Cursor, key: ^Val, data: ^Val, flags: Write_Flags) -> Error ---
cursor_put :: proc(cursor: ^Cursor, key: ^Val, data: ^Val, flags: u32) -> Error ---
/** @brief Delete current key/data pair
*
@@ -1620,7 +1541,7 @@ foreign lib {
* </ul>
*/
@(require_results)
cursor_del :: proc(cursor: ^Cursor, flags: Write_Flags) -> Error ---
cursor_del :: proc(cursor: ^Cursor, flags: u32) -> Error ---
/** @brief Return count of duplicates for current key.
*