Compare commits
12 Commits
03986f88a3
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| e8ffa28de3 | |||
| 5317b8f142 | |||
| e36229a3ef | |||
| bca19277b3 | |||
| 37da2ea068 | |||
| cfd9e504e1 | |||
| 0d424cbd6e | |||
| 64de816647 | |||
| 274289bd47 | |||
| 59c600d630 | |||
| 25fca052f5 | |||
| a8f98e893f |
+83
-8
@@ -5,30 +5,105 @@
|
|||||||
{
|
{
|
||||||
"label": "Test many_bits",
|
"label": "Test many_bits",
|
||||||
"command": "odin test many_bits -out=out/debug/test_many_bits",
|
"command": "odin test many_bits -out=out/debug/test_many_bits",
|
||||||
"cwd": "$ZED_WORKTREE_ROOT"
|
"cwd": "$ZED_WORKTREE_ROOT",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Test ring",
|
"label": "Test ring",
|
||||||
"command": "odin test ring -out=out/debug/test_ring",
|
"command": "odin test ring -out=out/debug/test_ring",
|
||||||
"cwd": "$ZED_WORKTREE_ROOT"
|
"cwd": "$ZED_WORKTREE_ROOT",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Test levsort",
|
"label": "Test levsort",
|
||||||
"command": "odin test levsort -out=out/debug/test_levsort",
|
"command": "odin test levsort -out=out/debug/test_levsort",
|
||||||
"cwd": "$ZED_WORKTREE_ROOT"
|
"cwd": "$ZED_WORKTREE_ROOT",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Test levsync",
|
"label": "Test levsync",
|
||||||
"command": "odin test levsync -out=out/debug/test_levsync",
|
"command": "odin test levsync -out=out/debug/test_levsync",
|
||||||
"cwd": "$ZED_WORKTREE_ROOT"
|
"cwd": "$ZED_WORKTREE_ROOT",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Test levmath",
|
||||||
|
"command": "odin test levmath -out=out/debug/test_levmath",
|
||||||
|
"cwd": "$ZED_WORKTREE_ROOT",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Test phased_executor",
|
||||||
|
"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",
|
||||||
|
"cwd": "$ZED_WORKTREE_ROOT",
|
||||||
},
|
},
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
// ----- LMDB Examples ------------------------
|
// ----- Examples ------------------------
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
{
|
{
|
||||||
"label": "Run lmdb example",
|
"label": "Run lmdb example",
|
||||||
"command": "odin run vendor/lmdb/examples -debug -out=out/debug/lmdb-examples",
|
"command": "odin run vendor/lmdb/examples -debug -out=out/debug/lmdb-examples",
|
||||||
"cwd": "$ZED_WORKTREE_ROOT"
|
"cwd": "$ZED_WORKTREE_ROOT",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Run draw hellope-clay example",
|
||||||
|
"command": "odin run draw/examples -debug -out=out/debug/draw-examples -- hellope-clay",
|
||||||
|
"cwd": "$ZED_WORKTREE_ROOT",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Run draw hellope-shapes example",
|
||||||
|
"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 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 ------------------------
|
// ----- Other ------------------------
|
||||||
@@ -36,6 +111,6 @@
|
|||||||
{
|
{
|
||||||
"label": "Run debug",
|
"label": "Run debug",
|
||||||
"command": "odin run debug -debug -out=out/debug/debug",
|
"command": "odin run debug -debug -out=out/debug/debug",
|
||||||
"cwd": "$ZED_WORKTREE_ROOT"
|
"cwd": "$ZED_WORKTREE_ROOT",
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
# LevLib
|
# LevLib
|
||||||
|
|
||||||
Narya + BFPOWER unified Odin library collection.
|
Narya + BFPOWER unified Odin library collection.
|
||||||
|
|
||||||
|
## Meta Tools
|
||||||
|
|
||||||
|
The `meta/` package contains build tools that can be run from the project root:
|
||||||
|
|
||||||
|
```
|
||||||
|
odin run meta -- <command>
|
||||||
|
```
|
||||||
|
|
||||||
|
Running with no arguments prints available commands.
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `gen-shaders` | Compile all GLSL shaders in `draw/shaders/source/` to SPIR-V and Metal Shading Language, writing results to `draw/shaders/generated/`. Requires `glslangValidator` and `spirv-cross` on PATH. |
|
||||||
|
|||||||
+1023
File diff suppressed because it is too large
Load Diff
+1170
File diff suppressed because it is too large
Load Diff
+1590
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,756 @@
|
|||||||
|
// 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. 0–4px 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 2px
|
||||||
|
// transparent + 2px black-at-3% (TINT_SCANLINE).
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
SCANLINE_STRIPE_PX :: 2
|
||||||
|
SCANLINE_GAP_PX :: 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.
Binary file not shown.
+1333
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,179 @@
|
|||||||
|
package draw_qr
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
// Returns the number of bytes to_texture will write for the given encoded
|
||||||
|
// QR buffer. Equivalent to size*size*4 where size = qrcode.get_size(qrcode_buf).
|
||||||
|
texture_size :: #force_inline proc(qrcode_buf: []u8) -> int {
|
||||||
|
size := qrcode.get_size(qrcode_buf)
|
||||||
|
return size * 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.
|
||||||
|
//
|
||||||
|
// Returns ok=false when:
|
||||||
|
// - qrcode_buf is invalid (qrcode.get_size returns 0).
|
||||||
|
// - texture_buf is smaller than texture_size(qrcode_buf).
|
||||||
|
@(require_results)
|
||||||
|
to_texture :: proc(
|
||||||
|
qrcode_buf: []u8,
|
||||||
|
texture_buf: []u8,
|
||||||
|
dark: draw.Color = DFT_QR_DARK,
|
||||||
|
light: draw.Color = DFT_QR_LIGHT,
|
||||||
|
) -> (
|
||||||
|
desc: draw.Texture_Desc,
|
||||||
|
ok: bool,
|
||||||
|
) {
|
||||||
|
size := qrcode.get_size(qrcode_buf)
|
||||||
|
if size == 0 do return {}, false
|
||||||
|
if len(texture_buf) < size * size * 4 do return {}, false
|
||||||
|
|
||||||
|
for y in 0 ..< size {
|
||||||
|
for x in 0 ..< size {
|
||||||
|
i := (y * size + x) * 4
|
||||||
|
c := dark if qrcode.get_module(qrcode_buf, x, y) else light
|
||||||
|
texture_buf[i + 0] = c[0]
|
||||||
|
texture_buf[i + 1] = c[1]
|
||||||
|
texture_buf[i + 2] = c[2]
|
||||||
|
texture_buf[i + 3] = c[3]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return draw.Texture_Desc {
|
||||||
|
width = u32(size),
|
||||||
|
height = u32(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,
|
||||||
|
temp_allocator := context.temp_allocator,
|
||||||
|
) -> (
|
||||||
|
texture: draw.Texture_Id,
|
||||||
|
ok: bool,
|
||||||
|
) {
|
||||||
|
tex_size := texture_size(qrcode_buf)
|
||||||
|
if tex_size == 0 do return draw.INVALID_TEXTURE, false
|
||||||
|
|
||||||
|
pixels, alloc_err := make([]u8, tex_size, temp_allocator)
|
||||||
|
if alloc_err != nil do return draw.INVALID_TEXTURE, false
|
||||||
|
defer delete(pixels, temp_allocator)
|
||||||
|
|
||||||
|
desc := to_texture(qrcode_buf, pixels, dark, light) or_return
|
||||||
|
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,
|
||||||
|
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, 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,
|
||||||
|
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, temp_allocator)
|
||||||
|
}
|
||||||
|
|
||||||
|
register_texture_from :: proc {
|
||||||
|
register_texture_from_text,
|
||||||
|
register_texture_from_binary,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default fit=.Fit preserves the QR's square aspect; override as needed.
|
||||||
|
clay_image :: #force_inline proc(
|
||||||
|
texture: draw.Texture_Id,
|
||||||
|
tint: draw.Color = draw.DFT_TINT,
|
||||||
|
) -> draw.Clay_Image_Data {
|
||||||
|
return draw.clay_image_data(texture, fit = .Fit, tint = tint)
|
||||||
|
}
|
||||||
@@ -0,0 +1,409 @@
|
|||||||
|
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})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
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_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_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_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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,375 @@
|
|||||||
|
package examples
|
||||||
|
|
||||||
|
import "core:math"
|
||||||
|
import "core:os"
|
||||||
|
import sdl "vendor:sdl3"
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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)
|
||||||
|
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})
|
||||||
|
|
||||||
|
// Background
|
||||||
|
draw.rectangle(base_layer, {0, 0, 500, 500}, draw.Color{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(
|
||||||
|
base_layer,
|
||||||
|
{20, 160, 460, 60},
|
||||||
|
draw.Linear_Gradient{start_color = {255, 0, 0, 255}, end_color = {0, 0, 255, 255}, angle = 0},
|
||||||
|
)
|
||||||
|
|
||||||
|
// ----- Rotation demos -----
|
||||||
|
|
||||||
|
// 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_px = 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.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)
|
||||||
|
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(24)
|
||||||
|
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 += 0.5
|
||||||
|
base_layer := draw.begin({width = 600, height = 600})
|
||||||
|
|
||||||
|
// ----- Text API demos -----
|
||||||
|
|
||||||
|
// Cached text with id — TTF_Text reused across frames (good for text-heavy apps)
|
||||||
|
draw.text(
|
||||||
|
base_layer,
|
||||||
|
"Hellope!",
|
||||||
|
{300, 80},
|
||||||
|
PLEX_SANS_REGULAR,
|
||||||
|
FONT_SIZE,
|
||||||
|
color = draw.WHITE,
|
||||||
|
origin = draw.center_of("Hellope!", PLEX_SANS_REGULAR, FONT_SIZE),
|
||||||
|
id = HELLOPE_ID,
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
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 = 36,
|
||||||
|
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 = 500, height = 500})
|
||||||
|
clay.SetLayoutDimensions({width = base_layer.bounds.width, height = base_layer.bounds.height})
|
||||||
|
clay.BeginLayout()
|
||||||
|
if clay.UI()(
|
||||||
|
{
|
||||||
|
id = clay.ID("outer"),
|
||||||
|
layout = {
|
||||||
|
sizing = {clay.SizingGrow({}), clay.SizingGrow({})},
|
||||||
|
childAlignment = {x = .Center, y = .Center},
|
||||||
|
},
|
||||||
|
backgroundColor = {127, 127, 127, 255},
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
clay.Text("Hellope!", &text_config)
|
||||||
|
}
|
||||||
|
clay_batch := draw.ClayBatch {
|
||||||
|
bounds = base_layer.bounds,
|
||||||
|
cmds = clay.EndLayout(),
|
||||||
|
}
|
||||||
|
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},
|
||||||
|
}
|
||||||
|
gauge2 := Gauge {
|
||||||
|
value = 0.45,
|
||||||
|
color = {200, 100, 50, 255},
|
||||||
|
}
|
||||||
|
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()(
|
||||||
|
{
|
||||||
|
id = 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()({id = clay.ID("title"), layout = {sizing = {clay.SizingFit({}), clay.SizingFit({})}}}) {
|
||||||
|
clay.Text("Custom Draw Demo", &text_config)
|
||||||
|
}
|
||||||
|
|
||||||
|
if clay.UI()(
|
||||||
|
{
|
||||||
|
id = clay.ID("gauge"),
|
||||||
|
layout = {sizing = {clay.SizingFixed(300), clay.SizingFixed(30)}},
|
||||||
|
custom = {customData = &gauge},
|
||||||
|
backgroundColor = {80, 80, 80, 255},
|
||||||
|
},
|
||||||
|
) {}
|
||||||
|
|
||||||
|
if clay.UI()(
|
||||||
|
{
|
||||||
|
id = clay.ID("gauge2"),
|
||||||
|
layout = {sizing = {clay.SizingFixed(300), clay.SizingFixed(30)}},
|
||||||
|
custom = {customData = &gauge2},
|
||||||
|
backgroundColor = {80, 80, 80, 255},
|
||||||
|
},
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
clay_batch := draw.ClayBatch {
|
||||||
|
bounds = base_layer.bounds,
|
||||||
|
cmds = clay.EndLayout(),
|
||||||
|
}
|
||||||
|
draw.prepare_clay_batch(base_layer, &clay_batch, {0, 0}, custom_draw = draw_custom)
|
||||||
|
draw.end(gpu, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
Gauge :: struct {
|
||||||
|
value: f32,
|
||||||
|
color: draw.Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
draw_custom :: proc(layer: ^draw.Layer, bounds: draw.Rectangle, render_data: clay.CustomRenderData) {
|
||||||
|
gauge := cast(^Gauge)render_data.customData
|
||||||
|
|
||||||
|
border_width: f32 = 2
|
||||||
|
draw.rectangle(
|
||||||
|
layer,
|
||||||
|
bounds,
|
||||||
|
draw.color_from_clay(render_data.backgroundColor),
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,410 @@
|
|||||||
|
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)
|
||||||
|
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},
|
||||||
|
)
|
||||||
|
draw.text(
|
||||||
|
base_layer,
|
||||||
|
"QR Code",
|
||||||
|
{COL1, ROW2_Y + ITEM_SIZE + LABEL_OFFSET},
|
||||||
|
PLEX_SANS_REGULAR,
|
||||||
|
FONT_SIZE,
|
||||||
|
color = draw.WHITE,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Rounded corners
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
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
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
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
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
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
|
||||||
|
draw.circle(
|
||||||
|
base_layer,
|
||||||
|
{SHAPE_COL1 + SHAPE_SIZE / 2, ROW4_Y + SHAPE_SIZE / 2},
|
||||||
|
SHAPE_SIZE / 2,
|
||||||
|
checker_fill,
|
||||||
|
)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
#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 [[user(locn2), flat]];
|
||||||
|
float4 f_radii [[user(locn3), flat]];
|
||||||
|
float f_half_feather [[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;
|
||||||
|
float4 param_3 = in.f_radii;
|
||||||
|
float d = sdRoundedBox(param_1, param_2, param_3);
|
||||||
|
if (d > in.f_half_feather)
|
||||||
|
{
|
||||||
|
discard_fragment();
|
||||||
|
}
|
||||||
|
float grad_magnitude = fast::max(fwidth(d), 9.9999999747524270787835121154785e-07);
|
||||||
|
float d_n = d / grad_magnitude;
|
||||||
|
float h_n = in.f_half_feather / grad_magnitude;
|
||||||
|
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.
@@ -0,0 +1,123 @@
|
|||||||
|
#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;
|
||||||
|
float2 half_size;
|
||||||
|
float half_feather;
|
||||||
|
uint color;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Gaussian_Blur_Primitive_1
|
||||||
|
{
|
||||||
|
float4 bounds;
|
||||||
|
float4 radii;
|
||||||
|
float2 half_size;
|
||||||
|
float half_feather;
|
||||||
|
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 [[user(locn2)]];
|
||||||
|
float4 f_radii [[user(locn3)]];
|
||||||
|
float f_half_feather [[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 = float2(0.0);
|
||||||
|
out.f_radii = float4(0.0);
|
||||||
|
out.f_half_feather = 0.0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Gaussian_Blur_Primitive p;
|
||||||
|
p.bounds = _69.primitives[int(gl_InstanceIndex)].bounds;
|
||||||
|
p.radii = _69.primitives[int(gl_InstanceIndex)].radii;
|
||||||
|
p.half_size = _69.primitives[int(gl_InstanceIndex)].half_size;
|
||||||
|
p.half_feather = _69.primitives[int(gl_InstanceIndex)].half_feather;
|
||||||
|
p.color = _69.primitives[int(gl_InstanceIndex)].color;
|
||||||
|
float2 corner = _97[int(gl_VertexIndex)];
|
||||||
|
float2 world_pos = mix(p.bounds.xy, p.bounds.zw, corner);
|
||||||
|
float2 center = (p.bounds.xy + p.bounds.zw) * 0.5;
|
||||||
|
out.p_local = (world_pos - center) * _13.dpi_scale;
|
||||||
|
out.f_color = unpack_unorm4x8_to_float(p.color);
|
||||||
|
out.f_half_size = p.half_size;
|
||||||
|
out.f_radii = p.radii;
|
||||||
|
out.f_half_feather = p.half_feather;
|
||||||
|
out.gl_Position = _13.projection * float4(world_pos * _13.dpi_scale, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
Binary file not shown.
@@ -0,0 +1,47 @@
|
|||||||
|
#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.
@@ -0,0 +1,18 @@
|
|||||||
|
#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.
@@ -0,0 +1,242 @@
|
|||||||
|
#pragma clang diagnostic ignored "-Wmissing-prototypes"
|
||||||
|
|
||||||
|
#include <metal_stdlib>
|
||||||
|
#include <simd/simd.h>
|
||||||
|
|
||||||
|
using namespace metal;
|
||||||
|
|
||||||
|
// Implementation of the GLSL mod() function, which is slightly different than Metal fmod()
|
||||||
|
template<typename Tx, typename Ty>
|
||||||
|
inline Tx mod(Tx x, Ty y)
|
||||||
|
{
|
||||||
|
return x - y * floor(x / y);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct main0_out
|
||||||
|
{
|
||||||
|
float4 out_color [[color(0)]];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct main0_in
|
||||||
|
{
|
||||||
|
float4 f_color [[user(locn0)]];
|
||||||
|
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)]];
|
||||||
|
};
|
||||||
|
|
||||||
|
static inline __attribute__((always_inline))
|
||||||
|
float sdRoundedBox(thread const float2& p, thread const float2& b, thread const float4& r)
|
||||||
|
{
|
||||||
|
float2 _48;
|
||||||
|
if (p.x > 0.0)
|
||||||
|
{
|
||||||
|
_48 = r.xy;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_48 = r.zw;
|
||||||
|
}
|
||||||
|
float2 rxy = _48;
|
||||||
|
float _62;
|
||||||
|
if (p.y > 0.0)
|
||||||
|
{
|
||||||
|
_62 = rxy.x;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_62 = rxy.y;
|
||||||
|
}
|
||||||
|
float rr = _62;
|
||||||
|
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 sdRegularPolygon(thread const float2& p, thread const float& r, thread const float& n)
|
||||||
|
{
|
||||||
|
float an = 3.1415927410125732421875 / n;
|
||||||
|
float bn = mod(precise::atan2(p.y, p.x), 2.0 * an) - an;
|
||||||
|
return (length(p) * cos(bn)) - r;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline __attribute__((always_inline))
|
||||||
|
float sdEllipseApprox(thread const float2& p, thread const float2& ab)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
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;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
float d = 1000000015047466219876688855040.0;
|
||||||
|
float h = 0.5;
|
||||||
|
float2 half_size = in.f_params.xy;
|
||||||
|
float2 p_local = in.f_local_or_uv;
|
||||||
|
if (kind == 1u)
|
||||||
|
{
|
||||||
|
float4 corner_radii = float4(in.f_params.zw, in.f_params2.xy);
|
||||||
|
h = in.f_params2.z;
|
||||||
|
float2 param = p_local;
|
||||||
|
float2 param_1 = half_size;
|
||||||
|
float4 param_2 = corner_radii;
|
||||||
|
d = sdRoundedBox(param, param_1, param_2);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (kind == 2u)
|
||||||
|
{
|
||||||
|
float radius = in.f_params.x;
|
||||||
|
float sides = in.f_params.y;
|
||||||
|
h = in.f_params.z;
|
||||||
|
float2 param_3 = p_local;
|
||||||
|
float param_4 = radius;
|
||||||
|
float param_5 = sides;
|
||||||
|
d = sdRegularPolygon(param_3, param_4, param_5);
|
||||||
|
half_size = float2(radius);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (kind == 3u)
|
||||||
|
{
|
||||||
|
float2 ab = in.f_params.xy;
|
||||||
|
h = in.f_params.z;
|
||||||
|
float2 param_6 = p_local;
|
||||||
|
float2 param_7 = ab;
|
||||||
|
d = sdEllipseApprox(param_6, param_7);
|
||||||
|
half_size = ab;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (kind == 4u)
|
||||||
|
{
|
||||||
|
float inner = in.f_params.x;
|
||||||
|
float outer = in.f_params.y;
|
||||||
|
float2 n_start = in.f_params.zw;
|
||||||
|
float2 n_end = in.f_params2.xy;
|
||||||
|
uint arc_bits = (flags >> 5u) & 3u;
|
||||||
|
h = in.f_params2.z;
|
||||||
|
float r = length(p_local);
|
||||||
|
d = fast::max(inner - r, r - outer);
|
||||||
|
if (arc_bits != 0u)
|
||||||
|
{
|
||||||
|
float d_start = dot(p_local, n_start);
|
||||||
|
float d_end = dot(p_local, n_end);
|
||||||
|
float _338;
|
||||||
|
if (arc_bits == 1u)
|
||||||
|
{
|
||||||
|
_338 = fast::max(d_start, d_end);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_338 = fast::min(d_start, d_end);
|
||||||
|
}
|
||||||
|
float d_wedge = _338;
|
||||||
|
d = fast::max(d, d_wedge);
|
||||||
|
}
|
||||||
|
half_size = float2(outer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
float grad_magnitude = fast::max(fwidth(d), 9.9999999747524270787835121154785e-07);
|
||||||
|
d /= grad_magnitude;
|
||||||
|
h /= grad_magnitude;
|
||||||
|
float4 shape_color;
|
||||||
|
if ((flags & 2u) != 0u)
|
||||||
|
{
|
||||||
|
float4 gradient_start = in.f_color;
|
||||||
|
float4 gradient_end = unpack_unorm4x8_to_float(in.f_effects.x);
|
||||||
|
if ((flags & 4u) != 0u)
|
||||||
|
{
|
||||||
|
float t_1 = length(p_local / half_size);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
float2 direction = float2(as_type<half2>(in.f_effects.z));
|
||||||
|
float t_2 = (dot(p_local / half_size, 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if ((flags & 1u) != 0u)
|
||||||
|
{
|
||||||
|
float4 uv_rect = in.f_uv_rect;
|
||||||
|
float2 local_uv = ((p_local / half_size) * 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 alpha = sdf_alpha(param_18, param_19);
|
||||||
|
out.out_color = float4((shape_color.xyz * shape_color.w) * alpha, shape_color.w * alpha);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
Binary file not shown.
@@ -0,0 +1,110 @@
|
|||||||
|
#include <metal_stdlib>
|
||||||
|
#include <simd/simd.h>
|
||||||
|
|
||||||
|
using namespace metal;
|
||||||
|
|
||||||
|
struct Uniforms
|
||||||
|
{
|
||||||
|
float4x4 projection;
|
||||||
|
float dpi_scale;
|
||||||
|
uint mode;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Core_2D_Primitive
|
||||||
|
{
|
||||||
|
float4 bounds;
|
||||||
|
uint color;
|
||||||
|
uint flags;
|
||||||
|
uint rotation_sc;
|
||||||
|
float _pad;
|
||||||
|
float4 params;
|
||||||
|
float4 params2;
|
||||||
|
float4 uv_rect;
|
||||||
|
uint4 effects;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Core_2D_Primitive_1
|
||||||
|
{
|
||||||
|
float4 bounds;
|
||||||
|
uint color;
|
||||||
|
uint flags;
|
||||||
|
uint rotation_sc;
|
||||||
|
float _pad;
|
||||||
|
float4 params;
|
||||||
|
float4 params2;
|
||||||
|
float4 uv_rect;
|
||||||
|
uint4 effects;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Core_2D_Primitives
|
||||||
|
{
|
||||||
|
Core_2D_Primitive_1 primitives[1];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct main0_out
|
||||||
|
{
|
||||||
|
float4 f_color [[user(locn0)]];
|
||||||
|
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)]];
|
||||||
|
float4 gl_Position [[position]];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct main0_in
|
||||||
|
{
|
||||||
|
float2 v_position [[attribute(0)]];
|
||||||
|
float2 v_uv [[attribute(1)]];
|
||||||
|
float4 v_color [[attribute(2)]];
|
||||||
|
};
|
||||||
|
|
||||||
|
vertex main0_out main0(main0_in in [[stage_in]], constant Uniforms& _12 [[buffer(0)]], const device Core_2D_Primitives& _75 [[buffer(1)]], uint gl_InstanceIndex [[instance_id]])
|
||||||
|
{
|
||||||
|
main0_out out = {};
|
||||||
|
if (_12.mode == 0u)
|
||||||
|
{
|
||||||
|
out.f_color = in.v_color;
|
||||||
|
out.f_local_or_uv = in.v_uv;
|
||||||
|
out.f_params = float4(0.0);
|
||||||
|
out.f_params2 = float4(0.0);
|
||||||
|
out.f_flags = 0u;
|
||||||
|
out.f_uv_rect = float4(0.0);
|
||||||
|
out.f_effects = uint4(0u);
|
||||||
|
out.gl_Position = _12.projection * float4(in.v_position * _12.dpi_scale, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Core_2D_Primitive p;
|
||||||
|
p.bounds = _75.primitives[int(gl_InstanceIndex)].bounds;
|
||||||
|
p.color = _75.primitives[int(gl_InstanceIndex)].color;
|
||||||
|
p.flags = _75.primitives[int(gl_InstanceIndex)].flags;
|
||||||
|
p.rotation_sc = _75.primitives[int(gl_InstanceIndex)].rotation_sc;
|
||||||
|
p._pad = _75.primitives[int(gl_InstanceIndex)]._pad;
|
||||||
|
p.params = _75.primitives[int(gl_InstanceIndex)].params;
|
||||||
|
p.params2 = _75.primitives[int(gl_InstanceIndex)].params2;
|
||||||
|
p.uv_rect = _75.primitives[int(gl_InstanceIndex)].uv_rect;
|
||||||
|
p.effects = _75.primitives[int(gl_InstanceIndex)].effects;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
Binary file not shown.
@@ -0,0 +1,155 @@
|
|||||||
|
#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;
|
||||||
|
layout(location = 3) flat in vec4 f_radii;
|
||||||
|
layout(location = 4) flat in float f_half_feather;
|
||||||
|
|
||||||
|
// --- 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, f_radii);
|
||||||
|
if (d > f_half_feather) {
|
||||||
|
discard;
|
||||||
|
}
|
||||||
|
|
||||||
|
// fwidth-based normalization for AA (matches main pipeline approach).
|
||||||
|
float grad_magnitude = max(fwidth(d), 1e-6);
|
||||||
|
float d_n = d / grad_magnitude;
|
||||||
|
float h_n = f_half_feather / grad_magnitude;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
#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: RRect half extents in physical pixels (mode 1 only).
|
||||||
|
layout(location = 2) flat out vec2 f_half_size;
|
||||||
|
// f_radii: per-corner radii in physical pixels (mode 1 only).
|
||||||
|
layout(location = 3) flat out vec4 f_radii;
|
||||||
|
// f_half_feather: SDF anti-aliasing feather (mode 1 only).
|
||||||
|
layout(location = 4) flat out float f_half_feather;
|
||||||
|
|
||||||
|
// --- 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)
|
||||||
|
vec4 radii; // 16-31: per-corner radii (physical px)
|
||||||
|
vec2 half_size; // 32-39: RRect half extents (physical px)
|
||||||
|
float half_feather; // 40-43: SDF anti-aliasing feather (physical px)
|
||||||
|
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 = vec2(0.0);
|
||||||
|
f_radii = vec4(0.0);
|
||||||
|
f_half_feather = 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 = p.half_size;
|
||||||
|
f_radii = p.radii;
|
||||||
|
f_half_feather = p.half_feather;
|
||||||
|
|
||||||
|
gl_Position = projection * vec4(world_pos * dpi_scale, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
#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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
#version 450 core
|
||||||
|
|
||||||
|
// --- Inputs from vertex shader ---
|
||||||
|
layout(location = 0) in mediump 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;
|
||||||
|
|
||||||
|
// --- Output ---
|
||||||
|
layout(location = 0) out vec4 out_color;
|
||||||
|
|
||||||
|
// --- Texture sampler (for tessellated/text path) ---
|
||||||
|
layout(set = 2, binding = 0) uniform sampler2D tex;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// SDF helper functions (Inigo Quilez)
|
||||||
|
// All operate in physical pixel space — no dpi_scale needed here.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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_px * 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// main
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
uint kind = f_flags & 0xFFu;
|
||||||
|
uint flags = (f_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.
|
||||||
|
if (kind == 0u) {
|
||||||
|
vec4 t = texture(tex, f_local_or_uv);
|
||||||
|
t.rgb *= t.a;
|
||||||
|
out_color = f_color * t;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SDF path — dispatch on kind
|
||||||
|
float d = 1e30;
|
||||||
|
float h = 0.5; // half-feather width; overwritten per shape kind
|
||||||
|
vec2 half_size = f_params.xy; // used by RRect and as reference size for gradients
|
||||||
|
|
||||||
|
vec2 p_local = f_local_or_uv; // arrives rotated; vertex shader handled .Rotated
|
||||||
|
|
||||||
|
if (kind == 1u) {
|
||||||
|
// RRect — half_feather in params2.z
|
||||||
|
vec4 corner_radii = vec4(f_params.zw, f_params2.xy);
|
||||||
|
h = f_params2.z;
|
||||||
|
d = sdRoundedBox(p_local, half_size, corner_radii);
|
||||||
|
}
|
||||||
|
else if (kind == 2u) {
|
||||||
|
// NGon — half_feather in params.z
|
||||||
|
float radius = f_params.x;
|
||||||
|
float sides = f_params.y;
|
||||||
|
h = f_params.z;
|
||||||
|
d = sdRegularPolygon(p_local, radius, sides);
|
||||||
|
half_size = vec2(radius); // for gradient UV computation
|
||||||
|
}
|
||||||
|
else if (kind == 3u) {
|
||||||
|
// Ellipse — half_feather in params.z
|
||||||
|
vec2 ab = f_params.xy;
|
||||||
|
h = f_params.z;
|
||||||
|
d = sdEllipseApprox(p_local, ab);
|
||||||
|
half_size = ab; // for gradient UV computation
|
||||||
|
}
|
||||||
|
else if (kind == 4u) {
|
||||||
|
// Ring_Arc — half_feather in params2.z
|
||||||
|
// Arc mode from flag bits 5-6: 0 = full, 1 = narrow (≤π), 2 = wide (>π)
|
||||||
|
float inner = f_params.x;
|
||||||
|
float outer = f_params.y;
|
||||||
|
vec2 n_start = f_params.zw;
|
||||||
|
vec2 n_end = f_params2.xy;
|
||||||
|
uint arc_bits = (flags >> 5u) & 3u;
|
||||||
|
|
||||||
|
h = f_params2.z;
|
||||||
|
|
||||||
|
float r = length(p_local);
|
||||||
|
d = max(inner - r, r - outer);
|
||||||
|
|
||||||
|
if (arc_bits != 0u) {
|
||||||
|
float d_start = dot(p_local, n_start);
|
||||||
|
float d_end = dot(p_local, n_end);
|
||||||
|
float d_wedge = (arc_bits == 1u)
|
||||||
|
? max(d_start, d_end) // arc ≤ π: intersect half-planes
|
||||||
|
: min(d_start, d_end); // arc > π: union half-planes
|
||||||
|
d = max(d, d_wedge);
|
||||||
|
}
|
||||||
|
|
||||||
|
half_size = vec2(outer); // for gradient UV computation
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 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 / half_size);
|
||||||
|
shape_color = gradient_2color(gradient_start, gradient_end, t);
|
||||||
|
} else {
|
||||||
|
// Linear gradient: direction pre-computed on CPU as (cos, sin) f16 pair
|
||||||
|
vec2 direction = unpackHalf2x16(f_effects.z);
|
||||||
|
mediump float t = dot(p_local / half_size, direction) * 0.5 + 0.5;
|
||||||
|
shape_color = gradient_2color(gradient_start, gradient_end, t);
|
||||||
|
}
|
||||||
|
} else if ((flags & 1u) != 0u) {
|
||||||
|
// Textured (bit 0)
|
||||||
|
vec4 uv_rect = f_uv_rect;
|
||||||
|
vec2 local_uv = p_local / half_size * 0.5 + 0.5;
|
||||||
|
vec2 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
#version 450 core
|
||||||
|
|
||||||
|
// ---------- 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 = 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;
|
||||||
|
|
||||||
|
// ---------- Uniforms (single block — avoids spirv-cross reordering on Metal) ----------
|
||||||
|
layout(set = 1, binding = 0) uniform Uniforms {
|
||||||
|
mat4 projection;
|
||||||
|
float dpi_scale;
|
||||||
|
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)
|
||||||
|
};
|
||||||
|
|
||||||
|
layout(std430, set = 0, binding = 0) readonly buffer Core_2D_Primitives {
|
||||||
|
Core_2D_Primitive primitives[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------- Entry point ----------
|
||||||
|
void main() {
|
||||||
|
if (mode == 0u) {
|
||||||
|
// ---- Mode 0: Tessellated (used for text and arbitrary user geometry) ----
|
||||||
|
f_color = v_color;
|
||||||
|
f_local_or_uv = v_uv;
|
||||||
|
f_params = vec4(0.0);
|
||||||
|
f_params2 = vec4(0.0);
|
||||||
|
f_flags = 0u;
|
||||||
|
f_uv_rect = vec4(0.0);
|
||||||
|
f_effects = uvec4(0);
|
||||||
|
|
||||||
|
gl_Position = projection * vec4(v_position * dpi_scale, 0.0, 1.0);
|
||||||
|
} else {
|
||||||
|
// ---- Mode 1: SDF instanced quads ----
|
||||||
|
Core_2D_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_params = p.params;
|
||||||
|
f_params2 = p.params2;
|
||||||
|
f_flags = p.flags;
|
||||||
|
f_uv_rect = p.uv_rect;
|
||||||
|
f_effects = p.effects;
|
||||||
|
|
||||||
|
gl_Position = projection * vec4(world_pos * dpi_scale, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,339 @@
|
|||||||
|
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 -----
|
||||||
|
|
||||||
|
// Color is premultiplied: the tessellated fragment shader passes it through directly
|
||||||
|
// and the blend state is ONE, ONE_MINUS_SRC_ALPHA.
|
||||||
|
//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.
|
||||||
|
// Interior vertices get the full premultiplied color; outer fringe vertices get BLANK (0,0,0,0).
|
||||||
|
// The rasterizer linearly interpolates between them, producing a smooth 1-pixel AA band.
|
||||||
|
// `aa_px` controls the extrusion width in logical pixels (default 1.0).
|
||||||
|
// This proc emits 21 vertices (3 interior + 6 edge quads × 3 verts each).
|
||||||
|
triangle_aa :: proc(
|
||||||
|
layer: ^draw.Layer,
|
||||||
|
v1, v2, v3: draw.Vec2,
|
||||||
|
color: draw.Color,
|
||||||
|
aa_px: f32 = draw.DFT_FEATHER_PX,
|
||||||
|
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)
|
||||||
|
|
||||||
|
extrude_distance := aa_px * 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 × 3 edge-quad = 21 vertices
|
||||||
|
vertices: [21]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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
+322
@@ -0,0 +1,322 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch SDL TTF font pointer for rendering.
|
||||||
|
//INTERNAL
|
||||||
|
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}
|
||||||
|
font := GLOB.text_cache.sdl_fonts[key]
|
||||||
|
|
||||||
|
if font == nil {
|
||||||
|
log.debug("Font with id:", id, "and size:", size, "not found. Adding..")
|
||||||
|
|
||||||
|
font_bytes := GLOB.text_cache.font_bytes[id]
|
||||||
|
if font_bytes == nil {
|
||||||
|
log.panicf("Font must first be registered with register_font before using (id=%d)", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
font_io := sdl.IOFromConstMem(raw_data(font_bytes[:]), len(font_bytes))
|
||||||
|
if font_io == nil {
|
||||||
|
log.panicf("Failed to create IOStream for font id=%d: %s", id, sdl.GetError())
|
||||||
|
}
|
||||||
|
|
||||||
|
sdl_font := sdl_ttf.OpenFontIO(font_io, true, f32(size))
|
||||||
|
if sdl_font == nil {
|
||||||
|
log.panicf("Failed to create SDL font for font id=%d size=%d: %s", id, size, sdl.GetError())
|
||||||
|
}
|
||||||
|
|
||||||
|
if !sdl_ttf.SetFontSizeDPI(sdl_font, f32(size), 72 * i32(GLOB.dpi_scaling), 72 * i32(GLOB.dpi_scaling)) {
|
||||||
|
log.panicf("Failed to set font DPI for font id=%d size=%d: %s", id, size, sdl.GetError())
|
||||||
|
}
|
||||||
|
|
||||||
|
GLOB.text_cache.sdl_fonts[key] = sdl_font
|
||||||
|
return sdl_font
|
||||||
|
} else {
|
||||||
|
return font
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns `false` if there are more than max(u16) fonts
|
||||||
|
register_font :: proc(bytes: []u8) -> (id: Font_Id, ok: bool) #optional_ok {
|
||||||
|
if GLOB.text_cache.engine == nil {
|
||||||
|
log.panicf("Cannot register font: text system not initialized. Call init() first.")
|
||||||
|
}
|
||||||
|
if len(GLOB.text_cache.font_bytes) > int(max(Font_Id)) do return 0, false
|
||||||
|
|
||||||
|
log.debug("Registering font...")
|
||||||
|
append(&GLOB.text_cache.font_bytes, bytes)
|
||||||
|
return Font_Id(len(GLOB.text_cache.font_bytes) - 1), true
|
||||||
|
}
|
||||||
|
|
||||||
|
//INTERNAL
|
||||||
|
Text :: struct {
|
||||||
|
sdl_text: ^sdl_ttf.Text,
|
||||||
|
position: Vec2,
|
||||||
|
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)
|
||||||
|
if sdl_text == nil {
|
||||||
|
log.panicf("Failed to create SDL text: %s", sdl.GetError())
|
||||||
|
}
|
||||||
|
GLOB.text_cache.cache[key] = sdl_text
|
||||||
|
return sdl_text
|
||||||
|
} else {
|
||||||
|
if !sdl_ttf.SetTextString(existing, c_str, 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
// ----- 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)
|
||||||
|
init_text_cache :: proc(
|
||||||
|
device: ^sdl.GPUDevice,
|
||||||
|
allocator := context.allocator,
|
||||||
|
) -> (
|
||||||
|
text_cache: Text_Cache,
|
||||||
|
ok: bool,
|
||||||
|
) {
|
||||||
|
log.debug("Initializing text state")
|
||||||
|
if !sdl_ttf.Init() {
|
||||||
|
log.errorf("Failed to initialize SDL_ttf: %s", sdl.GetError())
|
||||||
|
return text_cache, false
|
||||||
|
}
|
||||||
|
|
||||||
|
engine := sdl_ttf.CreateGPUTextEngine(device)
|
||||||
|
if engine == nil {
|
||||||
|
log.errorf("Failed to create GPU text engine: %s", sdl.GetError())
|
||||||
|
sdl_ttf.Quit()
|
||||||
|
return text_cache, false
|
||||||
|
}
|
||||||
|
sdl_ttf.SetGPUTextEngineWinding(engine, .COUNTER_CLOCKWISE)
|
||||||
|
|
||||||
|
text_cache = Text_Cache {
|
||||||
|
engine = engine,
|
||||||
|
cache = make(map[Cache_Key]^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)
|
||||||
|
sdl_ttf.DestroyGPUTextEngine(GLOB.text_cache.engine)
|
||||||
|
sdl_ttf.Quit()
|
||||||
|
}
|
||||||
@@ -0,0 +1,413 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
Clay_Image_Data :: struct {
|
||||||
|
texture_id: Texture_Id,
|
||||||
|
fit: Fit_Mode,
|
||||||
|
tint: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
clay_image_data :: proc(id: Texture_Id, fit: Fit_Mode = .Stretch, tint: Color = WHITE) -> Clay_Image_Data {
|
||||||
|
return {texture_id = id, fit = fit, tint = tint}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
// ----- Registration -------------
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@ package levmath
|
|||||||
|
|
||||||
import "base:intrinsics"
|
import "base:intrinsics"
|
||||||
import "core:math"
|
import "core:math"
|
||||||
import "core:testing"
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
// ----- Fast Exp (Schraudolph IEEE 754 bit trick) ---------------------------------------------------------------------
|
// ----- Fast Exp (Schraudolph IEEE 754 bit trick) ---------------------------------------------------------------------
|
||||||
@@ -77,6 +76,7 @@ fast_exp :: #force_inline proc "contextless" (x: $FLOAT) -> FLOAT where intrinsi
|
|||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
// ----- Testing -------------------------------------------------------------------------------------------------------
|
// ----- Testing -------------------------------------------------------------------------------------------------------
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
import "core:testing"
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_fast_exp_identity :: proc(t: ^testing.T) {
|
test_fast_exp_identity :: proc(t: ^testing.T) {
|
||||||
|
|||||||
@@ -124,6 +124,14 @@ spinlock_unlock :: #force_inline proc "contextless" (lock: ^Spinlock) {
|
|||||||
intrinsics.atomic_store_explicit(lock, false, .Release)
|
intrinsics.atomic_store_explicit(lock, false, .Release)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try_lock :: proc {
|
||||||
|
spinlock_try_lock,
|
||||||
|
}
|
||||||
|
|
||||||
|
unlock :: proc {
|
||||||
|
spinlock_unlock,
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
// ----- Tests ------------------------
|
// ----- Tests ------------------------
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|||||||
+34
-28
@@ -2,6 +2,7 @@ package many_bits
|
|||||||
|
|
||||||
import "base:builtin"
|
import "base:builtin"
|
||||||
import "base:intrinsics"
|
import "base:intrinsics"
|
||||||
|
import "base:runtime"
|
||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
import "core:slice"
|
import "core:slice"
|
||||||
|
|
||||||
@@ -25,15 +26,20 @@ Bits :: struct {
|
|||||||
length: int, // Total number of bits being stored
|
length: int, // Total number of bits being stored
|
||||||
}
|
}
|
||||||
|
|
||||||
delete :: proc(using bits: Bits, allocator := context.allocator) {
|
destroy :: proc(bits: Bits, allocator := context.allocator) -> runtime.Allocator_Error {
|
||||||
delete_slice(int_array, allocator)
|
return delete_slice(bits.int_array, allocator)
|
||||||
}
|
}
|
||||||
|
|
||||||
make :: proc(#any_int length: int, allocator := context.allocator) -> Bits {
|
create :: proc(
|
||||||
return Bits {
|
#any_int length: int,
|
||||||
int_array = make_slice([]Int_Bits, ((length - 1) >> INDEX_SHIFT) + 1, allocator),
|
allocator := context.allocator,
|
||||||
length = length,
|
) -> (
|
||||||
}
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sets all bits to 0 (false)
|
// Sets all bits to 0 (false)
|
||||||
@@ -507,8 +513,8 @@ import "core:testing"
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_set :: proc(t: ^testing.T) {
|
test_set :: proc(t: ^testing.T) {
|
||||||
bits := make(128)
|
bits := create(128)
|
||||||
defer delete(bits)
|
defer destroy(bits)
|
||||||
|
|
||||||
set(bits, 0, true)
|
set(bits, 0, true)
|
||||||
testing.expect_value(t, bits.int_array[0], Int_Bits{0})
|
testing.expect_value(t, bits.int_array[0], Int_Bits{0})
|
||||||
@@ -524,8 +530,8 @@ test_set :: proc(t: ^testing.T) {
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_get :: proc(t: ^testing.T) {
|
test_get :: proc(t: ^testing.T) {
|
||||||
bits := make(128)
|
bits := create(128)
|
||||||
defer delete(bits)
|
defer destroy(bits)
|
||||||
|
|
||||||
// Default is false
|
// Default is false
|
||||||
testing.expect(t, !get(bits, 0))
|
testing.expect(t, !get(bits, 0))
|
||||||
@@ -560,8 +566,8 @@ test_get :: proc(t: ^testing.T) {
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_set_true_set_false :: proc(t: ^testing.T) {
|
test_set_true_set_false :: proc(t: ^testing.T) {
|
||||||
bits := make(128)
|
bits := create(128)
|
||||||
defer delete(bits)
|
defer destroy(bits)
|
||||||
|
|
||||||
// set_true within first uint
|
// set_true within first uint
|
||||||
set_true(bits, 0)
|
set_true(bits, 0)
|
||||||
@@ -605,8 +611,8 @@ all_true_test :: proc(t: ^testing.T) {
|
|||||||
uint_max := UINT_MAX
|
uint_max := UINT_MAX
|
||||||
all_ones := transmute(Int_Bits)uint_max
|
all_ones := transmute(Int_Bits)uint_max
|
||||||
|
|
||||||
bits := make(132)
|
bits := create(132)
|
||||||
defer delete(bits)
|
defer destroy(bits)
|
||||||
|
|
||||||
bits.int_array[0] = all_ones
|
bits.int_array[0] = all_ones
|
||||||
bits.int_array[1] = all_ones
|
bits.int_array[1] = all_ones
|
||||||
@@ -616,8 +622,8 @@ all_true_test :: proc(t: ^testing.T) {
|
|||||||
bits.int_array[2] = {0, 1, 2}
|
bits.int_array[2] = {0, 1, 2}
|
||||||
testing.expect(t, !all_true(bits))
|
testing.expect(t, !all_true(bits))
|
||||||
|
|
||||||
bits2 := make(1)
|
bits2 := create(1)
|
||||||
defer delete(bits2)
|
defer destroy(bits2)
|
||||||
|
|
||||||
bits2.int_array[0] = {0}
|
bits2.int_array[0] = {0}
|
||||||
testing.expect(t, all_true(bits2))
|
testing.expect(t, all_true(bits2))
|
||||||
@@ -628,8 +634,8 @@ test_range_true :: proc(t: ^testing.T) {
|
|||||||
uint_max := UINT_MAX
|
uint_max := UINT_MAX
|
||||||
all_ones := transmute(Int_Bits)uint_max
|
all_ones := transmute(Int_Bits)uint_max
|
||||||
|
|
||||||
bits := make(192)
|
bits := create(192)
|
||||||
defer delete(bits)
|
defer destroy(bits)
|
||||||
|
|
||||||
// Empty range is vacuously true
|
// Empty range is vacuously true
|
||||||
testing.expect(t, range_true(bits, 0, 0))
|
testing.expect(t, range_true(bits, 0, 0))
|
||||||
@@ -676,7 +682,7 @@ test_range_true :: proc(t: ^testing.T) {
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
nearest_true_handles_same_word_and_boundaries :: proc(t: ^testing.T) {
|
nearest_true_handles_same_word_and_boundaries :: proc(t: ^testing.T) {
|
||||||
bits := make(128, context.temp_allocator)
|
bits := create(128, context.temp_allocator)
|
||||||
|
|
||||||
set_true(bits, 0)
|
set_true(bits, 0)
|
||||||
set_true(bits, 10)
|
set_true(bits, 10)
|
||||||
@@ -710,7 +716,7 @@ nearest_true_handles_same_word_and_boundaries :: proc(t: ^testing.T) {
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
nearest_false_handles_same_word_and_boundaries :: proc(t: ^testing.T) {
|
nearest_false_handles_same_word_and_boundaries :: proc(t: ^testing.T) {
|
||||||
bits := make(128, context.temp_allocator)
|
bits := create(128, context.temp_allocator)
|
||||||
|
|
||||||
// Start with all bits true, then clear a few to false.
|
// Start with all bits true, then clear a few to false.
|
||||||
for i := 0; i < bits.length; i += 1 {
|
for i := 0; i < bits.length; i += 1 {
|
||||||
@@ -749,7 +755,7 @@ nearest_false_handles_same_word_and_boundaries :: proc(t: ^testing.T) {
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
nearest_false_scans_across_words_and_returns_false_when_all_true :: proc(t: ^testing.T) {
|
nearest_false_scans_across_words_and_returns_false_when_all_true :: proc(t: ^testing.T) {
|
||||||
bits := make(192, context.temp_allocator)
|
bits := create(192, context.temp_allocator)
|
||||||
|
|
||||||
// Start with all bits true, then clear a couple far apart.
|
// Start with all bits true, then clear a couple far apart.
|
||||||
for i := 0; i < bits.length; i += 1 {
|
for i := 0; i < bits.length; i += 1 {
|
||||||
@@ -773,7 +779,7 @@ nearest_false_scans_across_words_and_returns_false_when_all_true :: proc(t: ^tes
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
nearest_true_scans_across_words_and_returns_false_when_empty :: proc(t: ^testing.T) {
|
nearest_true_scans_across_words_and_returns_false_when_empty :: proc(t: ^testing.T) {
|
||||||
bits := make(192, context.temp_allocator)
|
bits := create(192, context.temp_allocator)
|
||||||
|
|
||||||
set_true(bits, 5)
|
set_true(bits, 5)
|
||||||
set_true(bits, 130)
|
set_true(bits, 130)
|
||||||
@@ -790,7 +796,7 @@ nearest_true_scans_across_words_and_returns_false_when_empty :: proc(t: ^testing
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
nearest_false_handles_last_word_partial_length :: proc(t: ^testing.T) {
|
nearest_false_handles_last_word_partial_length :: proc(t: ^testing.T) {
|
||||||
bits := make(130, context.temp_allocator)
|
bits := create(130, context.temp_allocator)
|
||||||
|
|
||||||
// Start with all bits true, then clear the first and last valid bits.
|
// Start with all bits true, then clear the first and last valid bits.
|
||||||
for i := 0; i < bits.length; i += 1 {
|
for i := 0; i < bits.length; i += 1 {
|
||||||
@@ -811,7 +817,7 @@ nearest_false_handles_last_word_partial_length :: proc(t: ^testing.T) {
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
nearest_true_handles_last_word_partial_length :: proc(t: ^testing.T) {
|
nearest_true_handles_last_word_partial_length :: proc(t: ^testing.T) {
|
||||||
bits := make(130, context.temp_allocator)
|
bits := create(130, context.temp_allocator)
|
||||||
|
|
||||||
set_true(bits, 0)
|
set_true(bits, 0)
|
||||||
set_true(bits, 129)
|
set_true(bits, 129)
|
||||||
@@ -828,7 +834,7 @@ nearest_true_handles_last_word_partial_length :: proc(t: ^testing.T) {
|
|||||||
@(test)
|
@(test)
|
||||||
iterator_basic_mixed_bits :: proc(t: ^testing.T) {
|
iterator_basic_mixed_bits :: proc(t: ^testing.T) {
|
||||||
// Use non-word-aligned length to test partial last word handling
|
// Use non-word-aligned length to test partial last word handling
|
||||||
bits := make(100, context.temp_allocator)
|
bits := create(100, context.temp_allocator)
|
||||||
|
|
||||||
// Set specific bits: 0, 3, 64, 99 (last valid index)
|
// Set specific bits: 0, 3, 64, 99 (last valid index)
|
||||||
set_true(bits, 0)
|
set_true(bits, 0)
|
||||||
@@ -903,7 +909,7 @@ iterator_basic_mixed_bits :: proc(t: ^testing.T) {
|
|||||||
@(test)
|
@(test)
|
||||||
iterator_all_false_bits :: proc(t: ^testing.T) {
|
iterator_all_false_bits :: proc(t: ^testing.T) {
|
||||||
// Use non-word-aligned length
|
// Use non-word-aligned length
|
||||||
bits := make(100, context.temp_allocator)
|
bits := create(100, context.temp_allocator)
|
||||||
// All bits default to false, no need to set anything
|
// All bits default to false, no need to set anything
|
||||||
|
|
||||||
// Test iterate - should return all 100 bits as false
|
// Test iterate - should return all 100 bits as false
|
||||||
@@ -944,7 +950,7 @@ iterator_all_false_bits :: proc(t: ^testing.T) {
|
|||||||
@(test)
|
@(test)
|
||||||
iterator_all_true_bits :: proc(t: ^testing.T) {
|
iterator_all_true_bits :: proc(t: ^testing.T) {
|
||||||
// Use non-word-aligned length
|
// Use non-word-aligned length
|
||||||
bits := make(100, context.temp_allocator)
|
bits := create(100, context.temp_allocator)
|
||||||
// Set all bits to true
|
// Set all bits to true
|
||||||
for i := 0; i < bits.length; i += 1 {
|
for i := 0; i < bits.length; i += 1 {
|
||||||
set_true(bits, i)
|
set_true(bits, i)
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
package meta
|
||||||
|
|
||||||
|
import "core:fmt"
|
||||||
|
import "core:os"
|
||||||
|
import "core:strings"
|
||||||
|
|
||||||
|
// Compiles all GLSL shaders in source_dir to both SPIR-V (.spv) and
|
||||||
|
// Metal Shading Language (.metal), writing results to generated_dir.
|
||||||
|
// Overwrites any previously generated files with matching names.
|
||||||
|
// Requires `glslangValidator` and `spirv-cross` on PATH.
|
||||||
|
gen_shaders :: proc(source_dir, generated_dir: string) -> (success: bool) {
|
||||||
|
if !verify_shader_tool("glslangValidator") do return false
|
||||||
|
if !verify_shader_tool("spirv-cross") do return false
|
||||||
|
|
||||||
|
source_entries, read_err := os.read_all_directory_by_path(source_dir, context.temp_allocator)
|
||||||
|
if read_err != nil {
|
||||||
|
fmt.eprintfln("Failed to read shader source directory '%s': %v", source_dir, read_err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
shader_names := make([dynamic]string, len = 0, cap = 24, allocator = context.temp_allocator)
|
||||||
|
|
||||||
|
for entry in source_entries {
|
||||||
|
if strings.has_suffix(entry.name, ".vert") || strings.has_suffix(entry.name, ".frag") {
|
||||||
|
append(&shader_names, entry.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(shader_names) == 0 {
|
||||||
|
fmt.eprintfln("No shader source files (.vert, .frag) found in '%s'.", source_dir)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if os.exists(generated_dir) {
|
||||||
|
rmdir_err := os.remove_all(generated_dir)
|
||||||
|
if rmdir_err != nil {
|
||||||
|
fmt.eprintfln("Failed to remove old output directory '%s': %v", generated_dir, rmdir_err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mkdir_err := os.mkdir(generated_dir)
|
||||||
|
if mkdir_err != nil {
|
||||||
|
fmt.eprintfln("Failed to create output directory '%s': %v", generated_dir, mkdir_err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
compiled_count := 0
|
||||||
|
for shader_name in shader_names {
|
||||||
|
source_path := fmt.tprintf("%s/%s", source_dir, shader_name)
|
||||||
|
spv_path := fmt.tprintf("%s/%s.spv", generated_dir, shader_name)
|
||||||
|
metal_path := fmt.tprintf("%s/%s.metal", generated_dir, shader_name)
|
||||||
|
|
||||||
|
fmt.printfln("[GLSL -> SPIR-V] %s", shader_name)
|
||||||
|
if !compile_glsl_to_spirv(source_path, spv_path) do continue
|
||||||
|
|
||||||
|
fmt.printfln("[SPIR-V -> MSL] %s", shader_name)
|
||||||
|
if !compile_spirv_to_msl(spv_path, metal_path) do continue
|
||||||
|
|
||||||
|
compiled_count += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
total := len(shader_names)
|
||||||
|
if compiled_count == total {
|
||||||
|
fmt.printfln("Successfully compiled all %d shaders.", total)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.eprintfln("%d of %d shaders failed to compile.", total - compiled_count, total)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
verify_shader_tool :: proc(tool_name: string) -> bool {
|
||||||
|
_, _, _, err := os.process_exec(
|
||||||
|
os.Process_Desc{command = []string{tool_name, "--version"}},
|
||||||
|
context.temp_allocator,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.eprintfln("Required tool '%s' not found on PATH.", tool_name)
|
||||||
|
if tool_name == "glslangValidator" {
|
||||||
|
fmt.eprintln("\tInstall the Vulkan SDK or the glslang package:")
|
||||||
|
fmt.eprintln("\t macOS: brew install glslang")
|
||||||
|
fmt.eprintln("\t Arch: sudo pacman -S glslang")
|
||||||
|
fmt.eprintln("\t Debian: sudo apt install glslang-tools")
|
||||||
|
} else if tool_name == "spirv-cross" {
|
||||||
|
fmt.eprintln("\tInstall SPIRV-Cross:")
|
||||||
|
fmt.eprintln("\t macOS: brew install spirv-cross")
|
||||||
|
fmt.eprintln("\t Arch: sudo pacman -S spirv-cross")
|
||||||
|
fmt.eprintln("\t Debian: sudo apt install spirv-cross")
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
compile_glsl_to_spirv :: proc(source_path, output_path: string) -> bool {
|
||||||
|
state, stdout_bytes, stderr_bytes, err := os.process_exec(
|
||||||
|
os.Process_Desc{command = []string{"glslangValidator", "-V", source_path, "-o", output_path}},
|
||||||
|
context.temp_allocator,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.eprintfln("\tFailed to run glslangValidator for '%s': %v", source_path, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !state.success {
|
||||||
|
fmt.eprintfln("\tglslangValidator failed for '%s' (exit code %d):", source_path, state.exit_code)
|
||||||
|
print_tool_output(stdout_bytes, stderr_bytes)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
compile_spirv_to_msl :: proc(spv_path, output_path: string) -> bool {
|
||||||
|
state, stdout_bytes, stderr_bytes, err := os.process_exec(
|
||||||
|
os.Process_Desc{command = []string{"spirv-cross", "--msl", spv_path, "--output", output_path}},
|
||||||
|
context.temp_allocator,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.eprintfln("\tFailed to run spirv-cross for '%s': %v", spv_path, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !state.success {
|
||||||
|
fmt.eprintfln("\tspirv-cross failed for '%s' (exit code %d):", spv_path, state.exit_code)
|
||||||
|
print_tool_output(stdout_bytes, stderr_bytes)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
print_tool_output :: proc(stdout_bytes, stderr_bytes: []u8) {
|
||||||
|
stderr_text := strings.trim_right_space(transmute(string)stderr_bytes)
|
||||||
|
stdout_text := strings.trim_right_space(transmute(string)stdout_bytes)
|
||||||
|
|
||||||
|
if len(stderr_text) > 0 do fmt.eprintfln("\t%s", stderr_text)
|
||||||
|
if len(stdout_text) > 0 do fmt.eprintfln("\t%s", stdout_text)
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package meta
|
||||||
|
|
||||||
|
import "core:fmt"
|
||||||
|
import "core:log"
|
||||||
|
import "core:mem"
|
||||||
|
import "core:os"
|
||||||
|
|
||||||
|
Command :: struct {
|
||||||
|
name: string,
|
||||||
|
description: string,
|
||||||
|
run: proc() -> bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
COMMANDS :: []Command {
|
||||||
|
{
|
||||||
|
name = "gen-shaders",
|
||||||
|
description = "Compile GLSL shaders to SPIR-V and Metal Shading Language.",
|
||||||
|
run = proc() -> bool {
|
||||||
|
return gen_shaders("draw/shaders/source", "draw/shaders/generated")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
print_usage()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
command_name := args[0]
|
||||||
|
for command in COMMANDS {
|
||||||
|
if command.name == command_name {
|
||||||
|
if !command.run() do os.exit(1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.eprintfln("Unknown command '%s'.", command_name)
|
||||||
|
fmt.eprintln()
|
||||||
|
print_usage()
|
||||||
|
os.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
print_usage :: proc() {
|
||||||
|
fmt.eprintln("Usage: meta <command>")
|
||||||
|
fmt.eprintln()
|
||||||
|
fmt.eprintln("Commands:")
|
||||||
|
for command in COMMANDS {
|
||||||
|
fmt.eprintfln(" %-20s %s", command.name, command.description)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,289 @@
|
|||||||
|
// Executor for fan out/in phases where each phase must enitrely finish before the next begins.
|
||||||
|
// This executor does not gaurentee strict ordering of commands withing a phase so it is only suited
|
||||||
|
// to tasks where order within a phase is not critical.
|
||||||
|
package phased_executor
|
||||||
|
|
||||||
|
import "base:intrinsics"
|
||||||
|
import "base:runtime"
|
||||||
|
import que "core:container/queue"
|
||||||
|
import "core:prof/spall"
|
||||||
|
import "core:sync"
|
||||||
|
import "core:thread"
|
||||||
|
|
||||||
|
import b "../basic"
|
||||||
|
import "../levsync"
|
||||||
|
|
||||||
|
DEFT_BATCH_SIZE :: 1024 // Number of nodes in each batch
|
||||||
|
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),
|
||||||
|
spin: bool,
|
||||||
|
lock: levsync.Spinlock,
|
||||||
|
_pad: [64 - size_of(uint)]u8, // We want join_count to have its own cache line
|
||||||
|
join_count: uint, // Number of commands completed since last exec_join
|
||||||
|
}
|
||||||
|
|
||||||
|
// `nil` for type `T` is reserved for executor shutdown. If you need `nil` for something else wrap `T`
|
||||||
|
// in a union even if there is only a single type.
|
||||||
|
//
|
||||||
|
// Executor is not thread safe and can only be used from a single thread at a time.
|
||||||
|
// Executor can only handle 1 graph at a time.
|
||||||
|
// To execute multiple graphs at the same time, use multiple executors.
|
||||||
|
Executor :: struct($T: typeid) where intrinsics.type_has_nil(T) {
|
||||||
|
harnesses: []Harness(T), // Accessed from slave threads
|
||||||
|
spin_limit: uint, // Accessed from slave threads
|
||||||
|
num_cmds_in_round: uint, // Number of commands submitted without join being called
|
||||||
|
harness_index: int,
|
||||||
|
cmd_queue_floor: int,
|
||||||
|
thread_pool: thread.Pool,
|
||||||
|
initialized: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: Provide a way to set some aspects of context for the executor threads. Namely a logger.
|
||||||
|
init :: 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,
|
||||||
|
true,
|
||||||
|
.Seq_Cst,
|
||||||
|
.Seq_Cst,
|
||||||
|
)
|
||||||
|
assert(!was_initialized, "Executor already initialized.")
|
||||||
|
|
||||||
|
slave_task := build_task(on_command_received)
|
||||||
|
executor.spin_limit = spin_limit
|
||||||
|
executor.harnesses = make([]Harness(T), num_threads, allocator) or_return
|
||||||
|
for &harness in executor.harnesses {
|
||||||
|
que.init(&harness.cmd_queue, allocator = allocator) or_return
|
||||||
|
harness.spin = true
|
||||||
|
}
|
||||||
|
|
||||||
|
thread.pool_init(&executor.thread_pool, allocator, num_threads)
|
||||||
|
for i in 0 ..< num_threads {
|
||||||
|
thread.pool_add_task(&executor.thread_pool, allocator, slave_task, data = executor, user_index = i)
|
||||||
|
}
|
||||||
|
thread.pool_start(&executor.thread_pool)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanly shuts down all executor tasks then destroys the executor
|
||||||
|
destroy :: proc(executor: ^Executor($T), allocator := context.allocator) -> runtime.Allocator_Error {
|
||||||
|
was_initialized, _ := intrinsics.atomic_compare_exchange_strong_explicit(
|
||||||
|
&executor.initialized,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
.Seq_Cst,
|
||||||
|
.Seq_Cst,
|
||||||
|
)
|
||||||
|
assert(was_initialized, "Executor not initialized.")
|
||||||
|
|
||||||
|
// Exit thread loops
|
||||||
|
for &harness in executor.harnesses {
|
||||||
|
for {
|
||||||
|
if levsync.try_lock(&harness.lock) {
|
||||||
|
que.push_back(&harness.cmd_queue, nil)
|
||||||
|
if !harness.spin {
|
||||||
|
sync.mutex_lock(&harness.mutex)
|
||||||
|
sync.cond_signal(&harness.condition)
|
||||||
|
sync.mutex_unlock(&harness.mutex)
|
||||||
|
}
|
||||||
|
levsync.unlock(&harness.lock)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
thread.pool_join(&executor.thread_pool)
|
||||||
|
thread.pool_destroy(&executor.thread_pool)
|
||||||
|
for &harness in executor.harnesses {
|
||||||
|
que.destroy(&harness.cmd_queue)
|
||||||
|
}
|
||||||
|
delete(executor.harnesses, allocator) or_return
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
build_task :: proc(
|
||||||
|
$on_command_received: proc(command: $T),
|
||||||
|
) -> (
|
||||||
|
slave_task: proc(task: thread.Task),
|
||||||
|
) where intrinsics.type_has_nil(T) {
|
||||||
|
slave_task = proc(task: thread.Task) {
|
||||||
|
when b.SPALL_TRACE {
|
||||||
|
spall_data := make([]u8, spall.BUFFER_DEFAULT_SIZE)
|
||||||
|
spall_buffer = spall.buffer_create(spall_data, u32(sync.current_thread_id()))
|
||||||
|
defer spall.buffer_destroy(&spall_ctx, &spall_buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
executor := cast(^Executor(T))task.data
|
||||||
|
harness := &executor.harnesses[task.user_index]
|
||||||
|
sync.mutex_lock(&harness.mutex)
|
||||||
|
for {
|
||||||
|
defer free_all(context.temp_allocator)
|
||||||
|
// Spinning
|
||||||
|
spin_count: uint = 0
|
||||||
|
spin_loop: for {
|
||||||
|
if levsync.try_lock(&harness.lock) {
|
||||||
|
if que.len(harness.cmd_queue) > 0 {
|
||||||
|
|
||||||
|
// Execute command
|
||||||
|
command := que.pop_front(&harness.cmd_queue)
|
||||||
|
levsync.unlock(&harness.lock)
|
||||||
|
if command == nil do return
|
||||||
|
on_command_received(command)
|
||||||
|
|
||||||
|
spin_count = 0
|
||||||
|
intrinsics.atomic_add_explicit(&harness.join_count, 1, .Release)
|
||||||
|
} else {
|
||||||
|
defer intrinsics.cpu_relax()
|
||||||
|
defer levsync.unlock(&harness.lock)
|
||||||
|
spin_count += 1
|
||||||
|
if spin_count == executor.spin_limit {
|
||||||
|
harness.spin = false
|
||||||
|
break spin_loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else { // If master locked the command queue there will be a new command soon
|
||||||
|
spin_count = 0
|
||||||
|
intrinsics.cpu_relax()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sleeping
|
||||||
|
cond_loop: for { // We have to loop because cond_wait can return without signal sometimes
|
||||||
|
sync.cond_wait(&harness.condition, &harness.mutex)
|
||||||
|
for { // Loop to acquire harness lock
|
||||||
|
defer intrinsics.cpu_relax()
|
||||||
|
if levsync.try_lock(&harness.lock) {
|
||||||
|
defer levsync.unlock(&harness.lock)
|
||||||
|
if que.len(harness.cmd_queue) > 0 {
|
||||||
|
harness.spin = true
|
||||||
|
break cond_loop
|
||||||
|
} else {
|
||||||
|
continue cond_loop // Spurious wakeup, go back to sleep
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return slave_task
|
||||||
|
}
|
||||||
|
|
||||||
|
exec_command :: proc(executor: ^Executor($T), command: T) {
|
||||||
|
defer executor.num_cmds_in_round += 1
|
||||||
|
for {
|
||||||
|
if executor.num_cmds_in_round > 0 { // Avoid spinning multiple locks if we're only using 1 thread
|
||||||
|
if executor.harness_index == len(executor.harnesses) - 1 {
|
||||||
|
executor.harness_index = 0
|
||||||
|
} else {
|
||||||
|
executor.harness_index += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
slave_sleeping := !harness.spin
|
||||||
|
// Must release lock before signalling to avoid race from slave spurious wakeup
|
||||||
|
levsync.unlock(&harness.lock)
|
||||||
|
if slave_sleeping {
|
||||||
|
sync.mutex_lock(&harness.mutex)
|
||||||
|
sync.cond_signal(&harness.condition)
|
||||||
|
sync.mutex_unlock(&harness.mutex)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
levsync.unlock(&harness.lock)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spin check until issued executor commands finish
|
||||||
|
exec_join :: proc(executor: ^Executor($T)) {
|
||||||
|
defer executor.num_cmds_in_round = 0
|
||||||
|
for {
|
||||||
|
completed_commands: uint = 0
|
||||||
|
for &harness in executor.harnesses {
|
||||||
|
completed_commands += intrinsics.atomic_load_explicit(&harness.join_count, .Acquire)
|
||||||
|
}
|
||||||
|
if completed_commands == executor.num_cmds_in_round {
|
||||||
|
for &harness in executor.harnesses {
|
||||||
|
// We know the slave will never access join_count at this time so we don't need to synchronize
|
||||||
|
harness.join_count = 0
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
intrinsics.cpu_relax()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
// ----- Tests ---------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
import "core:fmt"
|
||||||
|
import "core:testing"
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
stress_test_executor :: proc(t: ^testing.T) {
|
||||||
|
STRESS_TOTAL_CMDS :: 200_000
|
||||||
|
STRESS_NUM_THREADS :: 8
|
||||||
|
STRESS_NUM_ROUNDS :: 100
|
||||||
|
STRESS_CMDS_PER_ROUND :: STRESS_TOTAL_CMDS / STRESS_NUM_ROUNDS
|
||||||
|
|
||||||
|
Stress_Cmd :: union {
|
||||||
|
Stress_Payload,
|
||||||
|
}
|
||||||
|
|
||||||
|
Stress_Payload :: struct {
|
||||||
|
exec_counts: ^[STRESS_TOTAL_CMDS]uint,
|
||||||
|
id: int,
|
||||||
|
}
|
||||||
|
|
||||||
|
stress_handler :: proc(command: Stress_Cmd) {
|
||||||
|
payload := command.(Stress_Payload)
|
||||||
|
intrinsics.atomic_add_explicit(&payload.exec_counts[payload.id], 1, .Release)
|
||||||
|
}
|
||||||
|
|
||||||
|
exec_counts := new([STRESS_TOTAL_CMDS]uint)
|
||||||
|
defer free(exec_counts)
|
||||||
|
|
||||||
|
executor: Executor(Stress_Cmd)
|
||||||
|
init(&executor, STRESS_NUM_THREADS, stress_handler, spin_limit = 500)
|
||||||
|
|
||||||
|
for round in 0 ..< STRESS_NUM_ROUNDS {
|
||||||
|
base := round * STRESS_CMDS_PER_ROUND
|
||||||
|
for i in 0 ..< STRESS_CMDS_PER_ROUND {
|
||||||
|
exec_command(&executor, Stress_Payload{exec_counts = exec_counts, id = base + i})
|
||||||
|
}
|
||||||
|
exec_join(&executor)
|
||||||
|
}
|
||||||
|
|
||||||
|
missed, duped: int
|
||||||
|
for i in 0 ..< STRESS_TOTAL_CMDS {
|
||||||
|
count := exec_counts[i]
|
||||||
|
if count == 0 do missed += 1
|
||||||
|
else if count > 1 do duped += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
testing.expect(t, missed == 0, fmt.tprintf("Missed %d / %d commands", missed, STRESS_TOTAL_CMDS))
|
||||||
|
testing.expect(t, duped == 0, fmt.tprintf("Duplicated %d / %d commands", duped, STRESS_TOTAL_CMDS))
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
testing.expect(t, !executor.initialized, "Executor still marked initialized after destroy")
|
||||||
|
}
|
||||||
@@ -0,0 +1,280 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
+269
-99
@@ -1,103 +1,139 @@
|
|||||||
package ring
|
package ring
|
||||||
|
|
||||||
|
import "base:runtime"
|
||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
|
|
||||||
@(private)
|
@(private)
|
||||||
ODIN_BOUNDS_CHECK :: !ODIN_NO_BOUNDS_CHECK
|
ODIN_BOUNDS_CHECK :: !ODIN_NO_BOUNDS_CHECK
|
||||||
|
|
||||||
Ring :: struct($T: typeid) {
|
Ring :: struct($E: typeid) {
|
||||||
data: []T,
|
data: []E,
|
||||||
_end_index, len: int,
|
next_write_index, len: int,
|
||||||
}
|
}
|
||||||
|
|
||||||
Ring_Soa :: struct($T: typeid) {
|
Ring_Soa :: struct($E: typeid) {
|
||||||
data: #soa[]T,
|
data: #soa[]E,
|
||||||
_end_index, len: int,
|
next_write_index, len: int,
|
||||||
}
|
}
|
||||||
|
|
||||||
from_slice_raos :: #force_inline proc(data: $T/[]$E) -> Ring(E) {
|
destroy_aos :: #force_inline proc(
|
||||||
return {data = data, _end_index = -1}
|
ring: ^Ring($E),
|
||||||
|
allocator := context.allocator,
|
||||||
|
) -> runtime.Allocator_Error {
|
||||||
|
return delete(ring.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
from_slice_rsoa :: #force_inline proc(data: $T/#soa[]$E) -> Ring_Soa(E) {
|
destroy_soa :: #force_inline proc(
|
||||||
return {data = data, _end_index = -1}
|
ring: ^Ring_Soa($E),
|
||||||
|
allocator := context.allocator,
|
||||||
|
) -> runtime.Allocator_Error {
|
||||||
|
return delete(ring.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
from_slice :: proc {
|
destroy :: proc {
|
||||||
from_slice_raos,
|
destroy_aos,
|
||||||
from_slice_rsoa,
|
destroy_soa,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// Index in the backing array where the ring starts
|
||||||
_start_index_raos :: proc(ring: Ring($T)) -> int {
|
start_index_aos :: #force_inline proc(ring: Ring($E)) -> int {
|
||||||
if ring.len < len(ring.data) {
|
return ring.len < len(ring.data) ? 0 : ring.next_write_index
|
||||||
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
|
// Index in the backing array where the ring starts
|
||||||
_start_index_rsoa :: proc(ring: Ring_Soa($T)) -> int {
|
start_index_soa :: #force_inline proc(ring: Ring_Soa($E)) -> int {
|
||||||
if ring.len < len(ring.data) {
|
return ring.len < len(ring.data) ? 0 : ring.next_write_index
|
||||||
return 0
|
|
||||||
} else {
|
|
||||||
start_index := ring._end_index + 1
|
|
||||||
return 0 if start_index == len(ring.data) else start_index
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
advance_raos :: proc(ring: ^Ring($T)) {
|
advance_aos :: #force_inline proc(ring: ^Ring($E)) {
|
||||||
// Length
|
// Length
|
||||||
if ring.len != len(ring.data) do ring.len += 1
|
if ring.len != len(ring.data) do ring.len += 1
|
||||||
// End index
|
// Write index
|
||||||
if ring._end_index == len(ring.data) - 1 { // If we are at the end of the backing array
|
ring.next_write_index += 1
|
||||||
ring._end_index = 0 // Overflow end to 0
|
if ring.next_write_index == len(ring.data) do ring.next_write_index = 0
|
||||||
} else {
|
|
||||||
ring._end_index += 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
advance_rsoa :: proc(ring: ^Ring_Soa($T)) {
|
advance_soa :: #force_inline proc(ring: ^Ring_Soa($E)) {
|
||||||
// Length
|
// Length
|
||||||
if ring.len != len(ring.data) do ring.len += 1
|
if ring.len != len(ring.data) do ring.len += 1
|
||||||
// End index
|
// Write index
|
||||||
if ring._end_index == len(ring.data) - 1 { // If we are at the end of the backing array
|
ring.next_write_index += 1
|
||||||
ring._end_index = 0 // Overflow end to 0
|
if ring.next_write_index == len(ring.data) do ring.next_write_index = 0
|
||||||
} else {
|
|
||||||
ring._end_index += 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
advance :: proc {
|
advance :: proc {
|
||||||
advance_raos,
|
advance_aos,
|
||||||
advance_rsoa,
|
advance_soa,
|
||||||
}
|
}
|
||||||
|
|
||||||
append_raos :: proc(ring: ^Ring($T), element: T) {
|
append_aos :: #force_inline proc(ring: ^Ring($E), element: E) {
|
||||||
|
ring.data[ring.next_write_index] = element
|
||||||
advance(ring)
|
advance(ring)
|
||||||
ring.data[ring._end_index] = element
|
|
||||||
}
|
}
|
||||||
|
|
||||||
append_rsoa :: proc(ring: ^Ring_Soa($T), element: T) {
|
append_soa :: #force_inline proc(ring: ^Ring_Soa($E), element: E) {
|
||||||
|
ring.data[ring.next_write_index] = element
|
||||||
advance(ring)
|
advance(ring)
|
||||||
ring.data[ring._end_index] = element
|
|
||||||
}
|
}
|
||||||
|
|
||||||
append :: proc {
|
append :: proc {
|
||||||
append_raos,
|
append_aos,
|
||||||
append_rsoa,
|
append_soa,
|
||||||
}
|
}
|
||||||
|
|
||||||
get_raos :: proc(ring: Ring($T), index: int) -> ^T {
|
get_aos :: #force_inline proc(ring: Ring($E), index: int) -> ^E {
|
||||||
when ODIN_BOUNDS_CHECK {
|
when ODIN_BOUNDS_CHECK {
|
||||||
if index >= ring.len {
|
fmt.assertf(index < ring.len, "Ring index %i out of bounds for length %i", index, ring.len)
|
||||||
panic(fmt.tprintf("Ring index %i out of bounds for length %i", index, ring.len))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
array_index := _start_index_raos(ring) + index
|
array_index := start_index_aos(ring) + index
|
||||||
if array_index < len(ring.data) {
|
if array_index < len(ring.data) {
|
||||||
return &ring.data[array_index]
|
return &ring.data[array_index]
|
||||||
} else {
|
} else {
|
||||||
@@ -107,14 +143,12 @@ get_raos :: proc(ring: Ring($T), index: int) -> ^T {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SOA can't return soa pointer to parapoly T.
|
// SOA can't return soa pointer to parapoly T.
|
||||||
get_rsoa :: proc(ring: Ring_Soa($T), index: int) -> T {
|
get_soa :: #force_inline proc(ring: Ring_Soa($E), index: int) -> E {
|
||||||
when ODIN_BOUNDS_CHECK {
|
when ODIN_BOUNDS_CHECK {
|
||||||
if index >= ring.len {
|
fmt.assertf(index < ring.len, "Ring index %i out of bounds for length %i", index, ring.len)
|
||||||
panic(fmt.tprintf("Ring index %i out of bounds for length %i", index, ring.len))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
array_index := _start_index_rsoa(ring) + index
|
array_index := start_index_soa(ring) + index
|
||||||
if array_index < len(ring.data) {
|
if array_index < len(ring.data) {
|
||||||
return ring.data[array_index]
|
return ring.data[array_index]
|
||||||
} else {
|
} else {
|
||||||
@@ -124,36 +158,36 @@ get_rsoa :: proc(ring: Ring_Soa($T), index: int) -> T {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get :: proc {
|
get :: proc {
|
||||||
get_raos,
|
get_aos,
|
||||||
get_rsoa,
|
get_soa,
|
||||||
}
|
}
|
||||||
|
|
||||||
get_last_raos :: #force_inline proc(ring: Ring($T)) -> ^T {
|
get_last_aos :: #force_inline proc(ring: Ring($E)) -> ^E {
|
||||||
return get(ring, ring.len - 1)
|
return get(ring, ring.len - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
get_last_rsoa :: #force_inline proc(ring: Ring_Soa($T)) -> T {
|
get_last_soa :: #force_inline proc(ring: Ring_Soa($E)) -> E {
|
||||||
return get(ring, ring.len - 1)
|
return get(ring, ring.len - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
get_last :: proc {
|
get_last :: proc {
|
||||||
get_last_raos,
|
get_last_aos,
|
||||||
get_last_rsoa,
|
get_last_soa,
|
||||||
}
|
}
|
||||||
|
|
||||||
clear_raos :: #force_inline proc "contextless" (ring: ^Ring($T)) {
|
clear_aos :: #force_inline proc "contextless" (ring: ^Ring($E)) {
|
||||||
ring.len = 0
|
ring.len = 0
|
||||||
ring._end_index = -1
|
ring.next_write_index = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
clear_rsoa :: #force_inline proc "contextless" (ring: ^Ring_Soa($T)) {
|
clear_soa :: #force_inline proc "contextless" (ring: ^Ring_Soa($E)) {
|
||||||
ring.len = 0
|
ring.len = 0
|
||||||
ring._end_index = -1
|
ring.next_write_index = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
clear :: proc {
|
clear :: proc {
|
||||||
clear_raos,
|
clear_aos,
|
||||||
clear_rsoa,
|
clear_soa,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
@@ -164,28 +198,27 @@ import "core:testing"
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_ring_aos :: proc(t: ^testing.T) {
|
test_ring_aos :: proc(t: ^testing.T) {
|
||||||
data := make_slice([]int, 10)
|
ring := create_aos(int, 10)
|
||||||
ring := from_slice(data)
|
defer destroy(&ring)
|
||||||
defer delete(ring.data)
|
|
||||||
|
|
||||||
for i in 1 ..= 5 {
|
for i in 1 ..= 5 {
|
||||||
append(&ring, i)
|
append(&ring, i)
|
||||||
log.debug("Length:", ring.len)
|
log.debug("Length:", ring.len)
|
||||||
log.debug("Start index:", _start_index_raos(ring))
|
log.debug("Start index:", start_index_aos(ring))
|
||||||
log.debug("End index:", ring._end_index)
|
log.debug("Next write index:", ring.next_write_index)
|
||||||
log.debug(ring.data)
|
log.debug(ring.data)
|
||||||
}
|
}
|
||||||
testing.expect_value(t, get(ring, 0)^, 1)
|
testing.expect_value(t, get(ring, 0)^, 1)
|
||||||
testing.expect_value(t, get(ring, 4)^, 5)
|
testing.expect_value(t, get(ring, 4)^, 5)
|
||||||
testing.expect_value(t, ring.len, 5)
|
testing.expect_value(t, ring.len, 5)
|
||||||
testing.expect_value(t, ring._end_index, 4)
|
testing.expect_value(t, ring.next_write_index, 5)
|
||||||
testing.expect_value(t, _start_index_raos(ring), 0)
|
testing.expect_value(t, start_index_aos(ring), 0)
|
||||||
|
|
||||||
for i in 6 ..= 15 {
|
for i in 6 ..= 15 {
|
||||||
append(&ring, i)
|
append(&ring, i)
|
||||||
log.debug("Length:", ring.len)
|
log.debug("Length:", ring.len)
|
||||||
log.debug("Start index:", _start_index_raos(ring))
|
log.debug("Start index:", start_index_aos(ring))
|
||||||
log.debug("End index:", ring._end_index)
|
log.debug("Next write index:", ring.next_write_index)
|
||||||
log.debug(ring.data)
|
log.debug(ring.data)
|
||||||
}
|
}
|
||||||
testing.expect_value(t, get(ring, 0)^, 6)
|
testing.expect_value(t, get(ring, 0)^, 6)
|
||||||
@@ -193,18 +226,18 @@ test_ring_aos :: proc(t: ^testing.T) {
|
|||||||
testing.expect_value(t, get(ring, 9)^, 15)
|
testing.expect_value(t, get(ring, 9)^, 15)
|
||||||
testing.expect_value(t, get_last(ring)^, 15)
|
testing.expect_value(t, get_last(ring)^, 15)
|
||||||
testing.expect_value(t, ring.len, 10)
|
testing.expect_value(t, ring.len, 10)
|
||||||
testing.expect_value(t, ring._end_index, 4)
|
testing.expect_value(t, ring.next_write_index, 5)
|
||||||
testing.expect_value(t, _start_index_raos(ring), 5)
|
testing.expect_value(t, start_index_aos(ring), 5)
|
||||||
|
|
||||||
for i in 15 ..= 25 {
|
for i in 15 ..= 25 {
|
||||||
append(&ring, i)
|
append(&ring, i)
|
||||||
log.debug("Length:", ring.len)
|
log.debug("Length:", ring.len)
|
||||||
log.debug("Start index:", _start_index_raos(ring))
|
log.debug("Start index:", start_index_aos(ring))
|
||||||
log.debug("End index:", ring._end_index)
|
log.debug("Next write index:", ring.next_write_index)
|
||||||
log.debug(ring.data)
|
log.debug(ring.data)
|
||||||
}
|
}
|
||||||
testing.expect_value(t, get(ring, 0)^, 16)
|
testing.expect_value(t, get(ring, 0)^, 16)
|
||||||
testing.expect_value(t, ring._end_index, 5)
|
testing.expect_value(t, ring.next_write_index, 6)
|
||||||
testing.expect_value(t, get_last(ring)^, 25)
|
testing.expect_value(t, get_last(ring)^, 25)
|
||||||
|
|
||||||
clear(&ring)
|
clear(&ring)
|
||||||
@@ -219,28 +252,27 @@ test_ring_soa :: proc(t: ^testing.T) {
|
|||||||
x, y: int,
|
x, y: int,
|
||||||
}
|
}
|
||||||
|
|
||||||
data := make_soa_slice(#soa[]Ints, 10)
|
ring := create_soa(Ints, 10)
|
||||||
ring := from_slice(data)
|
defer destroy(&ring)
|
||||||
defer delete(ring.data)
|
|
||||||
|
|
||||||
for i in 1 ..= 5 {
|
for i in 1 ..= 5 {
|
||||||
append(&ring, Ints{i, i})
|
append(&ring, Ints{i, i})
|
||||||
log.debug("Length:", ring.len)
|
log.debug("Length:", ring.len)
|
||||||
log.debug("Start index:", _start_index_rsoa(ring))
|
log.debug("Start index:", start_index_soa(ring))
|
||||||
log.debug("End index:", ring._end_index)
|
log.debug("Next write index:", ring.next_write_index)
|
||||||
log.debug(ring.data)
|
log.debug(ring.data)
|
||||||
}
|
}
|
||||||
testing.expect_value(t, get(ring, 0), Ints{1, 1})
|
testing.expect_value(t, get(ring, 0), Ints{1, 1})
|
||||||
testing.expect_value(t, get(ring, 4), Ints{5, 5})
|
testing.expect_value(t, get(ring, 4), Ints{5, 5})
|
||||||
testing.expect_value(t, ring.len, 5)
|
testing.expect_value(t, ring.len, 5)
|
||||||
testing.expect_value(t, ring._end_index, 4)
|
testing.expect_value(t, ring.next_write_index, 5)
|
||||||
testing.expect_value(t, _start_index_rsoa(ring), 0)
|
testing.expect_value(t, start_index_soa(ring), 0)
|
||||||
|
|
||||||
for i in 6 ..= 15 {
|
for i in 6 ..= 15 {
|
||||||
append(&ring, Ints{i, i})
|
append(&ring, Ints{i, i})
|
||||||
log.debug("Length:", ring.len)
|
log.debug("Length:", ring.len)
|
||||||
log.debug("Start index:", _start_index_rsoa(ring))
|
log.debug("Start index:", start_index_soa(ring))
|
||||||
log.debug("End index:", ring._end_index)
|
log.debug("Next write index:", ring.next_write_index)
|
||||||
log.debug(ring.data)
|
log.debug(ring.data)
|
||||||
}
|
}
|
||||||
testing.expect_value(t, get(ring, 0), Ints{6, 6})
|
testing.expect_value(t, get(ring, 0), Ints{6, 6})
|
||||||
@@ -248,18 +280,18 @@ test_ring_soa :: proc(t: ^testing.T) {
|
|||||||
testing.expect_value(t, get(ring, 9), Ints{15, 15})
|
testing.expect_value(t, get(ring, 9), Ints{15, 15})
|
||||||
testing.expect_value(t, get_last(ring), 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.len, 10)
|
||||||
testing.expect_value(t, ring._end_index, 4)
|
testing.expect_value(t, ring.next_write_index, 5)
|
||||||
testing.expect_value(t, _start_index_rsoa(ring), 5)
|
testing.expect_value(t, start_index_soa(ring), 5)
|
||||||
|
|
||||||
for i in 15 ..= 25 {
|
for i in 15 ..= 25 {
|
||||||
append(&ring, Ints{i, i})
|
append(&ring, Ints{i, i})
|
||||||
log.debug("Length:", ring.len)
|
log.debug("Length:", ring.len)
|
||||||
log.debug("Start index:", _start_index_rsoa(ring))
|
log.debug("Start index:", start_index_soa(ring))
|
||||||
log.debug("End index:", ring._end_index)
|
log.debug("Next write index:", ring.next_write_index)
|
||||||
log.debug(ring.data)
|
log.debug(ring.data)
|
||||||
}
|
}
|
||||||
testing.expect_value(t, get(ring, 0), Ints{16, 16})
|
testing.expect_value(t, get(ring, 0), Ints{16, 16})
|
||||||
testing.expect_value(t, ring._end_index, 5)
|
testing.expect_value(t, ring.next_write_index, 6)
|
||||||
testing.expect_value(t, get_last(ring), Ints{25, 25})
|
testing.expect_value(t, get_last(ring), Ints{25, 25})
|
||||||
|
|
||||||
clear(&ring)
|
clear(&ring)
|
||||||
@@ -267,3 +299,141 @@ test_ring_soa :: proc(t: ^testing.T) {
|
|||||||
testing.expect_value(t, ring.len, 1)
|
testing.expect_value(t, ring.len, 1)
|
||||||
testing.expect_value(t, get(ring, 0), Ints{1, 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})
|
||||||
|
}
|
||||||
|
|||||||
Vendored
+489
@@ -0,0 +1,489 @@
|
|||||||
|
package clay
|
||||||
|
|
||||||
|
import "core:c"
|
||||||
|
|
||||||
|
when ODIN_OS == .Windows {
|
||||||
|
foreign import Clay "windows/clay.lib"
|
||||||
|
} else when ODIN_OS == .Linux {
|
||||||
|
foreign import Clay "linux/clay.a"
|
||||||
|
} else when ODIN_OS == .Darwin {
|
||||||
|
when ODIN_ARCH == .arm64 {
|
||||||
|
foreign import Clay "macos-arm64/clay.a"
|
||||||
|
} else {
|
||||||
|
foreign import Clay "macos/clay.a"
|
||||||
|
}
|
||||||
|
} else when ODIN_ARCH == .wasm32 || ODIN_ARCH == .wasm64p32 {
|
||||||
|
foreign import Clay "wasm/clay.o"
|
||||||
|
}
|
||||||
|
|
||||||
|
String :: struct {
|
||||||
|
isStaticallyAllocated: c.bool,
|
||||||
|
length: c.int32_t,
|
||||||
|
chars: [^]c.char,
|
||||||
|
}
|
||||||
|
|
||||||
|
StringSlice :: struct {
|
||||||
|
length: c.int32_t,
|
||||||
|
chars: [^]c.char,
|
||||||
|
baseChars: [^]c.char,
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2 :: [2]c.float
|
||||||
|
|
||||||
|
Dimensions :: struct {
|
||||||
|
width: c.float,
|
||||||
|
height: c.float,
|
||||||
|
}
|
||||||
|
|
||||||
|
Arena :: struct {
|
||||||
|
nextAllocation: uintptr,
|
||||||
|
capacity: c.size_t,
|
||||||
|
memory: [^]c.char,
|
||||||
|
}
|
||||||
|
|
||||||
|
BoundingBox :: struct {
|
||||||
|
x: c.float,
|
||||||
|
y: c.float,
|
||||||
|
width: c.float,
|
||||||
|
height: c.float,
|
||||||
|
}
|
||||||
|
|
||||||
|
Color :: [4]c.float
|
||||||
|
|
||||||
|
CornerRadius :: struct {
|
||||||
|
topLeft: c.float,
|
||||||
|
topRight: c.float,
|
||||||
|
bottomLeft: c.float,
|
||||||
|
bottomRight: c.float,
|
||||||
|
}
|
||||||
|
|
||||||
|
BorderData :: struct {
|
||||||
|
width: u32,
|
||||||
|
color: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
ElementId :: struct {
|
||||||
|
id: u32,
|
||||||
|
offset: u32,
|
||||||
|
baseId: u32,
|
||||||
|
stringId: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
when ODIN_OS == .Windows {
|
||||||
|
EnumBackingType :: u32
|
||||||
|
} else {
|
||||||
|
EnumBackingType :: u8
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderCommandType :: enum EnumBackingType {
|
||||||
|
None,
|
||||||
|
Rectangle,
|
||||||
|
Border,
|
||||||
|
Text,
|
||||||
|
Image,
|
||||||
|
ScissorStart,
|
||||||
|
ScissorEnd,
|
||||||
|
Custom,
|
||||||
|
}
|
||||||
|
|
||||||
|
RectangleElementConfig :: struct {
|
||||||
|
color: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
TextWrapMode :: enum EnumBackingType {
|
||||||
|
Words,
|
||||||
|
Newlines,
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
TextAlignment :: enum EnumBackingType {
|
||||||
|
Left,
|
||||||
|
Center,
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
|
||||||
|
TextElementConfig :: struct {
|
||||||
|
userData: rawptr,
|
||||||
|
textColor: Color,
|
||||||
|
fontId: u16,
|
||||||
|
fontSize: u16,
|
||||||
|
letterSpacing: u16,
|
||||||
|
lineHeight: u16,
|
||||||
|
wrapMode: TextWrapMode,
|
||||||
|
textAlignment: TextAlignment,
|
||||||
|
}
|
||||||
|
|
||||||
|
AspectRatioElementConfig :: struct {
|
||||||
|
aspectRatio: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageElementConfig :: struct {
|
||||||
|
imageData: rawptr,
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomElementConfig :: struct {
|
||||||
|
customData: rawptr,
|
||||||
|
}
|
||||||
|
|
||||||
|
BorderWidth :: struct {
|
||||||
|
left: u16,
|
||||||
|
right: u16,
|
||||||
|
top: u16,
|
||||||
|
bottom: u16,
|
||||||
|
betweenChildren: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
BorderElementConfig :: struct {
|
||||||
|
color: Color,
|
||||||
|
width: BorderWidth,
|
||||||
|
}
|
||||||
|
|
||||||
|
ClipElementConfig :: struct {
|
||||||
|
horizontal: bool, // clip overflowing elements on the "X" axis
|
||||||
|
vertical: bool, // clip overflowing elements on the "Y" axis
|
||||||
|
childOffset: Vector2, // offsets the [X,Y] positions of all child elements, primarily for scrolling containers
|
||||||
|
}
|
||||||
|
|
||||||
|
FloatingAttachPointType :: enum EnumBackingType {
|
||||||
|
LeftTop,
|
||||||
|
LeftCenter,
|
||||||
|
LeftBottom,
|
||||||
|
CenterTop,
|
||||||
|
CenterCenter,
|
||||||
|
CenterBottom,
|
||||||
|
RightTop,
|
||||||
|
RightCenter,
|
||||||
|
RightBottom,
|
||||||
|
}
|
||||||
|
|
||||||
|
FloatingAttachPoints :: struct {
|
||||||
|
element: FloatingAttachPointType,
|
||||||
|
parent: FloatingAttachPointType,
|
||||||
|
}
|
||||||
|
|
||||||
|
PointerCaptureMode :: enum EnumBackingType {
|
||||||
|
Capture,
|
||||||
|
Passthrough,
|
||||||
|
}
|
||||||
|
|
||||||
|
FloatingAttachToElement :: enum EnumBackingType {
|
||||||
|
None,
|
||||||
|
Parent,
|
||||||
|
ElementWithId,
|
||||||
|
Root,
|
||||||
|
}
|
||||||
|
|
||||||
|
FloatingClipToElement :: enum EnumBackingType {
|
||||||
|
None,
|
||||||
|
AttachedParent,
|
||||||
|
}
|
||||||
|
|
||||||
|
FloatingElementConfig :: struct {
|
||||||
|
offset: Vector2,
|
||||||
|
expand: Dimensions,
|
||||||
|
parentId: u32,
|
||||||
|
zIndex: i16,
|
||||||
|
attachment: FloatingAttachPoints,
|
||||||
|
pointerCaptureMode: PointerCaptureMode,
|
||||||
|
attachTo: FloatingAttachToElement,
|
||||||
|
clipTo: FloatingClipToElement,
|
||||||
|
}
|
||||||
|
|
||||||
|
TextRenderData :: struct {
|
||||||
|
stringContents: StringSlice,
|
||||||
|
textColor: Color,
|
||||||
|
fontId: u16,
|
||||||
|
fontSize: u16,
|
||||||
|
letterSpacing: u16,
|
||||||
|
lineHeight: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
RectangleRenderData :: struct {
|
||||||
|
backgroundColor: Color,
|
||||||
|
cornerRadius: CornerRadius,
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageRenderData :: struct {
|
||||||
|
backgroundColor: Color,
|
||||||
|
cornerRadius: CornerRadius,
|
||||||
|
imageData: rawptr,
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomRenderData :: struct {
|
||||||
|
backgroundColor: Color,
|
||||||
|
cornerRadius: CornerRadius,
|
||||||
|
customData: rawptr,
|
||||||
|
}
|
||||||
|
|
||||||
|
BorderRenderData :: struct {
|
||||||
|
color: Color,
|
||||||
|
cornerRadius: CornerRadius,
|
||||||
|
width: BorderWidth,
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderCommandData :: struct #raw_union {
|
||||||
|
rectangle: RectangleRenderData,
|
||||||
|
text: TextRenderData,
|
||||||
|
image: ImageRenderData,
|
||||||
|
custom: CustomRenderData,
|
||||||
|
border: BorderRenderData,
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderCommand :: struct {
|
||||||
|
boundingBox: BoundingBox,
|
||||||
|
renderData: RenderCommandData,
|
||||||
|
userData: rawptr,
|
||||||
|
id: u32,
|
||||||
|
zIndex: i16,
|
||||||
|
commandType: RenderCommandType,
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollContainerData :: struct {
|
||||||
|
// Note: This is a pointer to the real internal scroll position, mutating it may cause a change in final layout.
|
||||||
|
// Intended for use with external functionality that modifies scroll position, such as scroll bars or auto scrolling.
|
||||||
|
scrollPosition: ^Vector2,
|
||||||
|
scrollContainerDimensions: Dimensions,
|
||||||
|
contentDimensions: Dimensions,
|
||||||
|
config: ClipElementConfig,
|
||||||
|
// Indicates whether an actual scroll container matched the provided ID or if the default struct was returned.
|
||||||
|
found: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
ElementData :: struct {
|
||||||
|
boundingBox: BoundingBox,
|
||||||
|
found: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
PointerDataInteractionState :: enum EnumBackingType {
|
||||||
|
PressedThisFrame,
|
||||||
|
Pressed,
|
||||||
|
ReleasedThisFrame,
|
||||||
|
Released,
|
||||||
|
}
|
||||||
|
|
||||||
|
PointerData :: struct {
|
||||||
|
position: Vector2,
|
||||||
|
state: PointerDataInteractionState,
|
||||||
|
}
|
||||||
|
|
||||||
|
SizingType :: enum EnumBackingType {
|
||||||
|
Fit,
|
||||||
|
Grow,
|
||||||
|
Percent,
|
||||||
|
Fixed,
|
||||||
|
}
|
||||||
|
|
||||||
|
SizingConstraintsMinMax :: struct {
|
||||||
|
min: c.float,
|
||||||
|
max: c.float,
|
||||||
|
}
|
||||||
|
|
||||||
|
SizingConstraints :: struct #raw_union {
|
||||||
|
sizeMinMax: SizingConstraintsMinMax,
|
||||||
|
sizePercent: c.float,
|
||||||
|
}
|
||||||
|
|
||||||
|
SizingAxis :: struct {
|
||||||
|
// Note: `min` is used for CLAY_SIZING_PERCENT, slightly different to clay.h due to lack of C anonymous unions
|
||||||
|
constraints: SizingConstraints,
|
||||||
|
type: SizingType,
|
||||||
|
}
|
||||||
|
|
||||||
|
Sizing :: struct {
|
||||||
|
width: SizingAxis,
|
||||||
|
height: SizingAxis,
|
||||||
|
}
|
||||||
|
|
||||||
|
Padding :: struct {
|
||||||
|
left: u16,
|
||||||
|
right: u16,
|
||||||
|
top: u16,
|
||||||
|
bottom: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
LayoutDirection :: enum EnumBackingType {
|
||||||
|
LeftToRight,
|
||||||
|
TopToBottom,
|
||||||
|
}
|
||||||
|
|
||||||
|
LayoutAlignmentX :: enum EnumBackingType {
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
Center,
|
||||||
|
}
|
||||||
|
|
||||||
|
LayoutAlignmentY :: enum EnumBackingType {
|
||||||
|
Top,
|
||||||
|
Bottom,
|
||||||
|
Center,
|
||||||
|
}
|
||||||
|
|
||||||
|
ChildAlignment :: struct {
|
||||||
|
x: LayoutAlignmentX,
|
||||||
|
y: LayoutAlignmentY,
|
||||||
|
}
|
||||||
|
|
||||||
|
LayoutConfig :: struct {
|
||||||
|
sizing: Sizing,
|
||||||
|
padding: Padding,
|
||||||
|
childGap: u16,
|
||||||
|
childAlignment: ChildAlignment,
|
||||||
|
layoutDirection: LayoutDirection,
|
||||||
|
}
|
||||||
|
|
||||||
|
ClayArray :: struct($type: typeid) {
|
||||||
|
capacity: i32,
|
||||||
|
length: i32,
|
||||||
|
internalArray: [^]type,
|
||||||
|
}
|
||||||
|
|
||||||
|
ElementDeclaration :: struct {
|
||||||
|
id: ElementId,
|
||||||
|
layout: LayoutConfig,
|
||||||
|
backgroundColor: Color,
|
||||||
|
cornerRadius: CornerRadius,
|
||||||
|
aspectRatio: AspectRatioElementConfig,
|
||||||
|
image: ImageElementConfig,
|
||||||
|
floating: FloatingElementConfig,
|
||||||
|
custom: CustomElementConfig,
|
||||||
|
clip: ClipElementConfig,
|
||||||
|
border: BorderElementConfig,
|
||||||
|
userData: rawptr,
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorType :: enum EnumBackingType {
|
||||||
|
TextMeasurementFunctionNotProvided,
|
||||||
|
ArenaCapacityExceeded,
|
||||||
|
ElementsCapacityExceeded,
|
||||||
|
TextMeasurementCapacityExceeded,
|
||||||
|
DuplicateId,
|
||||||
|
FloatingContainerParentNotFound,
|
||||||
|
PercentageOver1,
|
||||||
|
InternalError,
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorData :: struct {
|
||||||
|
errorType: ErrorType,
|
||||||
|
errorText: String,
|
||||||
|
userData: rawptr,
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorHandler :: struct {
|
||||||
|
handler: proc "c" (errorData: ErrorData),
|
||||||
|
userData: rawptr,
|
||||||
|
}
|
||||||
|
|
||||||
|
Context :: struct {} // opaque structure, only use as a pointer
|
||||||
|
|
||||||
|
@(link_prefix = "Clay_", default_calling_convention = "c")
|
||||||
|
foreign Clay {
|
||||||
|
_OpenElement :: proc() ---
|
||||||
|
_CloseElement :: proc() ---
|
||||||
|
MinMemorySize :: proc() -> u32 ---
|
||||||
|
CreateArenaWithCapacityAndMemory :: proc(capacity: c.size_t, offset: [^]u8) -> Arena ---
|
||||||
|
SetPointerState :: proc(position: Vector2, pointerDown: bool) ---
|
||||||
|
Initialize :: proc(arena: Arena, layoutDimensions: Dimensions, errorHandler: ErrorHandler) -> ^Context ---
|
||||||
|
GetCurrentContext :: proc() -> ^Context ---
|
||||||
|
SetCurrentContext :: proc(ctx: ^Context) ---
|
||||||
|
UpdateScrollContainers :: proc(enableDragScrolling: bool, scrollDelta: Vector2, deltaTime: c.float) ---
|
||||||
|
SetLayoutDimensions :: proc(dimensions: Dimensions) ---
|
||||||
|
BeginLayout :: proc() ---
|
||||||
|
EndLayout :: proc() -> ClayArray(RenderCommand) ---
|
||||||
|
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 ---
|
||||||
|
GetScrollOffset :: proc() -> Vector2 ---
|
||||||
|
GetScrollContainerData :: proc(id: ElementId) -> ScrollContainerData ---
|
||||||
|
SetMeasureTextFunction :: proc(measureTextFunction: proc "c" (text: StringSlice, config: ^TextElementConfig, userData: rawptr) -> Dimensions, userData: rawptr) ---
|
||||||
|
SetQueryScrollOffsetFunction :: proc(queryScrollOffsetFunction: proc "c" (elementId: u32, userData: rawptr) -> Vector2, userData: rawptr) ---
|
||||||
|
RenderCommandArray_Get :: proc(array: ^ClayArray(RenderCommand), index: i32) -> ^RenderCommand ---
|
||||||
|
SetDebugModeEnabled :: proc(enabled: bool) ---
|
||||||
|
IsDebugModeEnabled :: proc() -> bool ---
|
||||||
|
SetCullingEnabled :: proc(enabled: bool) ---
|
||||||
|
GetMaxElementCount :: proc() -> i32 ---
|
||||||
|
SetMaxElementCount :: proc(maxElementCount: i32) ---
|
||||||
|
GetMaxMeasureTextCacheWordCount :: proc() -> i32 ---
|
||||||
|
SetMaxMeasureTextCacheWordCount :: proc(maxMeasureTextCacheWordCount: i32) ---
|
||||||
|
ResetMeasureTextCache :: proc() ---
|
||||||
|
}
|
||||||
|
|
||||||
|
@(link_prefix = "Clay_", default_calling_convention = "c", private)
|
||||||
|
foreign Clay {
|
||||||
|
_ConfigureOpenElement :: proc(config: ElementDeclaration) ---
|
||||||
|
_HashString :: proc(key: String, offset: u32, seed: u32) -> ElementId ---
|
||||||
|
_OpenTextElement :: proc(text: String, textConfig: ^TextElementConfig) ---
|
||||||
|
_StoreTextElementConfig :: proc(config: TextElementConfig) -> ^TextElementConfig ---
|
||||||
|
_GetParentElementId :: proc() -> u32 ---
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigureOpenElement :: proc(config: ElementDeclaration) -> bool {
|
||||||
|
_ConfigureOpenElement(config)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
@(deferred_none = _CloseElement)
|
||||||
|
UI :: proc() -> proc (config: ElementDeclaration) -> bool {
|
||||||
|
_OpenElement()
|
||||||
|
return ConfigureOpenElement
|
||||||
|
}
|
||||||
|
|
||||||
|
Text :: proc($text: string, config: ^TextElementConfig) {
|
||||||
|
wrapped := MakeString(text)
|
||||||
|
wrapped.isStaticallyAllocated = true
|
||||||
|
_OpenTextElement(wrapped, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 }
|
||||||
|
}
|
||||||
|
|
||||||
|
BorderOutside :: proc(width: u16) -> BorderWidth {
|
||||||
|
return {width, width, width, width, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
BorderAll :: proc(width: u16) -> BorderWidth {
|
||||||
|
return {width, width, width, width, width}
|
||||||
|
}
|
||||||
|
|
||||||
|
CornerRadiusAll :: proc(radius: f32) -> CornerRadius {
|
||||||
|
return CornerRadius{radius, radius, radius, radius}
|
||||||
|
}
|
||||||
|
|
||||||
|
SizingFit :: proc(sizeMinMax: SizingConstraintsMinMax) -> SizingAxis {
|
||||||
|
return SizingAxis{type = SizingType.Fit, constraints = {sizeMinMax = sizeMinMax}}
|
||||||
|
}
|
||||||
|
|
||||||
|
SizingGrow :: proc(sizeMinMax: SizingConstraintsMinMax) -> SizingAxis {
|
||||||
|
return SizingAxis{type = SizingType.Grow, constraints = {sizeMinMax = sizeMinMax}}
|
||||||
|
}
|
||||||
|
|
||||||
|
SizingFixed :: proc(size: c.float) -> SizingAxis {
|
||||||
|
return SizingAxis{type = SizingType.Fixed, constraints = {sizeMinMax = {size, size}}}
|
||||||
|
}
|
||||||
|
|
||||||
|
SizingPercent :: proc(sizePercent: c.float) -> SizingAxis {
|
||||||
|
return SizingAxis{type = SizingType.Percent, constraints = {sizePercent = sizePercent}}
|
||||||
|
}
|
||||||
|
|
||||||
|
MakeString :: proc(label: string) -> String {
|
||||||
|
return String{chars = raw_data(label), length = cast(c.int)len(label)}
|
||||||
|
}
|
||||||
|
|
||||||
|
ID :: proc(label: string, index: u32 = 0) -> ElementId {
|
||||||
|
return _HashString(MakeString(label), index, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
ID_LOCAL :: proc(label: string, index: u32 = 0) -> ElementId {
|
||||||
|
return _HashString(MakeString(label), index, _GetParentElementId())
|
||||||
|
}
|
||||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
+6
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://raw.githubusercontent.com/DanielGavin/ols/master/misc/odinfmt.schema.json",
|
||||||
|
"character_width": 180,
|
||||||
|
"sort_imports": true,
|
||||||
|
"tabs": false
|
||||||
|
}
|
||||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
+879
-973
File diff suppressed because it is too large
Load Diff
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
+70
-27
@@ -1,8 +1,11 @@
|
|||||||
package examples
|
package examples
|
||||||
|
|
||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
|
import "core:log"
|
||||||
|
import "core:mem"
|
||||||
import "core:os"
|
import "core:os"
|
||||||
import "core:sys/posix"
|
import "core:sys/posix"
|
||||||
|
|
||||||
import mdb "../../lmdb"
|
import mdb "../../lmdb"
|
||||||
|
|
||||||
// 0o660
|
// 0o660
|
||||||
@@ -10,34 +13,74 @@ DB_MODE :: posix.mode_t{.IWGRP, .IRGRP, .IWUSR, .IRUSR}
|
|||||||
DB_PATH :: "out/debug/lmdb_example_db"
|
DB_PATH :: "out/debug/lmdb_example_db"
|
||||||
|
|
||||||
main :: proc() {
|
main :: proc() {
|
||||||
environment: ^mdb.Env
|
//----- 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)
|
||||||
|
|
||||||
// Create environment for lmdb
|
// Default
|
||||||
mdb.panic_on_err(mdb.env_create(&environment))
|
track: mem.Tracking_Allocator
|
||||||
// Create directory for databases. Won't do anything if it already exists.
|
mem.tracking_allocator_init(&track, context.allocator)
|
||||||
// 0o774 gives all permissions for owner and group, read for everyone else.
|
context.allocator = mem.tracking_allocator(&track)
|
||||||
os.make_directory(DB_PATH, 0o774)
|
// Log a warning about any memory that was not freed by the end of the program.
|
||||||
// Open the database files (creates them if they don't already exist)
|
// This could be fine for some global state or it could be a memory leak.
|
||||||
mdb.panic_on_err(mdb.env_open(environment, DB_PATH, 0, DB_MODE))
|
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)
|
||||||
|
|
||||||
// Transactions
|
|
||||||
txn_handle: ^mdb.Txn
|
|
||||||
db_handle: mdb.Dbi
|
|
||||||
// Put transaction
|
|
||||||
key := 7
|
|
||||||
key_val := mdb.autoval(&key)
|
|
||||||
put_data := 12
|
|
||||||
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
|
environment: ^mdb.Env
|
||||||
get_data_val := mdb.nil_autoval(int)
|
|
||||||
mdb.panic_on_err(mdb.txn_begin(environment, nil, 0, &txn_handle))
|
// Create environment for lmdb
|
||||||
mdb.panic_on_err(mdb.get(txn_handle, db_handle, &key_val.raw, &get_data_val.raw))
|
mdb.panic_on_err(mdb.env_create(&environment))
|
||||||
mdb.panic_on_err(mdb.txn_commit(txn_handle))
|
// Create directory for databases. Won't do anything if it already exists.
|
||||||
data_cpy := mdb.autoval_get_data(&get_data_val)^
|
os.make_directory(DB_PATH)
|
||||||
fmt.println("Get result:", data_cpy)
|
// Open the database files (creates them if they don't already exist)
|
||||||
|
mdb.panic_on_err(mdb.env_open(environment, DB_PATH, {}, DB_MODE))
|
||||||
|
|
||||||
|
// Transactions
|
||||||
|
txn_handle: ^mdb.Txn
|
||||||
|
db_handle: mdb.Dbi
|
||||||
|
// Put transaction
|
||||||
|
key := 7
|
||||||
|
key_val := mdb.blittable_val(&key)
|
||||||
|
put_data := 12
|
||||||
|
put_data_val := mdb.blittable_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, {}))
|
||||||
|
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.blittable_copy(&data_val, int)
|
||||||
|
mdb.panic_on_err(mdb.txn_commit(txn_handle))
|
||||||
|
fmt.println("Get result:", data_cpy)
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+195
-153
@@ -164,24 +164,123 @@
|
|||||||
*/
|
*/
|
||||||
package lmdb
|
package lmdb
|
||||||
|
|
||||||
foreign import lib "system:lmdb"
|
|
||||||
|
|
||||||
import "core:c"
|
import "core:c"
|
||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
|
import "core:reflect"
|
||||||
import "core:sys/posix"
|
import "core:sys/posix"
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
// ----- Added Odin Helpers ------------------------
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Wrap a blittable value's bytes as an LMDB Val.
|
||||||
|
// T must be a contiguous type with no indirection (no pointers, slices, strings, maps, etc.).
|
||||||
|
blittable_val :: #force_inline proc(val_ptr: ^$T) -> Val {
|
||||||
|
fmt.assertf(
|
||||||
|
reflect.has_no_indirections(type_info_of(T)),
|
||||||
|
"blitval: type '%v' contains indirection and cannot be stored directly in LMDB",
|
||||||
|
typeid_of(T),
|
||||||
|
)
|
||||||
|
return Val{size_of(T), val_ptr}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reads a blittable T out of the LMDB memory map by copying it into caller
|
||||||
|
// storage. The returned T has no lifetime tie to the transaction.
|
||||||
|
blittable_copy :: #force_inline proc(val: ^Val, $T: typeid) -> T {
|
||||||
|
fmt.assertf(
|
||||||
|
reflect.has_no_indirections(type_info_of(T)),
|
||||||
|
"blitval_copy: type '%v' contains indirection and cannot be read directly from LMDB",
|
||||||
|
typeid_of(T),
|
||||||
|
)
|
||||||
|
return (cast(^T)val.data)^
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zero-copy pointer view into the LMDB memory map as a ^T.
|
||||||
|
// Useful for large blittable types where you want to read individual fields
|
||||||
|
// without copying the entire value (e.g. ptr.timestamp, ptr.flags).
|
||||||
|
// MUST NOT be written through — writes either segfault (default env mode)
|
||||||
|
// or silently corrupt the database (ENV_WRITEMAP).
|
||||||
|
// MUST NOT be retained past txn_commit, txn_abort, or any subsequent write
|
||||||
|
// operation on the same env — the pointer is invalidated.
|
||||||
|
blittable_view :: #force_inline proc(val: ^Val, $T: typeid) -> ^T {
|
||||||
|
fmt.assertf(
|
||||||
|
reflect.has_no_indirections(type_info_of(T)),
|
||||||
|
"blitval_view: type '%v' contains indirection and cannot be viewed directly from LMDB",
|
||||||
|
typeid_of(T),
|
||||||
|
)
|
||||||
|
return cast(^T)val.data
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap a slice of blittable elements as an LMDB Val for use with put/get.
|
||||||
|
// T must be a contiguous type with no indirection.
|
||||||
|
// The caller's slice must remain valid (not freed, not resized) for the
|
||||||
|
// duration of the put call that consumes this Val.
|
||||||
|
slice_val :: #force_inline proc(s: []$T) -> Val {
|
||||||
|
fmt.assertf(
|
||||||
|
reflect.has_no_indirections(type_info_of(T)),
|
||||||
|
"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.
|
||||||
|
slice_view :: #force_inline proc(val: ^Val, $T: typeid) -> []T {
|
||||||
|
fmt.assertf(
|
||||||
|
reflect.has_no_indirections(type_info_of(T)),
|
||||||
|
"slice_view: element type '%v' contains indirection and cannot be read directly from LMDB",
|
||||||
|
typeid_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
|
_ :: c
|
||||||
|
|
||||||
when ODIN_OS == .Windows {
|
when ODIN_OS == .Windows {
|
||||||
|
#panic("TODO: Compile windows .lib for lmdb")
|
||||||
mode_t :: c.int
|
mode_t :: c.int
|
||||||
} else {
|
|
||||||
mode_t :: posix.mode_t
|
|
||||||
}
|
|
||||||
|
|
||||||
when ODIN_OS == .Windows {
|
|
||||||
filehandle_t :: rawptr
|
filehandle_t :: rawptr
|
||||||
} else {
|
} 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
|
filehandle_t :: c.int
|
||||||
|
} else {
|
||||||
|
#panic("levlib/vendor/lmdb: unsupported OS target")
|
||||||
}
|
}
|
||||||
|
|
||||||
Env :: struct {}
|
Env :: struct {}
|
||||||
@@ -189,7 +288,7 @@ Env :: struct {}
|
|||||||
Txn :: struct {}
|
Txn :: struct {}
|
||||||
|
|
||||||
/** @brief A handle for an individual database in the DB environment. */
|
/** @brief A handle for an individual database in the DB environment. */
|
||||||
Dbi :: u32
|
Dbi :: c.uint
|
||||||
|
|
||||||
Cursor :: struct {}
|
Cursor :: struct {}
|
||||||
|
|
||||||
@@ -205,33 +304,8 @@ Cursor :: struct {}
|
|||||||
* Other data items can in theory be from 0 to 0xffffffff bytes long.
|
* Other data items can in theory be from 0 to 0xffffffff bytes long.
|
||||||
*/
|
*/
|
||||||
Val :: struct {
|
Val :: struct {
|
||||||
mv_size: uint, /**< size of the data item */
|
size: uint, /**< size of the data item */
|
||||||
mv_data: rawptr, /**< address of the data item */
|
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 */
|
/** @brief A callback function used to compare two keys in a database */
|
||||||
@@ -253,85 +327,65 @@ Cmp_Func :: #type proc "c" (_: ^Val, _: ^Val) -> i32
|
|||||||
*/
|
*/
|
||||||
Rel_Func :: #type proc "c" (item: ^Val, oldptr, newptr, relctx: rawptr)
|
Rel_Func :: #type proc "c" (item: ^Val, oldptr, newptr, relctx: rawptr)
|
||||||
|
|
||||||
/** @defgroup mdb_env Environment Flags
|
/** @defgroup mdb_env Environment Flags
|
||||||
* @{
|
* @{
|
||||||
*/
|
*/
|
||||||
/** mmap at a fixed address (experimental) */
|
Env_Flag :: enum u32 {
|
||||||
ENV_FIXEDMAP :: 0x01
|
FIXEDMAP = 0, /**< mmap at a fixed address (experimental) */
|
||||||
/** no environment directory */
|
NOSUBDIR = 14, /**< no environment directory */
|
||||||
ENV_NOSUBDIR :: 0x4000
|
NOSYNC = 16, /**< don't fsync after commit */
|
||||||
/** don't fsync after commit */
|
RDONLY = 17, /**< read only */
|
||||||
ENV_NOSYNC :: 0x10000
|
NOMETASYNC = 18, /**< don't fsync metapage after commit */
|
||||||
/** read only */
|
WRITEMAP = 19, /**< use writable mmap */
|
||||||
ENV_RDONLY :: 0x20000
|
MAPASYNC = 20, /**< use asynchronous msync when WRITEMAP is used */
|
||||||
/** don't fsync metapage after commit */
|
NOTLS = 21, /**< tie reader locktable slots to Txn objects instead of to threads */
|
||||||
ENV_NOMETASYNC :: 0x40000
|
NOLOCK = 22, /**< don't do any locking, caller must manage their own locks */
|
||||||
/** use writable mmap */
|
NORDAHEAD = 23, /**< don't do readahead (no effect on Windows) */
|
||||||
ENV_WRITEMAP :: 0x80000
|
NOMEMINIT = 24, /**< don't initialize malloc'd memory before writing to datafile */
|
||||||
/** use asynchronous msync when #MDB_WRITEMAP is used */
|
PREVSNAPSHOT = 25, /**< use the previous snapshot rather than the latest one */
|
||||||
ENV_MAPASYNC :: 0x100000
|
}
|
||||||
/** tie reader locktable slots to #MDB_txn objects instead of to threads */
|
Env_Flags :: distinct bit_set[Env_Flag;c.uint]
|
||||||
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
|
/** @defgroup mdb_dbi_open Database Flags
|
||||||
* @{
|
* @{
|
||||||
*/
|
*/
|
||||||
/** use reverse string keys */
|
Db_Flag :: enum u32 {
|
||||||
DB_REVERSEKEY :: 0x02
|
REVERSEKEY = 1, /**< use reverse string keys */
|
||||||
/** use sorted duplicates */
|
DUPSORT = 2, /**< use sorted duplicates */
|
||||||
DB_DUPSORT :: 0x04
|
INTEGERKEY = 3, /**< numeric keys in native byte order */
|
||||||
/** numeric keys in native byte order: either unsigned int or size_t.
|
DUPFIXED = 4, /**< with DUPSORT, sorted dup items have fixed size */
|
||||||
* The keys must all be of the same size. */
|
INTEGERDUP = 5, /**< with DUPSORT, dups are INTEGERKEY-style integers */
|
||||||
DB_INTEGERKEY :: 0x08
|
REVERSEDUP = 6, /**< with DUPSORT, use reverse string dups */
|
||||||
/** with #MDB_DUPSORT, sorted dup items have fixed size */
|
CREATE = 18, /**< create DB if not already existing */
|
||||||
DB_DUPFIXED :: 0x10
|
}
|
||||||
/** with #MDB_DUPSORT, dups are #MDB_INTEGERKEY-style integers */
|
Db_Flags :: distinct bit_set[Db_Flag;c.uint]
|
||||||
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
|
/** @defgroup mdb_put Write Flags
|
||||||
* @{
|
* @{
|
||||||
*/
|
*/
|
||||||
/** For put: Don't write if the key already exists. */
|
Write_Flag :: enum u32 {
|
||||||
WRITE_NOOVERWRITE :: 0x10
|
NOOVERWRITE = 4, /**< For put: Don't write if the key already exists */
|
||||||
/** Only for #MDB_DUPSORT<br>
|
NODUPDATA = 5, /**< For DUPSORT: don't write if the key and data pair already exist.
|
||||||
* For put: don't write if the key and data pair already exist.<br>
|
For mdb_cursor_del: remove all duplicate data items. */
|
||||||
* 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 */
|
||||||
WRITE_NODUPDATA :: 0x20
|
APPEND = 17, /**< Data is being appended, don't split full pages */
|
||||||
/** For mdb_cursor_put: overwrite the current key/data pair */
|
APPENDDUP = 18, /**< Duplicate data is being appended, don't split full pages */
|
||||||
WRITE_CURRENT :: 0x40
|
MULTIPLE = 19, /**< Store multiple data items in one call. Only for DUPFIXED. */
|
||||||
/** For put: Just reserve space for data, don't copy it. Return a
|
}
|
||||||
* pointer to the reserved space.
|
Write_Flags :: distinct bit_set[Write_Flag;c.uint]
|
||||||
*/
|
/** @} */
|
||||||
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
|
/** @defgroup mdb_copy Copy Flags
|
||||||
* @{
|
* @{
|
||||||
*/
|
*/
|
||||||
/** Compacting copy: Omit free space from copy, and renumber all
|
Copy_Flag :: enum u32 {
|
||||||
* pages sequentially.
|
COMPACT = 0, /**< Compacting copy: Omit free space from copy, and renumber all pages sequentially. */
|
||||||
*/
|
}
|
||||||
CP_COMPACT :: 0x01
|
Copy_Flags :: distinct bit_set[Copy_Flag;c.uint]
|
||||||
/* @} */
|
/** @} */
|
||||||
|
|
||||||
/** @brief Cursor Get operations.
|
/** @brief Cursor Get operations.
|
||||||
*
|
*
|
||||||
@@ -340,33 +394,24 @@ CP_COMPACT :: 0x01
|
|||||||
*/
|
*/
|
||||||
Cursor_Op :: enum c.int {
|
Cursor_Op :: enum c.int {
|
||||||
FIRST, /**< Position at first key/data item */
|
FIRST, /**< Position at first key/data item */
|
||||||
FIRST_DUP, /**< Position at first data item of current key.
|
FIRST_DUP, /**< Position at first data item of current key. Only for DUPSORT */
|
||||||
Only for #MDB_DUPSORT */
|
GET_BOTH, /**< Position at key/data pair. Only for DUPSORT */
|
||||||
GET_BOTH, /**< Position at key/data pair. Only for #MDB_DUPSORT */
|
GET_BOTH_RANGE, /**< Position at key, nearest data. Only for DUPSORT */
|
||||||
GET_BOTH_RANGE, /**< position at key, nearest data. Only for #MDB_DUPSORT */
|
|
||||||
GET_CURRENT, /**< Return key/data at current cursor position */
|
GET_CURRENT, /**< Return key/data at current cursor position */
|
||||||
GET_MULTIPLE, /**< Return up to a page of duplicate data items
|
GET_MULTIPLE, /**< Return up to a page of duplicate data items from current cursor position. Only for DUPFIXED */
|
||||||
from current cursor position. Move cursor to prepare
|
|
||||||
for #MDB_NEXT_MULTIPLE. Only for #MDB_DUPFIXED */
|
|
||||||
LAST, /**< Position at last key/data item */
|
LAST, /**< Position at last key/data item */
|
||||||
LAST_DUP, /**< Position at last data item of current key.
|
LAST_DUP, /**< Position at last data item of current key. Only for DUPSORT */
|
||||||
Only for #MDB_DUPSORT */
|
|
||||||
NEXT, /**< Position at next data item */
|
NEXT, /**< Position at next data item */
|
||||||
NEXT_DUP, /**< Position at next data item of current key.
|
NEXT_DUP, /**< Position at next data item of current key. Only for DUPSORT */
|
||||||
Only for #MDB_DUPSORT */
|
NEXT_MULTIPLE, /**< Return up to a page of duplicate data items from next cursor position. Only for DUPFIXED */
|
||||||
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 */
|
NEXT_NODUP, /**< Position at first data item of next key */
|
||||||
PREV, /**< Position at previous data item */
|
PREV, /**< Position at previous data item */
|
||||||
PREV_DUP, /**< Position at previous data item of current key.
|
PREV_DUP, /**< Position at previous data item of current key. Only for DUPSORT */
|
||||||
Only for #MDB_DUPSORT */
|
|
||||||
PREV_NODUP, /**< Position at last data item of previous key */
|
PREV_NODUP, /**< Position at last data item of previous key */
|
||||||
SET, /**< Position at specified key */
|
SET, /**< Position at specified key */
|
||||||
SET_KEY, /**< Position at specified key, return key + data */
|
SET_KEY, /**< Position at specified key, return key + data */
|
||||||
SET_RANGE, /**< Position at first key greater than or equal to specified key. */
|
SET_RANGE, /**< Position at first key greater than or equal to specified key */
|
||||||
PREV_MULTIPLE, /**< Position at previous page and return up to
|
PREV_MULTIPLE, /**< Position at previous page and return up to a page of duplicate data items. Only for DUPFIXED */
|
||||||
a page of duplicate data items. Only for #MDB_DUPFIXED */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Error :: enum c.int {
|
Error :: enum c.int {
|
||||||
@@ -419,33 +464,28 @@ Error :: enum c.int {
|
|||||||
BAD_VALSIZE = -30781,
|
BAD_VALSIZE = -30781,
|
||||||
/** The specified DBI was changed unexpectedly */
|
/** The specified DBI was changed unexpectedly */
|
||||||
BAD_DBI = -30780,
|
BAD_DBI = -30780,
|
||||||
|
/** Unexpected problem - txn should abort */
|
||||||
|
PROBLEM = -30779,
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @brief Statistics for a database in the environment */
|
/** @brief Statistics for a database in the environment */
|
||||||
Stat :: struct {
|
Stat :: struct {
|
||||||
ms_psize: u32,
|
psize: u32, /**< Size of a database page. This is currently the same for all databases. */
|
||||||
/**< Size of a database page.
|
depth: u32, /**< Depth (height) of the B-tree */
|
||||||
This is currently the same for all databases. */
|
branch_pages: uint, /**< Number of internal (non-leaf) pages */
|
||||||
ms_depth: u32,
|
leaf_pages: uint, /**< Number of leaf pages */
|
||||||
/**< Depth (height) of the B-tree */
|
overflow_pages: uint, /**< Number of overflow pages */
|
||||||
ms_branch_pages: uint,
|
entries: uint, /**< Number of data items */
|
||||||
/**< 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 */
|
/** @brief Information about the environment */
|
||||||
Env_Info :: struct {
|
Env_Info :: struct {
|
||||||
me_mapaddr: rawptr, /**< Address of map, if fixed */
|
mapaddr: rawptr, /**< Address of map, if fixed */
|
||||||
me_mapsize: uint, /**< Size of the data memory map */
|
mapsize: uint, /**< Size of the data memory map */
|
||||||
me_last_pgno: uint, /**< ID of the last used page */
|
last_pgno: uint, /**< ID of the last used page */
|
||||||
me_last_txnid: uint, /**< ID of the last committed transaction */
|
last_txnid: uint, /**< ID of the last committed transaction */
|
||||||
me_maxreaders: u32, /**< max reader slots in the environment */
|
maxreaders: u32, /**< max reader slots in the environment */
|
||||||
me_numreaders: u32, /**< max reader slots used in the environment */
|
numreaders: u32, /**< max reader slots used in the environment */
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @brief A callback function for most LMDB assert() failures,
|
/** @brief A callback function for most LMDB assert() failures,
|
||||||
@@ -454,7 +494,7 @@ Env_Info :: struct {
|
|||||||
* @param[in] env An environment handle returned by #mdb_env_create().
|
* @param[in] env An environment handle returned by #mdb_env_create().
|
||||||
* @param[in] msg The assertion message, not including newline.
|
* @param[in] msg The assertion message, not including newline.
|
||||||
*/
|
*/
|
||||||
Assert_Func :: proc "c" (_: ^Env, _: cstring)
|
Assert_Func :: #type proc "c" (_: ^Env, _: cstring)
|
||||||
|
|
||||||
/** @brief A callback function used to print a message from the library.
|
/** @brief A callback function used to print a message from the library.
|
||||||
*
|
*
|
||||||
@@ -462,7 +502,7 @@ Assert_Func :: proc "c" (_: ^Env, _: cstring)
|
|||||||
* @param[in] ctx An arbitrary context pointer for the callback.
|
* @param[in] ctx An arbitrary context pointer for the callback.
|
||||||
* @return < 0 on failure, >= 0 on success.
|
* @return < 0 on failure, >= 0 on success.
|
||||||
*/
|
*/
|
||||||
Msg_Func :: proc "c" (_: cstring, _: rawptr) -> i32
|
Msg_Func :: #type proc "c" (_: cstring, _: rawptr) -> i32
|
||||||
|
|
||||||
@(default_calling_convention = "c", link_prefix = "mdb_")
|
@(default_calling_convention = "c", link_prefix = "mdb_")
|
||||||
foreign lib {
|
foreign lib {
|
||||||
@@ -623,7 +663,7 @@ foreign lib {
|
|||||||
* </ul>
|
* </ul>
|
||||||
*/
|
*/
|
||||||
@(require_results)
|
@(require_results)
|
||||||
env_open :: proc(env: ^Env, path: cstring, flags: u32, mode: mode_t) -> Error ---
|
env_open :: proc(env: ^Env, path: cstring, flags: Env_Flags, mode: mode_t) -> Error ---
|
||||||
|
|
||||||
/** @brief Copy an LMDB environment to the specified path.
|
/** @brief Copy an LMDB environment to the specified path.
|
||||||
*
|
*
|
||||||
@@ -682,7 +722,7 @@ foreign lib {
|
|||||||
* @return A non-zero error value on failure and 0 on success.
|
* @return A non-zero error value on failure and 0 on success.
|
||||||
*/
|
*/
|
||||||
@(require_results)
|
@(require_results)
|
||||||
env_copy2 :: proc(env: ^Env, path: cstring, flags: u32) -> Error ---
|
env_copy2 :: proc(env: ^Env, path: cstring, flags: Copy_Flags) -> Error ---
|
||||||
|
|
||||||
/** @brief Copy an LMDB environment to the specified file descriptor,
|
/** @brief Copy an LMDB environment to the specified file descriptor,
|
||||||
* with options.
|
* with options.
|
||||||
@@ -702,7 +742,7 @@ foreign lib {
|
|||||||
* @return A non-zero error value on failure and 0 on success.
|
* @return A non-zero error value on failure and 0 on success.
|
||||||
*/
|
*/
|
||||||
@(require_results)
|
@(require_results)
|
||||||
env_copyfd2 :: proc(env: ^Env, fd: filehandle_t, flags: u32) -> Error ---
|
env_copyfd2 :: proc(env: ^Env, fd: filehandle_t, flags: Copy_Flags) -> Error ---
|
||||||
|
|
||||||
/** @brief Return statistics about the LMDB environment.
|
/** @brief Return statistics about the LMDB environment.
|
||||||
*
|
*
|
||||||
@@ -767,7 +807,7 @@ foreign lib {
|
|||||||
* </ul>
|
* </ul>
|
||||||
*/
|
*/
|
||||||
@(require_results)
|
@(require_results)
|
||||||
env_set_flags :: proc(env: ^Env, flags: u32, onoff: i32) -> Error ---
|
env_set_flags :: proc(env: ^Env, flags: Env_Flags, onoff: i32) -> Error ---
|
||||||
|
|
||||||
/** @brief Get environment flags.
|
/** @brief Get environment flags.
|
||||||
*
|
*
|
||||||
@@ -780,7 +820,7 @@ foreign lib {
|
|||||||
* </ul>
|
* </ul>
|
||||||
*/
|
*/
|
||||||
@(require_results)
|
@(require_results)
|
||||||
env_get_flags :: proc(env: ^Env, flags: ^u32) -> Error ---
|
env_get_flags :: proc(env: ^Env, flags: ^Env_Flags) -> Error ---
|
||||||
|
|
||||||
/** @brief Return the path that was used in #mdb_env_open().
|
/** @brief Return the path that was used in #mdb_env_open().
|
||||||
*
|
*
|
||||||
@@ -973,7 +1013,7 @@ foreign lib {
|
|||||||
* </ul>
|
* </ul>
|
||||||
*/
|
*/
|
||||||
@(require_results)
|
@(require_results)
|
||||||
txn_begin :: proc(env: ^Env, parent: ^Txn, flags: u32, txn: ^^Txn) -> Error ---
|
txn_begin :: proc(env: ^Env, parent: ^Txn, flags: Env_Flags, txn: ^^Txn) -> Error ---
|
||||||
|
|
||||||
/** @brief Returns the transaction's #MDB_env
|
/** @brief Returns the transaction's #MDB_env
|
||||||
*
|
*
|
||||||
@@ -1126,7 +1166,7 @@ foreign lib {
|
|||||||
* </ul>
|
* </ul>
|
||||||
*/
|
*/
|
||||||
@(require_results)
|
@(require_results)
|
||||||
dbi_open :: proc(txn: ^Txn, name: cstring, flags: u32, dbi: ^Dbi) -> Error ---
|
dbi_open :: proc(txn: ^Txn, name: cstring, flags: Db_Flags, dbi: ^Dbi) -> Error ---
|
||||||
|
|
||||||
/** @brief Retrieve statistics for a database.
|
/** @brief Retrieve statistics for a database.
|
||||||
*
|
*
|
||||||
@@ -1151,7 +1191,7 @@ foreign lib {
|
|||||||
* @return A non-zero error value on failure and 0 on success.
|
* @return A non-zero error value on failure and 0 on success.
|
||||||
*/
|
*/
|
||||||
@(require_results)
|
@(require_results)
|
||||||
dbi_flags :: proc(txn: ^Txn, dbi: Dbi, flags: ^u32) -> Error ---
|
dbi_flags :: proc(txn: ^Txn, dbi: Dbi, flags: ^Db_Flags) -> Error ---
|
||||||
|
|
||||||
/** @brief Close a database handle. Normally unnecessary. Use with care:
|
/** @brief Close a database handle. Normally unnecessary. Use with care:
|
||||||
*
|
*
|
||||||
@@ -1229,6 +1269,7 @@ foreign lib {
|
|||||||
@(require_results)
|
@(require_results)
|
||||||
set_dupsort :: proc(txn: ^Txn, dbi: Dbi, cmp: Cmp_Func) -> Error ---
|
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.
|
/** @brief Set a relocation function for a #MDB_FIXEDMAP database.
|
||||||
*
|
*
|
||||||
* @todo The relocation function is called whenever it is necessary to move the data
|
* @todo The relocation function is called whenever it is necessary to move the data
|
||||||
@@ -1250,6 +1291,7 @@ foreign lib {
|
|||||||
@(require_results)
|
@(require_results)
|
||||||
set_relfunc :: proc(txn: ^Txn, dbi: Dbi, rel: Rel_Func) -> Error ---
|
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.
|
/** @brief Set a context pointer for a #MDB_FIXEDMAP database's relocation function.
|
||||||
*
|
*
|
||||||
* See #mdb_set_relfunc and #MDB_rel_func for more details.
|
* See #mdb_set_relfunc and #MDB_rel_func for more details.
|
||||||
@@ -1344,7 +1386,7 @@ foreign lib {
|
|||||||
* </ul>
|
* </ul>
|
||||||
*/
|
*/
|
||||||
@(require_results)
|
@(require_results)
|
||||||
put :: proc(txn: ^Txn, dbi: Dbi, key: ^Val, data: ^Val, flags: u32) -> Error ---
|
put :: proc(txn: ^Txn, dbi: Dbi, key: ^Val, data: ^Val, flags: Write_Flags) -> Error ---
|
||||||
|
|
||||||
/** @brief Delete items from a database.
|
/** @brief Delete items from a database.
|
||||||
*
|
*
|
||||||
@@ -1517,7 +1559,7 @@ foreign lib {
|
|||||||
* </ul>
|
* </ul>
|
||||||
*/
|
*/
|
||||||
@(require_results)
|
@(require_results)
|
||||||
cursor_put :: proc(cursor: ^Cursor, key: ^Val, data: ^Val, flags: u32) -> Error ---
|
cursor_put :: proc(cursor: ^Cursor, key: ^Val, data: ^Val, flags: Write_Flags) -> Error ---
|
||||||
|
|
||||||
/** @brief Delete current key/data pair
|
/** @brief Delete current key/data pair
|
||||||
*
|
*
|
||||||
@@ -1541,7 +1583,7 @@ foreign lib {
|
|||||||
* </ul>
|
* </ul>
|
||||||
*/
|
*/
|
||||||
@(require_results)
|
@(require_results)
|
||||||
cursor_del :: proc(cursor: ^Cursor, flags: u32) -> Error ---
|
cursor_del :: proc(cursor: ^Cursor, flags: Write_Flags) -> Error ---
|
||||||
|
|
||||||
/** @brief Return count of duplicates for current key.
|
/** @brief Return count of duplicates for current key.
|
||||||
*
|
*
|
||||||
|
|||||||
Reference in New Issue
Block a user