Compare commits

..

5 Commits

Author SHA1 Message Date
Zachary Levy ea19b83ba4 Cleanup 2026-04-21 16:59:11 -07:00
Zachary Levy 7650b90d91 Comment cleanup 2026-04-21 16:09:40 -07:00
Zachary Levy ba522fa051 QR code improvements 2026-04-21 15:51:17 -07:00
Zachary Levy a4623a13b5 Basic texture support 2026-04-21 13:46:41 -07:00
Zachary Levy f85187eff3 Clean up memory management 2026-04-20 22:39:21 -07:00
79 changed files with 4544 additions and 8449 deletions
-10
View File
@@ -75,16 +75,6 @@
"command": "odin run draw/examples -debug -out=out/debug/draw-examples -- textures", "command": "odin run draw/examples -debug -out=out/debug/draw-examples -- textures",
"cwd": "$ZED_WORKTREE_ROOT", "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", "label": "Run qrcode basic example",
"command": "odin run qrcode/examples -debug -out=out/debug/qrcode-examples -- basic", "command": "odin run qrcode/examples -debug -out=out/debug/qrcode-examples -- basic",
+200 -409
View File
@@ -5,60 +5,38 @@ Clay UI integration.
## Current state ## Current state
The renderer uses a single unified `Core_2D` (`TRIANGLELIST` pipeline) with two submission The renderer uses a single unified `Pipeline_2D_Base` (`TRIANGLELIST` pipeline) with two submission
modes dispatched by a push constant: modes dispatched by a push constant:
- **Mode 0 (Tessellated):** Vertex buffer contains real geometry. Used for text (indexed draws into - **Mode 0 (Tessellated):** Vertex buffer contains real geometry. Used for text (indexed draws into
SDL_ttf atlas textures), single-pixel points (`tess.pixel`), arbitrary user geometry SDL_ttf atlas textures), axis-aligned sharp-corner rectangles (already optimal as 2 triangles),
(`tess.triangle`, `tess.triangle_aa`, `tess.triangle_lines`, `tess.triangle_fan`, per-vertex color gradients (`rectangle_gradient`, `circle_gradient`), angular-clipped circle
`tess.triangle_strip`), and any raw vertex geometry submitted via `prepare_shape`. The fragment sectors (`circle_sector`), and arbitrary user geometry (`triangle`, `triangle_fan`,
shader premultiplies the texture sample (`t.rgb *= t.a`) and computes `out = color * t`. `triangle_strip`). The fragment shader computes `out = color * texture(tex, uv)`.
- **Mode 1 (SDF):** A static 6-vertex unit-quad buffer is drawn instanced, with per-primitive - **Mode 1 (SDF):** A static 6-vertex unit-quad buffer is drawn instanced, with per-primitive
`Core_2D_Primitive` structs (96 bytes each) uploaded each frame to a GPU storage buffer. The vertex `Primitive` structs uploaded each frame to a GPU storage buffer. The vertex shader reads
shader reads `primitives[gl_InstanceIndex]`, computes world-space position from unit quad corners + `primitives[gl_InstanceIndex]`, computes world-space position from unit quad corners + primitive
primitive bounds. The fragment shader dispatches on `Shape_Kind` (encoded in the low byte of bounds. The fragment shader dispatches on `Shape_Kind` to evaluate the correct signed distance
`Core_2D_Primitive.flags`) to evaluate one of four signed distance functions: function analytically.
- **RRect** (kind 1) — `sdRoundedBox` with per-corner radii. Covers rectangles (sharp or rounded),
circles (uniform radii = half-size), and line segments / capsules (rotated RRect with uniform
radii = half-thickness). Covers filled, outlined, textured, and gradient-filled variants.
- **NGon** (kind 2) — `sdRegularPolygon` for regular N-sided polygons.
- **Ellipse** (kind 3) — `sdEllipseApprox`, an approximate ellipse SDF suitable for UI rendering.
- **Ring_Arc** (kind 4) — annular ring with optional angular clipping via pre-computed edge
normals. Covers full rings, partial arcs, and pie slices (`inner_radius = 0`).
All SDF shapes support fill, outline, solid color, 2-color linear gradients, 2-color radial Seven SDF shape kinds are implemented:
gradients, and texture fills via `Shape_Flags` (see `core_2d.odin`). The texture UV rect
(`uv_rect: [4]f32`) and the gradient/outline parameters (`effects: Gradient_Outline`) live in their
own 16-byte slots in `Core_2D_Primitive`, so a primitive can carry texture and outline simultaneously.
Gradient and texture remain mutually exclusive at the fill-source level (a Brush variant chooses one
or the other) since they share the worst-case fragment-shader register path.
All SDF shapes produce mathematically exact curves with analytical anti-aliasing via `smoothstep` 1. **RRect** — rounded rectangle with per-corner radii (iq's `sdRoundedBox`)
no tessellation, no piecewise-linear approximation. A rounded rectangle is 1 primitive (96 bytes) 2. **Circle** — filled or stroked circle
instead of ~250 vertices (~5000 bytes). 3. **Ellipse** — exact signed-distance ellipse (iq's iterative `sdEllipse`)
4. **Segment** — capsule-style line segment with rounded caps
5. **Ring_Arc** — annular ring with angular clipping for arcs
6. **NGon** — regular polygon with arbitrary side count and rotation
7. **Polyline** — decomposed into independent `Segment` primitives per adjacent point pair
The main pipeline's register budget is **≤24 registers** (see "Main/effects split: register pressure" All SDF shapes support fill and stroke modes via `Shape_Flags`, and produce mathematically exact
in the pipeline plan below for the full cliff/margin analysis and SBC architecture context). curves with analytical anti-aliasing via `smoothstep` — no tessellation, no piecewise-linear
The fragment shader's estimated peak footprint is ~2226 fp32 VGPRs (~1622 fp16 VGPRs on architectures approximation. A rounded rectangle is 1 primitive (64 bytes) instead of ~250 vertices (~5000 bytes).
with native mediump) via manual live-range analysis. The dominant peak is the Ring_Arc kind path
(wedge normals + inner/outer radii + dot-product temporaries live simultaneously with carried state
like `f_color`, `f_uv_rect`/`f_effects`, and `half_size`). RRect is 12 regs lower (`corner_radii` vec4
replaces the separate inner/outer + normal pairs). NGon and Ellipse are lighter still. Real compilers
apply live-range coalescing, mediump-to-fp16 promotion, and rematerialization that typically shave
24 regs from hand-counted estimates — the conservative 26-reg upper bound is expected to compile
down to within the 24-register budget, but this must be verified with `malioc` (see "Verifying
register counts" below). On V3D and Bifrost architectures (16-register cliff), the compiler
statically allocates registers for the worst-case path (Ring_Arc) regardless of which kind any given
fragment actually evaluates, so all fragments pay the occupancy cost of the heaviest branch. This is
a documented limitation, not a design constraint (see "Known limitations: V3D and Bifrost" below).
MSAA is intentionally not supported. SDF text and shapes compute fragment coverage analytically MSAA is opt-in (default `._1`, no MSAA) via `Init_Options.msaa_samples`. SDF rendering does not
via `smoothstep`, so they don't benefit from multisampling. Tessellated user geometry submitted via benefit from MSAA because fragment coverage is computed analytically. MSAA remains useful for text
`prepare_shape` is rendered without anti-aliasing — if AA is required for tessellated content, the glyph edges and tessellated user geometry if desired.
caller must render it to their own offscreen target and submit the result as a texture. This
decision matches RAD Debugger's architecture and aligns with the SBC target (Mali Valhall, where
MSAA's per-tile bandwidth multiplier is expensive).
## 2D rendering pipeline plan ## 2D rendering pipeline plan
@@ -72,23 +50,22 @@ primitives and effects can be added to the library without architectural changes
The 2D renderer uses three GPU pipelines, split by **register pressure** (main vs effects) and The 2D renderer uses three GPU pipelines, split by **register pressure** (main vs effects) and
**render-pass structure** (everything vs backdrop): **render-pass structure** (everything vs backdrop):
1. **Main pipeline** — shapes (SDF and tessellated), text, and textured rectangles. Register budget: 1. **Main pipeline** — shapes (SDF and tessellated), text, and textured rectangles. Low register
**≤24 registers** (full occupancy on Valhall and all desktop GPUs). Handles 90%+ of all fragments footprint (~1824 registers per thread). Runs at full GPU occupancy on every architecture.
in a typical frame. Handles 90%+ of all fragments in a typical frame.
2. **Effects pipeline** — drop shadows, inner shadows, outer glow, and similar ALU-bound blur 2. **Effects pipeline** — drop shadows, inner shadows, outer glow, and similar ALU-bound blur
effects. Register budget: **≤56 registers** (targets Valhall's second cliff at 64; reduced effects. Medium register footprint (~4860 registers). Each effects primitive includes the base
occupancy at the first cliff is accepted by design). Each effects primitive includes the base
shape's SDF so that it can draw both the effect and the shape in a single fragment pass, avoiding shape's SDF so that it can draw both the effect and the shape in a single fragment pass, avoiding
redundant overdraw. Separated from the main pipeline to protect main-pipeline occupancy on redundant overdraw. Separated from the main pipeline to protect main-pipeline occupancy on
low-end hardware (see register analysis below). low-end hardware (see register analysis below).
3. **Backdrop pipeline** — frosted glass, refraction, and any effect that samples the current render 3. **Backdrop pipeline** — frosted glass, refraction, and any effect that samples the current render
target as input. Implemented as a multi-pass sequence (downsample, separable blur, composite), target as input. Implemented as a multi-pass sequence (downsample, separable blur, composite),
where each individual sub-pass has a register budget of **≤24 registers** (full occupancy on where each individual pass has a low-to-medium register footprint (~1540 registers). Separated
Valhall). Separated from the other pipelines because it structurally requires ending the current from the other pipelines because it structurally requires ending the current render pass and
render pass and copying the render target before any backdrop-sampling fragment can execute — a copying the render target before any backdrop-sampling fragment can execute — a command-buffer-
command-buffer-level boundary that cannot be avoided regardless of shader complexity. level boundary that cannot be avoided regardless of shader complexity.
A typical UI frame with no effects uses 1 pipeline bind and 0 switches. A frame with drop shadows A typical UI frame with no effects uses 1 pipeline bind and 0 switches. A frame with drop shadows
uses 2 pipelines and 1 switch. A frame with shadows and frosted glass uses all 3 pipelines and 2 uses 2 pipelines and 1 switch. A frame with shadows and frosted glass uses all 3 pipelines and 2
@@ -104,113 +81,56 @@ code) or many per-primitive-type pipelines (no branching overhead, lean per-shad
A GPU shader core has a fixed register pool shared among all concurrent threads. The compiler A GPU shader core has a fixed register pool shared among all concurrent threads. The compiler
allocates registers pessimistically based on the worst-case path through the shader. If the shader allocates registers pessimistically based on the worst-case path through the shader. If the shader
contains both a 24-register RRect SDF and a 56-register drop-shadow blur, _every_ fragment — even contains both a 20-register RRect SDF and a 48-register drop-shadow blur, _every_ fragment — even
trivial RRects — is allocated 56 registers. This directly reduces **occupancy** (the number of trivial RRects — is allocated 48 registers. This directly reduces **occupancy** (the number of
warps/wavefronts that can run simultaneously), which reduces the GPU's ability to hide memory warps/wavefronts that can run simultaneously), which reduces the GPU's ability to hide memory
latency. latency.
Each GPU architecture has discrete **occupancy cliffs**register counts above which the number of Each GPU architecture has a **register cliff**a threshold above which occupancy starts dropping.
concurrent threads drops in a step. Below the cliff, adding registers has zero occupancy cost. One Below the cliff, adding registers has zero occupancy cost.
register over, throughput drops sharply.
**Target architecture: ARM Mali Valhall (32-register first cliff).** The binding constraint for our On consumer Ampere/Ada GPUs (RTX 30xx/40xx, 65,536 regs/SM, max 1,536 threads/SM, cliff at ~43 regs):
register budgets comes from the SBC (single-board computer) market, where Mali Valhall is the
dominant current GPU architecture:
- **RK3588-class boards** (Orange Pi 5, Radxa Rock 5, Khadas Edge 2, NanoPi R6, Banana Pi M7) ship | Register allocation | Reg-limited threads | Actual (hw-capped) | Occupancy |
**Mali-G610** (Valhall). This is the dominant non-Pi SBC platform. First occupancy cliff at **32 | ----------------------- | ------------------- | ------------------ | --------- |
registers**, second cliff at **64 registers**. | 20 regs (main pipeline) | 3,276 | 1,536 | 100% |
- **ARM Mali Valhall** (G57, G77, G78, G610, G710, G715; 2019+) and **5th-gen / Mali-G1** (2024+): | 32 regs | 2,048 | 1,536 | 100% |
same cliff structure — first at 32, second at 64. | 48 regs (effects) | 1,365 | 1,365 | ~89% |
- **ARM Mali Bifrost** (G31, G51, G52, G71, G72, G76; ~20162018): first cliff at **16 registers**.
Legacy; found on older budget boards (Allwinner H6/H618, Amlogic S922X). See Known limitations
below.
- **Broadcom V3D 4.x / 7.x** (Raspberry Pi 4 / Pi 5): first cliff at **16 registers**. Outlier in
the current SBC market. See Known limitations below.
- **Apple M3+**: Dynamic Caching (register file virtualization) eliminates the static cliff entirely.
Register allocation happens at runtime based on actual usage.
- **Qualcomm Adreno**: dynamic register allocation with soft thresholds; no hard cliff.
- **NVIDIA desktop** (Ampere/Ada): cliff at ~43 registers. Not a constraint for any of our pipelines.
**Register budgets and margin.** We target Valhall's 32-register first cliff for the main and On Volta/A100 GPUs (65,536 regs/SM, max 2,048 threads/SM, cliff at ~32 regs):
backdrop pipelines, and Valhall's 64-register second cliff for the effects pipeline, each with **8
registers of margin**:
| Pipeline | Cliff targeted | Margin | Register budget | Rationale | | Register allocation | Reg-limited threads | Actual (hw-capped) | Occupancy |
| ------------------- | ---------------------- | ------ | ----------------- | --------------------------------------------------------------------------------------------- | | ----------------------- | ------------------- | ------------------ | --------- |
| Main pipeline | 32 (Valhall 1st cliff) | 8 | **≤24 regs** | Handles 90%+ of frame fragments; must run at full occupancy | | 20 regs (main pipeline) | 3,276 | 2,048 | 100% |
| Backdrop sub-passes | 32 (Valhall 1st cliff) | 8 | **≤24 regs** each | Multi-pass structure keeps each pass small; no reason to give up occupancy | | 32 regs | 2,048 | 2,048 | 100% |
| Effects pipeline | 64 (Valhall 2nd cliff) | 8 | **≤56 regs** | Reduced occupancy at 1st cliff accepted by design — the entire point of splitting effects out | | 48 regs (effects) | 1,365 | 1,365 | ~67% |
**Why 8 registers of margin.** Targeting the cliff exactly is fragile. Three forces push register On low-end mobile (ARM Mali Bifrost/Valhall, 64 regs/thread, cliff fixed at 32 regs):
counts upward over a shader's lifetime:
1. **Compiler version changes.** Mali driver releases (r35p0 → r55p0 etc.) ship new register | Register allocation | Occupancy |
allocators. Shaders typically drift ±23 registers between versions on unchanged source. | -------------------- | -------------------------- |
2. **Feature additions.** Each new effect, flag, or uniform adds 14 live registers. A new gradient | 032 regs (main) | 100% (full thread count) |
mode or outline option lands in this range. | 3364 regs (effects) | ~50% (thread count halves) |
3. **Precision regressions.** A `mediump` demoted to `highp` (by bug fix, compiler heuristic change,
or a contributor not knowing) costs 2 registers per affected `vec4`.
Realistic creep over a couple of years is 48 registers. The cost of conservatism is zero — a shader Mali's cliff at 32 registers is the binding constraint. On desktop the occupancy difference between
at 24 regs runs identically to one at 32 on every Valhall device. The cost of crossing the cliff is 20 and 48 registers is modest (89100%); on Mali it is a hard 2× throughput reduction. The
a 2× throughput drop with no warning. Asymmetric costs justify a generous margin. main/effects split protects 90%+ of a frame's fragments (shapes, text, textures) from the effects
pipeline's register cost.
**Why the main/effects split exists.** If the main pipeline shader contained both the 24-register For the effects pipeline's drop-shadow shader — erf-approximation blur math with several texture
SDF path and the ~50-register drop-shadow blur, every fragment — even trivial RRects — would be fetches — 50% occupancy on Mali roughly halves throughput. At 4K with 1.5× overdraw (~12.4M
allocated ~50 registers. On Valhall this crosses the 32-register first cliff, halving occupancy for
90%+ of the frame's fragments. Separating effects into their own pipeline means the main pipeline
stays at ≤24 registers (full Valhall occupancy), and only the small fraction of fragments that
actually render effects (~510% in a typical UI) run at reduced occupancy.
For the effects pipeline's drop-shadow shader — analytical erf-approximation blur (~80 FLOPs, no
texture samples) — 50% occupancy on Valhall roughly halves throughput. At 4K with 1.5× overdraw (~12.4M
fragments), a single unified shader containing the shadow branch would cost ~4ms instead of ~2ms on fragments), a single unified shader containing the shadow branch would cost ~4ms instead of ~2ms on
Valhall. This is a per-frame multiplier even when the heavy branch is never taken, because the low-end mobile. This is a per-frame multiplier even when the heavy branch is never taken, because the
compiler allocates registers for the worst-case path. compiler allocates registers for the worst-case path.
The effects pipeline's ≤56-register budget keeps it under Valhall's second cliff at 64, yielding All main-pipeline members (SDF shapes, tessellated geometry, text, textured rectangles) cluster at
5067% occupancy on effected shapes. This is acceptable for the small fraction of frame fragments 1224 registers — below the cliff on every architecture — so unifying them costs nothing in
that effects cover. occupancy.
**Note on Apple M3+ GPUs:** Apple's M3 Dynamic Caching allocates registers at runtime based on **Note on Apple M3+ GPUs:** Apple's M3 introduces Dynamic Caching (register file virtualization),
actual usage rather than worst-case. This eliminates the static register-pressure argument on M3 and which allocates registers at runtime based on actual usage rather than worst-case. This weakens the
later, but the split remains useful for isolating blur ALU complexity and keeping the backdrop static register-pressure argument on M3 and later, but the split remains useful for isolating blur
texture-copy out of the main render pass. ALU complexity and keeping the backdrop texture-copy out of the main render pass.
**Note on NVIDIA desktop GPUs:** On consumer Ampere/Ada (cliff at ~43 regs), even the effects
pipeline's ≤56-register budget only reduces occupancy to ~89% — well within noise. On Volta/A100
(cliff at ~32 regs), the effects pipeline drops to ~67%. In both cases the main pipeline runs at
100% occupancy. Desktop GPUs are not the binding constraint; Valhall is.
#### Known limitations: V3D and Bifrost (16-register cliff)
Broadcom V3D 4.x / 7.x (Raspberry Pi 4 / Pi 5) and ARM Mali Bifrost (G31, G51, G52, G71, G72, G76)
have a first occupancy cliff at **16 registers**. All three of our pipelines exceed this cliff — even
the main pipeline's ≤24-register budget is above 16. On these architectures, every shader runs at
reduced occupancy regardless of which shape kind or effect is active.
Restoring full occupancy on V3D / Bifrost would require a fundamentally different shader
architecture: per-shape-kind pipeline splitting (one pipeline per SDF kind, each with a minimal
register footprint under 16). This conflicts with the unified-pipeline design that enables single
draw calls per scissor, submission-order Z preservation, and low PSO compilation cost. It would
effectively be the GPUI-style approach whose tradeoffs are analyzed in "Why not per-primitive-type
pipelines" below.
We treat this as a documented limitation, not a design constraint. The 16-register cliff is legacy
(Bifrost) or a single-vendor outlier (V3D). The dominant current SBC platform (RK3588 / Mali-G610)
and all mainstream mobile and desktop GPUs have cliffs at 32 or higher. The long-term direction in
GPU architecture is toward eliminating static cliffs entirely (Apple Dynamic Caching, Adreno dynamic
allocation).
#### Verifying register counts
The register estimates in this document are hand-counted via manual live-range analysis (see Current
state). Shader changes that affect the main or effects pipeline should be verified with `malioc`
(ARM Mali Offline Compiler) against current Valhall driver versions before merging. `malioc` reports
exact register allocation, spilling, and occupancy for each Mali generation. On desktop, Radeon GPU
Analyzer (RGA) and NVIDIA Nsight provide equivalent data. Replacing the hand-counted estimates with
measured `malioc` numbers is a follow-up task.
#### Backdrop split: render-pass structure #### Backdrop split: render-pass structure
@@ -220,11 +140,10 @@ render target must be copied to a separate texture via `CopyGPUTextureToTexture`
level operation that requires ending the current render pass. This boundary exists regardless of level operation that requires ending the current render pass. This boundary exists regardless of
shader complexity and cannot be optimized away. shader complexity and cannot be optimized away.
The backdrop pipeline's individual shader passes (downsample, separable blur, composite) are budgeted The backdrop pipeline's individual shader passes (downsample, separable blur, composite) are
at ≤24 registers each (same as the main pipeline), so merging them into the effects pipeline would register-light (~1540 regs each), so merging them into the effects pipeline would cause no occupancy
cause no occupancy problem. But the render-pass boundary makes merging structurally impossible — problem. But the render-pass boundary makes merging structurally impossible — effects draws happen
effects draws happen inside the main render pass, backdrop draws happen inside their own bracketed inside the main render pass, backdrop draws happen inside their own bracketed pass sequence.
pass sequence.
#### Why not per-primitive-type pipelines (GPUI's approach) #### Why not per-primitive-type pipelines (GPUI's approach)
@@ -253,9 +172,9 @@ API where each layer draws shadows before quads before glyphs. Our design avoids
submission order is draw order, no layer juggling required. submission order is draw order, no layer juggling required.
**PSO compilation costs multiply.** Each pipeline takes 150ms to compile on Metal/Vulkan/D3D12 at **PSO compilation costs multiply.** Each pipeline takes 150ms to compile on Metal/Vulkan/D3D12 at
first use. 7 pipelines is ~175ms cold startup; 3 pipelines is ~75ms. Adding state axes (blend first use. 7 pipelines is ~175ms cold startup; 3 pipelines is ~75ms. Adding state axes (MSAA
modes, color formats) multiplies combinatorially — a 2.3× larger variant matrix per additional variants, blend modes, color formats) multiplies combinatorially — a 2.3× larger variant matrix per
axis with 7 pipelines vs 3. additional axis with 7 pipelines vs 3.
**Branching cost comparison: unified vs per-kind in the effects pipeline.** The effects pipeline is **Branching cost comparison: unified vs per-kind in the effects pipeline.** The effects pipeline is
the strongest candidate for per-kind splitting because effect branches are heavier than shape the strongest candidate for per-kind splitting because effect branches are heavier than shape
@@ -336,23 +255,17 @@ There are three categories of branch condition in a fragment shader, ranked by c
#### Which category our branches fall into #### Which category our branches fall into
Our design has three branch points: Our design has two branch points:
1. **`mode` (push constant): tessellated vs. SDF.** This is category 2 — uniform per draw call. 1. **`mode` (push constant): tessellated vs. SDF.** This is category 2 — uniform per draw call.
Every thread in every warp of a draw call sees the same `mode` value. **Zero divergence, zero Every thread in every warp of a draw call sees the same `mode` value. **Zero divergence, zero
cost.** cost.**
2. **`kind` (flat varying from storage buffer): SDF shape kind dispatch.** This is category 3. 2. **`shape_kind` (flat varying from storage buffer): which SDF to evaluate.** This is category 3.
The low byte of `Primitive.flags` encodes `Shape_Kind` (RRect, NGon, Ellipse, Ring_Arc), passed The `flat` interpolation qualifier ensures that all fragments rasterized from one primitive's quad
to the fragment shader as a `flat` varying. All fragments of one primitive's quad receive the same receive the same `shape_kind` value. Divergence can only occur at the **boundary between two
kind value. The fragment shader's `if/else if` chain selects the appropriate SDF function (~1530 adjacent primitives of different kinds**, where the rasterizer might pack fragments from both
instructions per kind). Divergence occurs only at primitive boundaries where adjacent quads have primitives into the same warp.
different kinds.
3. **`flags` (flat varying from storage buffer): gradient/texture/outline mode.** Also category 3.
The upper bits of `Primitive.flags` encode `Shape_Flags`, controlling gradient vs. texture vs.
solid color selection and outline rendering — all lightweight branches (38 instructions per
path). Divergence at primitive boundaries between different flag combinations has negligible cost.
For category 3, the divergence analysis depends on primitive size: For category 3, the divergence analysis depends on primitive size:
@@ -369,12 +282,10 @@ For category 3, the divergence analysis depends on primitive size:
frame-level divergence is typically **13%** of all warps. frame-level divergence is typically **13%** of all warps.
At 13% divergence, the throughput impact is negligible. At 4K with 12.4M total fragments At 13% divergence, the throughput impact is negligible. At 4K with 12.4M total fragments
(~387,000 warps), divergent boundary warps number in the low thousands. The longest SDF kind branch (~387,000 warps), divergent boundary warps number in the low thousands. Each divergent warp pays at
is Ring_Arc (~30 instructions); when a divergent warp straddles two different kinds, it pays the cost most ~25 extra instructions (the cost of the longest untaken SDF branch). At ~12G instructions/sec
of both (~4560 instructions total). Each divergent warp's extra cost is modest — at ~12G on a mid-range GPU, that totals ~4μs — under 0.05% of an 8.3ms (120 FPS) frame budget. This is
instructions/sec on a mid-range GPU, even 3,000 divergent warps × 60 extra instructions totals confirmed by production renderers that use exactly this pattern:
~15μs, under 0.2% of an 8.3ms (120 FPS) frame budget. This is confirmed by production renderers
that use exactly this pattern:
- **vger / vger-rs** (Audulus): single pipeline, 11 primitive kinds dispatched by a `switch` on a - **vger / vger-rs** (Audulus): single pipeline, 11 primitive kinds dispatched by a `switch` on a
flat varying `prim_type`. Ships at 120 FPS on iPads. The author (Taylor Holliday) replaced nanovg flat varying `prim_type`. Ships at 120 FPS on iPads. The author (Taylor Holliday) replaced nanovg
@@ -398,10 +309,9 @@ our design:
> have no per-fragment data-dependent branches in the main pipeline. > have no per-fragment data-dependent branches in the main pipeline.
2. **Branches where both paths are very long.** If both sides of a branch are 500+ instructions, 2. **Branches where both paths are very long.** If both sides of a branch are 500+ instructions,
divergent warps pay double a large cost. Our SDF kind branches are short (~1530 instructions divergent warps pay double a large cost. Our SDF functions are 1025 instructions each. Even
each), and the gradient/texture/solid color selection branches are shorter still (38 instructions fully divergent, the penalty is ~25 extra instructions — less than a single texture sample's
each). Even fully divergent, the combined penalty is ~3060 extra instructions — comparable to a latency.
single texture sample's latency.
3. **Branches that prevent compiler optimizations.** Some compilers cannot schedule instructions 3. **Branches that prevent compiler optimizations.** Some compilers cannot schedule instructions
across branch boundaries, reducing VLIW utilization on older architectures. Modern GPUs (NVIDIA across branch boundaries, reducing VLIW utilization on older architectures. Modern GPUs (NVIDIA
@@ -409,10 +319,9 @@ our design:
concern. concern.
4. **Register pressure from the union of all branches.** This is the real cost, and it is why we 4. **Register pressure from the union of all branches.** This is the real cost, and it is why we
split heavy effects into separate pipelines. Within the main pipeline, the four split heavy effects (shadows, glass) into separate pipelines. Within the main pipeline, all SDF
SDF kind branches and flag-based color selection cluster at ~2226 registers (see register branches have similar register footprints (1222 registers), so combining them causes negligible
analysis in Current state), within the ≤24-register budget that guarantees full occupancy on occupancy loss.
Valhall and all desktop architectures. See Known limitations for V3D / Bifrost.
**References:** **References:**
@@ -433,29 +342,25 @@ our design:
### Main pipeline: SDF + tessellated (unified) ### Main pipeline: SDF + tessellated (unified)
The main pipeline serves two submission modes through a single `TRIANGLELIST` pipeline and a single The main pipeline serves two submission modes through a single `TRIANGLELIST` pipeline and a single
vertex input layout, distinguished by a `mode` field in the `Vertex_Uniforms_2D` push constant vertex input layout, distinguished by a push constant:
(`Core_2D_Mode.Tessellated = 0`, `Core_2D_Mode.SDF = 1`), pushed per draw call via `push_globals`. The
vertex shader branches on this uniform to select the tessellated or SDF code path.
- **Tessellated mode** (`mode = 0`): direct vertex buffer with explicit geometry. Used for text - **Tessellated mode** (`mode = 0`): direct vertex buffer with explicit geometry. Unchanged from
(SDL_ttf atlas sampling), triangles, triangle fans/strips, single-pixel points, and any today. Used for text (SDL_ttf atlas sampling), polylines, triangle fans/strips, gradient-filled
user-provided raw vertex geometry. shapes, and any user-provided raw vertex geometry.
- **SDF mode** (`mode = 1`): shared unit-quad vertex buffer + GPU storage buffer of - **SDF mode** (`mode = 1`): shared unit-quad vertex buffer + GPU storage buffer of `Primitive`
`Core_2D_Primitive` structs, drawn instanced. Used for all shapes with closed-form signed distance structs, drawn instanced. Used for all shapes with closed-form signed distance functions.
functions.
Both modes use the same fragment shader. The fragment shader checks `Shape_Kind` (low byte of Both modes converge on the same fragment shader, which dispatches on a `shape_kind` discriminant
`Core_2D_Primitive.flags`): kind 0 (`Solid`) is the tessellated path, which premultiplies the texture carried either in the vertex data (tessellated, always `Solid = 0`) or in the storage-buffer
sample and computes `out = color * t`; kinds 14 dispatch to one of four SDF functions (RRect, NGon, primitive struct (SDF modes).
Ellipse, Ring_Arc) and apply gradient/texture/outline/solid color based on `Shape_Flags` bits.
#### Why SDF for shapes #### Why SDF for shapes
CPU-side adaptive tessellation for curved shapes (the current approach) has three problems: CPU-side adaptive tessellation for curved shapes (the current approach) has three problems:
1. **Vertex bandwidth.** A rounded rectangle with four corner arcs produces ~250 vertices × 20 bytes 1. **Vertex bandwidth.** A rounded rectangle with four corner arcs produces ~250 vertices × 20 bytes
= 5 KB. An SDF rounded rectangle is one `Core_2D_Primitive` struct (96 bytes) plus 4 shared = 5 KB. An SDF rounded rectangle is one `Primitive` struct (~56 bytes) plus 4 shared unit-quad
unit-quad vertices. That is roughly a 50× reduction per shape. vertices. That is roughly a 90× reduction per shape.
2. **Quality.** Tessellated curves are piecewise-linear approximations. At high DPI or under 2. **Quality.** Tessellated curves are piecewise-linear approximations. At high DPI or under
animation/zoom, faceting is visible at any practical segment count. SDF evaluation produces animation/zoom, faceting is visible at any practical segment count. SDF evaluation produces
@@ -486,55 +391,49 @@ SDF primitives are submitted via a GPU storage buffer indexed by `gl_InstanceInd
shader, rather than encoding per-primitive data redundantly in vertex attributes. This follows the shader, rather than encoding per-primitive data redundantly in vertex attributes. This follows the
pattern used by both Zed GPUI and vger-rs. pattern used by both Zed GPUI and vger-rs.
Each SDF shape is described by a single `Core_2D_Primitive` struct (96 bytes) in the storage Each SDF shape is described by a single `Primitive` struct (~56 bytes) in the storage buffer. The
buffer. The vertex shader reads `primitives[gl_InstanceIndex]`, computes the quad corner position vertex shader reads `primitives[gl_InstanceIndex]`, computes the quad corner position from the unit
from the unit vertex and the primitive's bounds, and passes shape parameters to the fragment shader vertex and the primitive's bounds, and passes shape parameters to the fragment shader via `flat`
via `flat` interpolated varyings. interpolated varyings.
Compared to encoding per-primitive data in vertex attributes (the "fat vertex" approach), storage- Compared to encoding per-primitive data in vertex attributes (the "fat vertex" approach), storage-
buffer instancing eliminates the 46× data duplication across quad corners. A rounded rectangle costs buffer instancing eliminates the 46× data duplication across quad corners. A rounded rectangle costs
96 bytes instead of 4 vertices × 60+ bytes = 240+ bytes. 56 bytes instead of 4 vertices × 40+ bytes = 160+ bytes.
The tessellated path retains the existing direct vertex buffer layout (20 bytes/vertex, no storage The tessellated path retains the existing direct vertex buffer layout (20 bytes/vertex, no storage
buffer access). The vertex shader branch on `mode` (push constant) is warp-uniform — every invocation buffer access). The vertex shader branch on `mode` (push constant) is warp-uniform — every invocation
in a draw call has the same mode — so it is effectively free on all modern GPUs. in a draw call has the same mode — so it is effectively free on all modern GPUs.
#### Shape kinds and SDF dispatch #### Shape kinds
The fragment shader dispatches on `Shape_Kind` (low byte of `Core_2D_Primitive.flags`) to evaluate Primitives in the main pipeline's storage buffer carry a `Shape_Kind` discriminant:
one of four signed distance functions. The `Shape_Kind` enum, per-kind `*_Params` structs, and
CPU-side drawing procs all live in `core_2d.odin`. The drawing procs build the appropriate
`Core_2D_Primitive` and set the kind automatically:
Each user-facing shape proc accepts a `Brush` union (color, linear gradient, radial gradient, | Kind | SDF function | Notes |
or textured fill) as its fill source, plus optional outline parameters. The procs map to SDF | ---------- | -------------------------------------- | --------------------------------------------------------- |
kinds as follows: | `RRect` | `sdRoundedBox` (iq) | Per-corner radii. Covers all Clay rectangles and borders. |
| `Circle` | `sdCircle` | Filled and stroked. |
| `Ellipse` | `sdEllipse` | Exact (iq's closed-form). |
| `Segment` | `sdSegment` capsule | Rounded caps, correct sub-pixel thin lines. |
| `Ring_Arc` | `abs(sdCircle) - thickness` + arc mask | Rings, arcs, circle sectors unified. |
| `NGon` | `sdRegularPolygon` | Regular n-gon for n ≥ 5. |
| User-facing proc | Shape_Kind | SDF function | Notes | The `Solid` kind (value 0) is reserved for the tessellated path, where `shape_kind` is implicitly
| -------------------- | ---------- | ------------------ | ---------------------------------------------------------- | zero because the fragment shader receives it from zero-initialized vertex attributes.
| `rectangle` | `RRect` | `sdRoundedBox` | Per-corner radii from `radii` param |
| `circle` | `RRect` | `sdRoundedBox` | Uniform radii = half-size (circle is a degenerate RRect) |
| `line`, `line_strip` | `RRect` | `sdRoundedBox` | Rotated capsule — stadium shape (radii = half-thickness) |
| `ellipse` | `Ellipse` | `sdEllipseApprox` | Approximate ellipse SDF (fast, suitable for UI) |
| `polygon` | `NGon` | `sdRegularPolygon` | Regular N-sided polygon inscribed in a circle |
| `ring` (full) | `Ring_Arc` | Annular radial SDF | `max(inner - r, r - outer)` with no angular clipping |
| `ring` (partial arc) | `Ring_Arc` | Annular radial SDF | Pre-computed edge normals for angular wedge mask |
| `ring` (pie slice) | `Ring_Arc` | Annular radial SDF | `inner_radius = 0`, angular clipping via `start/end_angle` |
The `Shape_Flags` bit set controls per-primitive rendering mode (outline, gradient, texture, rotation, Stroke/outline variants of each shape are handled by the `Shape_Flags` bit set rather than separate
arc geometry). See the `Shape_Flag` enum in `core_2d.odin` for the authoritative flag shape kinds. The fragment shader transforms `d = abs(d) - stroke_width` when the `Stroke` flag is
definitions and bit assignments. set.
**What stays tessellated:** **What stays tessellated:**
- Text (SDL_ttf atlas, pending future MSDF evaluation) - Text (SDL_ttf atlas, pending future MSDF evaluation)
- `tess.pixel` (single-pixel points) - `rectangle_gradient`, `circle_gradient` (per-vertex color interpolation)
- `tess.triangle`, `tess.triangle_aa`, `tess.triangle_lines` (single triangles) - `triangle_fan`, `triangle_strip` (arbitrary user-provided point lists)
- `tess.triangle_fan`, `tess.triangle_strip` (arbitrary user-provided geometry) - `line_strip` / polylines (SDF polyline rendering is possible but complex; deferred)
- Any raw vertex geometry submitted via `prepare_shape` - Any raw vertex geometry submitted via `prepare_shape`
The design rule: if the shape has a closed-form SDF, it goes through the SDF path with its own The rule: if the shape has a closed-form SDF, it goes SDF. If it's described only by a vertex list or
`Shape_Kind`. If it is described by a vertex list or has no practical SDF, it stays tessellated. needs per-vertex color interpolation, it stays tessellated.
### Effects pipeline ### Effects pipeline
@@ -595,153 +494,44 @@ Wallace's variant) and vger-rs.
### Backdrop pipeline ### Backdrop pipeline
The backdrop pipeline handles effects that sample the current render target as input: frosted glass, The backdrop pipeline handles effects that sample the current render target as input: frosted glass,
refraction, mirror surfaces. It is separated from the main and effects pipelines for a structural refraction, mirror surfaces. It is separated from the effects pipeline for a structural reason, not
reason, not register pressure. register pressure.
**Render-pass boundary.** Before any backdrop-sampling fragment can run, the current render target **Render-pass boundary.** Before any backdrop-sampling fragment can run, the current render target
must be in a sampler-readable state. A draw call that samples the render target it is also writing must be copied to a separate texture via `CopyGPUTextureToTexture`. This is a command-buffer-level
to is a hard GPU constraint; the only way to satisfy it is to end the current render pass and start operation that cannot happen mid-render-pass. The copy naturally creates a pipeline boundary that no
a new one. That render-pass boundary is what a “bracket” is. amount of shader optimization can eliminate — it is a fundamental requirement of sampling a surface
while also writing to it.
**Multi-pass implementation.** Backdrop effects are implemented as separable multi-pass sequences **Multi-pass implementation.** Backdrop effects are implemented as separable multi-pass sequences
(downsample → horizontal blur → vertical blur → composite), following the standard approach used (downsample → horizontal blur → vertical blur → composite), following the standard approach used by
by iOS `UIVisualEffectView`, Android `RenderEffect`, and Flutter's `BackdropFilter`. Each individual iOS `UIVisualEffectView`, Android `RenderEffect`, and Flutter's `BackdropFilter`. Each individual
sub-pass is budgeted at **≤24 registers** (same as the main pipeline — full Valhall occupancy). The pass has a low-to-medium register footprint (~1540 registers), well within the main pipeline's
multi-pass approach avoids the monolithic 70+ register shader that a single-pass Gaussian blur would occupancy range. The multi-pass approach avoids the monolithic 70+ register shader that a single-pass
require, keeping each sub-pass well under the 32-register cliff. Gaussian blur would require, making backdrop effects viable on low-end mobile GPUs (including
Mali-G31 and VideoCore VI) where per-thread register limits are tight.
**Render-target choice.** When any layer in the frame contains a backdrop draw, the entire **Bracketed execution.** All backdrop draws in a frame share a single bracketed region of the command
frame renders into `source_texture` (a full-resolution single-sample texture owned by the buffer: end the current render pass, copy the render target, execute all backdrop sub-passes, then
backdrop pipeline) instead of directly into the swapchain. At the end of the frame, resume normal drawing. The entry/exit cost (texture copy + render-pass break) is paid once per frame
`source_texture` is copied to the swapchain via a single `CopyGPUTextureToTexture` call. regardless of how many backdrop effects are visible. When no backdrop effects are present, the bracket
This means each bracket has no mid-frame texture copy: by the time a bracket runs, is never entered and the texture copy never happens — zero cost.
`source_texture` already contains the contents written by everything that preceded it on the
timeline and is the natural sampler input. When no layer in the frame has a backdrop draw,
the existing fast path runs: the frame renders directly to the swapchain and the backdrop
pipeline's working textures are never touched. Zero cost for backdrop-free frames.
**Why not split the backdrop sub-passes into separate pipelines?** Each sub-pass is budgeted at ≤24 **Why not split the backdrop sub-passes into separate pipelines?** The individual passes range from
registers, well under Valhall's 32-register cliff, so there is no occupancy motivation for splitting. ~15 to ~40 registers, which does cross Mali's 32-register cliff. However, the register-pressure argument
The sub-passes also have no common-vs-uncommon distinction — if backdrop effects are active, every that justifies the main/effects split does not apply here. The main/effects split protects the
sub-pass runs; if not, none run. The backdrop pipeline either executes as a complete unit or not at _common path_ (90%+ of frame fragments) from the uncommon path's register cost. Inside the backdrop
all. Additionally, backdrop effects cover a small fraction of the frame's total fragments (~5% at pipeline there is no common-vs-uncommon distinction — if backdrop effects are active, every sub-pass
typical UI scales), so even if a sub-pass did cross a cliff, the occupancy variation within the runs; if not, none run. The backdrop pipeline either executes as a complete unit or not at all.
bracket would have negligible impact on frame time. Additionally, backdrop effects cover a small fraction of the frame's total fragments (~5% at typical
UI scales), so the occupancy variation within the bracket has negligible impact on frame time.
#### Bracket scheduling
Backdrop draws are scheduled via **explicit scopes**: every call to `backdrop_blur` must be wrapped
in a `begin_backdrop` / `end_backdrop` pair (or the RAII-style `backdrop_scope` wrapper). Each
scope produces exactly one bracket at render time. A layer may contain any number of scopes; draws
between scopes render at their submission position relative to the brackets, so the user controls
exactly which backdrops share a bracket.
At render time, `draw_layer` walks the layer's sub-batch list once, alternating between two run
kinds:
- **Non-backdrop runs** are rendered to `source_texture` in one render pass via
`render_layer_sub_batch_range`. Clear-vs-load is tracked frame-globally via `GLOB.cleared`.
- **Backdrop runs** are dispatched to `run_backdrop_bracket` with their index range. Each run is
one bracket; the bracket opens and closes its own render passes for downsample, H-blur, V-blur,
and composite stages.
Within a bracket, the scheduler groups contiguous same-sigma sub-batches and runs four sub-passes
per group: downsample (`source_texture``downsample_texture`), H-blur (`downsample_texture`
`h_blur_texture`), V-blur (`h_blur_texture``downsample_texture`, ping-pong reuse), and
composite (`downsample_texture``source_texture` with SDF mask and tint applied). Each group
picks its own downsample factor (1, 2, or 4) based on sigma; see the comment block at the top of
`backdrop.odin` for the factor-selection table.
Sub-batch coalescing in `append_or_extend_sub_batch` merges contiguous same-sigma backdrops
sharing one scissor into a single instanced composite draw. Same-sigma backdrops separated by a
`ScissorStart` boundary stay in one sigma group (one set of blur passes) but issue separate
composite draws; the composite pass calls `SetGPUScissor` between draws when the active scissor
changes.
Working textures are sized at full swapchain resolution; larger downsample factors fill a sub-rect
via viewport-limited rendering.
#### Scope contract
Scope state is global: `GLOB.open_backdrop_layer` tracks the currently-open scope (or `nil`) for
the whole renderer. The five misuse cases panic via `log.panic` / `log.panicf`:
1. `backdrop_blur` called outside an open scope.
2. A non-backdrop draw call issued on a layer with an open scope. Asserted at the top of
`append_or_extend_sub_batch`.
3. `new_layer` called while a scope is open.
4. `end()` called while a scope is open.
5. `begin_backdrop` while one is already open, or `end_backdrop` on the wrong layer.
Worked example with two scopes on the same layer:
```
base := draw.begin(...)
draw.rectangle(base, bg, GRAY)
draw.rectangle(base, card_blue, BLUE)
{
draw.backdrop_scope(base)
draw.backdrop_blur(base, panelA, sigma=12) // bracket 1: sees bg + blue card
}
draw.rectangle(base, card_red, RED) // renders ON TOP of panelA's composite
{
draw.backdrop_scope(base)
draw.backdrop_blur(base, panelB, sigma=12) // bracket 2: sees bg + blue card + panelA + card_red
}
draw.text(base, "label", ...) // renders ON TOP of panelB's composite
```
Each bracket adds four render passes (downsample + H-blur + V-blur + composite) plus tile-cache
flushes on tilers like Mali Valhall, so users who don't need interleaving should group backdrops
into a single scope to amortize:
```
{
draw.backdrop_scope(base)
draw.backdrop_blur(base, panelA, sigma=12) // shares one bracket with panelB;
draw.backdrop_blur(base, panelB, sigma=12) // same sigma also coalesces into one
} // instanced composite draw call
```
#### Clay integration: `Backdrop_Marker`
Clay has no notion of backdrops. The integration uses Clay's only extension point — the opaque
`customData: rawptr` on `clay.CustomElementConfig` — to carry a magic-number-tagged struct that
`prepare_clay_batch` recognizes:
```
Backdrop_Marker :: struct {
magic: u32, // BACKDROP_MARKER_MAGIC (0x42445054, 'BDPT')
sigma: f32,
tint: Color,
radii: Rectangle_Radii,
feather_px: f32,
}
```
The user populates a `Backdrop_Marker` (with stable lifetime through the `prepare_clay_batch`
call) and points the corresponding `clay.CustomElementConfig.customData` at it.
`prepare_clay_batch` walks Clay's command stream once, calling `is_clay_backdrop` per command
(a u32 magic check on `customData`'s first 4 bytes). On a hit it opens a backdrop scope (or
extends an open one) and dispatches via `backdrop_blur`. Non-backdrop commands issued during an
open scope go to a deferred index buffer for replay after the scope closes; this preserves Clay's
painter's-algorithm ordering across backdrops without violating the scope contract.
The magic-number sentinel keeps the marker type self-describing in core dumps and decouples the
integration from Clay-side changes. Zero-init memory has `magic = 0`, so a marker with a forgotten
magic field gets routed through the regular `custom_draw` path and surfaces as "my custom draw
never fired" rather than as a silent backdrop schedule.
### Vertex layout ### Vertex layout
The vertex struct is unchanged from the current 20-byte layout: The vertex struct is unchanged from the current 20-byte layout:
``` ```
Vertex_2D :: struct { Vertex :: struct {
position: [2]f32, // 0: screen-space position position: [2]f32, // 0: screen-space position
uv: [2]f32, // 8: atlas UV (text) or unused (shapes) uv: [2]f32, // 8: atlas UV (text) or unused (shapes)
color: Color, // 16: u8x4, GPU-normalized to float color: Color, // 16: u8x4, GPU-normalized to float
@@ -753,30 +543,25 @@ draws, `position` carries actual world-space geometry. For SDF draws, `position`
corners (0,0 to 1,1) and the vertex shader computes world-space position from the storage-buffer corners (0,0 to 1,1) and the vertex shader computes world-space position from the storage-buffer
primitive's bounds. primitive's bounds.
The `Core_2D_Primitive` struct for SDF shapes lives in the storage buffer, not in vertex attributes: The `Primitive` struct for SDF shapes lives in the storage buffer, not in vertex attributes:
``` ```
Core_2D_Primitive :: struct { Primitive :: struct {
bounds: [4]f32, // 0: min_x, min_y, max_x, max_y bounds: [4]f32, // 0: min_x, min_y, max_x, max_y
color: Color, // 16: u8x4, unpacked in shader via unpackUnorm4x8 color: Color, // 16: u8x4, unpacked in shader via unpackUnorm4x8
flags: u32, // 20: low byte = Shape_Kind, bits 8+ = Shape_Flags kind_flags: u32, // 20: (kind as u32) | (flags as u32 << 8)
rotation_sc: u32, // 24: packed f16 pair (sin, cos). Requires .Rotated flag. rotation: f32, // 24: shader self-rotation in radians
_pad: f32, // 28: reserved for future use _pad: f32, // 28: alignment
params: Shape_Params, // 32: per-kind params union (half_feather, radii, etc.) (32 bytes) params: Shape_Params, // 32: raw union, 32 bytes (two vec4s of shape-specific data)
uv_rect: [4]f32, // 64: texture UV coordinates. Read when .Textured. uv_rect: [4]f32, // 64: texture UV sub-region (u_min, v_min, u_max, v_max)
effects: Gradient_Outline, // 80: gradient and/or outline parameters (16 bytes).
} }
// Total: 96 bytes (std430 aligned) // Total: 80 bytes (std430 aligned)
``` ```
`Shape_Params` is a `#raw_union` over `RRect_Params`, `NGon_Params`, `Ellipse_Params`, and `Shape_Params` is a `#raw_union` with named variants per shape kind (`rrect`, `circle`, `segment`,
`Ring_Arc_Params` (plus a `raw: [8]f32` view), defined in `core_2d.odin`. Each SDF kind etc.), ensuring type safety on the CPU side and zero-cost reinterpretation on the GPU side. The
writes its own params variant; the fragment shader reads the appropriate fields based on `Shape_Kind`. `uv_rect` field is used by textured SDF primitives (Shape_Flag.Textured); non-textured primitives
`Gradient_Outline` is a 16-byte struct containing `gradient_color: Color`, `outline_color: Color`, leave it zeroed.
`gradient_dir_sc: u32` (packed f16 cos/sin pair), and `outline_packed: u32` (packed f16 outline
width). It is independent of `uv_rect`, so a primitive can carry texture and outline parameters at
the same time. The `flags` field encodes the `Shape_Kind` in the low byte and `Shape_Flags` in bits
8+ via `pack_kind_flags`.
### Draw submission order ### Draw submission order
@@ -798,18 +583,18 @@ invariant is that each primitive is drawn exactly once, in the pipeline that own
Text rendering currently uses SDL_ttf's GPU text engine, which rasterizes glyphs per `(font, size)` Text rendering currently uses SDL_ttf's GPU text engine, which rasterizes glyphs per `(font, size)`
pair into bitmap atlases and emits indexed triangle data via `GetGPUTextDrawData`. This path is pair into bitmap atlases and emits indexed triangle data via `GetGPUTextDrawData`. This path is
**unchanged** by the SDF migration — text continues to flow through the main pipeline's tessellated **unchanged** by the SDF migration — text continues to flow through the main pipeline's tessellated
mode with `mode = 0`, sampling the SDL_ttf atlas texture. mode with `shape_kind = Solid`, sampling the SDL_ttf atlas texture.
MSDF (multi-channel signed distance field) text rendering may be evaluated later, which would A future phase may evaluate MSDF (multi-channel signed distance field) text rendering, which would
allow resolution-independent glyph rendering from a single small atlas per font. This would involve: allow resolution-independent glyph rendering from a single small atlas per font. This would involve:
- Offline atlas generation via Chlumský's msdf-atlas-gen tool. - Offline atlas generation via Chlumský's msdf-atlas-gen tool.
- Runtime glyph metrics via `vendor:stb/truetype` (already in the Odin distribution). - Runtime glyph metrics via `vendor:stb/truetype` (already in the Odin distribution).
- A new MSDF glyph `Shape_Kind` in the fragment shader (additive — the kind dispatch infrastructure - A new `Shape_Kind.MSDF_Glyph` variant in the main pipeline's fragment shader.
already exists for the four current SDF kinds).
- Potential removal of the SDL_ttf dependency. - Potential removal of the SDL_ttf dependency.
This is explicitly deferred. This is explicitly deferred. The SDF shape migration is independent of and does not block text
changes.
**References:** **References:**
@@ -823,8 +608,8 @@ This is explicitly deferred.
### Textures ### Textures
Textures plug into the existing main pipeline — no additional GPU pipeline, no shader rewrite. The Textures plug into the existing main pipeline — no additional GPU pipeline, no shader rewrite. The
work is a resource layer (registration, upload, sampling, lifecycle) plus a `Texture_Fill` Brush work is a resource layer (registration, upload, sampling, lifecycle) plus two textured-draw procs
variant that routes the existing shape procs through the SDF path with the `.Textured` flag set. that route into the existing tessellated and SDF paths respectively.
#### Why draw owns registered textures #### Why draw owns registered textures
@@ -874,30 +659,35 @@ with the same texture but different samplers produce separate draw calls, which
#### Textured draw procs #### Textured draw procs
Textures share the same shape procs as colors and gradients. Each shape proc takes a `Brush` Textured rectangles route through the existing SDF path via `draw.rectangle_texture` and
union as its fill source; passing a `Texture_Fill` value (carrying `Texture_Id`, `tint`, `draw.rectangle_texture_corners`, mirroring `draw.rectangle` and `draw.rectangle_corners` exactly —
`uv_rect`, and `Sampler_Preset`) routes the draw through the SDF path with the `.Textured` same parameters, same naming — with the color parameter replaced by a texture ID plus an optional
flag set. There is no dedicated `rectangle_texture` / `circle_texture` proc — the same tint.
`rectangle`, `circle`, `ellipse`, `polygon`, `ring`, `line`, and `line_strip` procs handle
all fill sources.
A separate tessellated proc for "simple" fullscreen quads was considered on the theory that An earlier iteration of this design considered a separate tessellated `draw.texture` proc for
the tessellated path's lower register count would improve occupancy at large fragment counts. "simple" fullscreen quads, on the theory that the tessellated path's lower register count (~16 regs
Both paths are well within the ≤24-register main pipeline budget — both run at full vs ~24 for the SDF textured branch) would improve occupancy at large fragment counts. Applying the
occupancy on every target architecture (Valhall and above). The remaining ALU difference register-pressure analysis from the pipeline-strategy section above shows this is wrong: both 16 and
(~15 extra instructions for the SDF evaluation) amounts to ~20μs at 4K — below noise. 24 registers are well below the register cliff (~43 regs on consumer Ampere/Ada, ~32 on Volta/A100),
Meanwhile, splitting into a separate pipeline would add ~15μs per pipeline bind on the CPU so both run at 100% occupancy. The remaining ALU difference (~15 extra instructions for the SDF
side per scissor, matching or exceeding the GPU-side savings. Within the main pipeline, evaluation) amounts to ~20μs at 4K — below noise. Meanwhile, splitting into a separate pipeline
unified remains strictly better. would add ~15μs per pipeline bind on the CPU side per scissor, matching or exceeding the GPU-side
savings. Within the main pipeline, unified remains strictly better.
SDF drawing procs live in the `draw` package with unprefixed names (`rectangle`, `circle`, The naming convention follows the existing shape API: `rectangle_texture` and
`ellipse`, `polygon`, `ring`, `line`, `line_strip`). Gradients, textures, and outlines are `rectangle_texture_corners` sit alongside `rectangle` and `rectangle_corners`, mirroring the
selected via the `Brush` union and optional outline parameters rather than separate overloads. `rectangle_gradient` / `circle_gradient` pattern where the shape is the primary noun and the
modifier (gradient, texture) is secondary. This groups related procs together in autocomplete
(`rectangle_*`) and reads as natural English ("draw a rectangle with a texture").
Future per-shape texture variants (`circle_texture`, `ellipse_texture`, `polygon_texture`) are
reserved by this naming convention and require only a `Shape_Flag.Textured` bit plus a small
per-shape UV mapping function in the fragment shader. These are additive.
#### What SDF anti-aliasing does and does not do for textured draws #### What SDF anti-aliasing does and does not do for textured draws
The SDF path anti-aliases the **shape's outer silhouette** — rounded-corner edges, rotated edges, The SDF path anti-aliases the **shape's outer silhouette** — rounded-corner edges, rotated edges,
outline edges. It does not anti-alias or sharpen the texture content. Inside the shape, fragments stroke outlines. It does not anti-alias or sharpen the texture content. Inside the shape, fragments
sample through the chosen `Sampler_Preset`, and image quality is whatever the sampler produces from sample through the chosen `Sampler_Preset`, and image quality is whatever the sampler produces from
the source texels. A low-resolution texture displayed at a large size shows bilinear blur regardless the source texels. A low-resolution texture displayed at a large size shows bilinear blur regardless
of which draw proc is used. This matches the current text-rendering model, where glyph sharpness of which draw proc is used. This matches the current text-rendering model, where glyph sharpness
@@ -906,8 +696,8 @@ depends on how closely the display size matches the SDL_ttf atlas's rasterized s
#### Fit modes are a computation layer, not a renderer concept #### Fit modes are a computation layer, not a renderer concept
Standard image-fit behaviors (stretch, fill/cover, fit/contain, tile, center) are expressed as UV Standard image-fit behaviors (stretch, fill/cover, fit/contain, tile, center) are expressed as UV
sub-region computations on top of the `uv_rect` field of `Texture_Fill`. The renderer has no sub-region computations on top of the `uv_rect` parameter that both textured-draw procs accept. The
knowledge of fit modes — it samples whatever UV region it is given. renderer has no knowledge of fit modes — it samples whatever UV region it is given.
A `fit_params` helper computes the appropriate `uv_rect`, sampler preset, and (for letterbox/fit A `fit_params` helper computes the appropriate `uv_rect`, sampler preset, and (for letterbox/fit
mode) shrunken inner rect from a `Fit_Mode` enum, the target rect, and the texture's pixel size. mode) shrunken inner rect from a `Fit_Mode` enum, the target rect, and the texture's pixel size.
@@ -931,13 +721,13 @@ textures onto a free list that is processed in `r_end_frame`, not at the call si
Clay's `RenderCommandType.Image` is handled by dereferencing `imageData: rawptr` as a pointer to a Clay's `RenderCommandType.Image` is handled by dereferencing `imageData: rawptr` as a pointer to a
`Clay_Image_Data` struct containing a `Texture_Id`, `Fit_Mode`, and tint color. Routing mirrors the `Clay_Image_Data` struct containing a `Texture_Id`, `Fit_Mode`, and tint color. Routing mirrors the
existing rectangle handling: `fit_params` computes UVs from the fit mode, then `rectangle` is existing rectangle handling: zero `cornerRadius` dispatches to `draw.texture` (tessellated), nonzero
called with a `Texture_Fill` brush and the appropriate radii (zero for sharp corners, per-corner dispatches to `draw.rectangle_texture_corners` (SDF). A `fit_params` call computes UVs from the fit
values from Clay's `cornerRadius` otherwise). mode before dispatch.
#### Deferred features #### Deferred features
The following are plumbed in `Texture_Desc` but not yet implemented: The following are plumbed in the descriptor but not implemented in phase 1:
- **Mipmaps**: `Texture_Desc.mip_levels` field exists; generation via SDL3 deferred. - **Mipmaps**: `Texture_Desc.mip_levels` field exists; generation via SDL3 deferred.
- **Compressed formats**: `Texture_Desc.format` accepts BC/ASTC; upload path deferred. - **Compressed formats**: `Texture_Desc.format` accepts BC/ASTC; upload path deferred.
@@ -945,6 +735,7 @@ The following are plumbed in `Texture_Desc` but not yet implemented:
- **3D textures, arrays, cube maps**: `Texture_Desc.type` and `depth_or_layers` fields exist. - **3D textures, arrays, cube maps**: `Texture_Desc.type` and `depth_or_layers` fields exist.
- **Additional samplers**: anisotropic, trilinear, clamp-to-border — additive enum values. - **Additional samplers**: anisotropic, trilinear, clamp-to-border — additive enum values.
- **Atlas packing**: internal optimization for sub-batch coalescing; invisible to callers. - **Atlas packing**: internal optimization for sub-batch coalescing; invisible to callers.
- **Per-shape texture variants**: `circle_texture`, `ellipse_texture`, etc. — reserved by naming.
**References:** **References:**
-1170
View File
File diff suppressed because it is too large Load Diff
-1590
View File
File diff suppressed because it is too large Load Diff
-756
View File
@@ -1,756 +0,0 @@
// CYBERSTEEL DESIGN SYSTEM — Odin theme constants
//
// Retrofuturist. Technical. Direct. Gruvbox-derived palette
// with Art Deco type system. Every visual token from the
// Cybersteel design system, transferred 1:1 to Odin constants.
//
// Conventions:
// - Colors are [4]u8 RGBA. Alpha 255 = fully opaque.
// Translucent tints carry their alpha in the 4th channel.
// - Times are time.Duration via core:time.
// - Pixel sizes, weights, line-heights, letter-spacings, and
// ratio-like values are plain (untyped) numeric literals so
// callers can use them with whatever numeric type they need.
// - Letter-spacing values are expressed in EMs (multiply by
// the resolved font size to get pixels).
// - Line-heights are unitless multipliers of the font size.
package cybersteel
import "core:time"
import draw ".."
// ============================================================
// BASE BACKGROUNDS — warm dark, Gruvbox-derived
// Never pure black. The warmth is intentional: aged metal,
// amber phosphor, old paper. Order is: deepest chrome first
// (shell), then page, then progressively lighter surfaces.
// ============================================================
// Topbar, sidebar, nav chrome, modal backdrops. Deepest base.
BG_SHELL :: draw.Color{0x1d, 0x20, 0x21, 0xff}
// Default page canvas / main content area. One step up from shell.
BG_PAGE :: draw.Color{0x31, 0x31, 0x31, 0xff}
// Cards, panels, drawers, input fields, code blocks, table rows.
// Slightly lighter than the page so raised surfaces read clearly
// without shadows.
BG_SURFACE :: draw.Color{0x3c, 0x38, 0x36, 0xff}
// Selected rows, active nav items, hover states. One step lighter
// than BG_SURFACE.
BG_ACTIVE :: draw.Color{0x50, 0x49, 0x45, 0xff}
// Disabled buttons / inputs background. Pairs with FG_MUTED text
// only — the contrast is intentionally low.
BG_DISABLED :: draw.Color{0x66, 0x5c, 0x54, 0xff}
// Borders, dividers, rules, input outlines. Never use as a text
// surface — it has no fg-pair guarantee.
BG_BORDER :: draw.Color{0x7c, 0x6f, 0x64, 0xff}
// ============================================================
// BASE FOREGROUNDS — warm cream / ivory, never pure white
// Five-step ramp from brightest (heading) to most muted.
// ============================================================
// Hero text, page headings, display titles. Brightest fg.
FG_HEADING :: draw.Color{0xfb, 0xf1, 0xc7, 0xff}
// Primary body text, default readable content.
FG_BODY :: draw.Color{0xf2, 0xe2, 0xba, 0xff}
// Labels, secondary descriptions, table data.
FG_SECONDARY :: draw.Color{0xe0, 0xd0, 0xa8, 0xff}
// Captions, metadata, timestamps, placeholders.
FG_CAPTION :: draw.Color{0xce, 0xbd, 0x9e, 0xff}
// Disabled text, token labels, subtle UI annotations.
FG_MUTED :: draw.Color{0xb8, 0xa9, 0x8e, 0xff}
// ============================================================
// ACCENT — GOLD (signature color, Art Deco)
// The defining accent of the system. Use sparingly: borders,
// highlights, focus rings, primary interactive states.
// ============================================================
// Primary interactive, focus rings, headline interactive accent.
GOLD_BRIGHT :: draw.Color{0xfa, 0xbd, 0x2f, 0xff}
// Borders, decorative rules, default Art Deco ornament color.
GOLD_DIM :: draw.Color{0xd7, 0x99, 0x21, 0xff}
// Hover states, pressed accents, dimmer gold contexts.
GOLD_MUTED :: draw.Color{0xb5, 0x76, 0x14, 0xff}
// Pure CRT amber. Reserved for terminal-style glow / phosphor
// references — distinct from gold ramp.
AMBER :: draw.Color{0xff, 0xb0, 0x00, 0xff}
// ============================================================
// ACCENT — RED (danger, errors, critical alerts)
// ============================================================
RED_BRIGHT :: draw.Color{0xfb, 0x49, 0x34, 0xff}
RED_DIM :: draw.Color{0xcc, 0x24, 0x1d, 0xff}
RED_MUTED :: draw.Color{0x9d, 0x00, 0x06, 0xff}
// ============================================================
// ACCENT — GREEN (success, safe, complete)
// ============================================================
GREEN_BRIGHT :: draw.Color{0xb8, 0xbb, 0x26, 0xff}
GREEN_DIM :: draw.Color{0x98, 0x97, 0x1a, 0xff}
GREEN_MUTED :: draw.Color{0x79, 0x74, 0x0e, 0xff}
// ============================================================
// ACCENT — BLUE / TEAL (info, links, cool technical elements)
// ============================================================
BLUE_BRIGHT :: draw.Color{0x83, 0xa5, 0x98, 0xff}
BLUE_DIM :: draw.Color{0x45, 0x85, 0x88, 0xff}
BLUE_MUTED :: draw.Color{0x07, 0x66, 0x78, 0xff}
// ============================================================
// ACCENT — ORANGE (warnings, in-progress, hot paths)
// ============================================================
ORANGE_BRIGHT :: draw.Color{0xfe, 0x80, 0x19, 0xff}
ORANGE_DIM :: draw.Color{0xd6, 0x5d, 0x0e, 0xff}
ORANGE_MUTED :: draw.Color{0xaf, 0x3a, 0x03, 0xff}
// ============================================================
// ACCENT — AQUA (cool secondary accent, fresh/active states)
// ============================================================
AQUA_BRIGHT :: draw.Color{0x8e, 0xc0, 0x7c, 0xff}
AQUA_DIM :: draw.Color{0x68, 0x9d, 0x6a, 0xff}
AQUA_MUTED :: draw.Color{0x42, 0x7b, 0x58, 0xff}
// ============================================================
// ACCENT — PURPLE (rare, for categorical / data-vis variety)
// ============================================================
PURPLE_BRIGHT :: draw.Color{0xd3, 0x86, 0x9b, 0xff}
PURPLE_DIM :: draw.Color{0xb1, 0x62, 0x86, 0xff}
PURPLE_MUTED :: draw.Color{0x8f, 0x3f, 0x71, 0xff}
// ============================================================
// SEMANTIC COLOR ROLES
// Aliases to accent ramps, named by intent. Prefer these in
// product code so meaning travels with the value.
// ============================================================
// Primary brand interactive — buttons, key links, focus ring.
COLOR_PRIMARY :: GOLD_BRIGHT
COLOR_PRIMARY_DIM :: GOLD_DIM
// Destructive / error / critical states.
COLOR_DANGER :: RED_BRIGHT
COLOR_DANGER_DIM :: RED_DIM
// Successful operation / safe state / completion.
COLOR_SUCCESS :: GREEN_BRIGHT
COLOR_SUCCESS_DIM :: GREEN_DIM
// Caution / in-progress / non-fatal anomaly.
COLOR_WARNING :: ORANGE_BRIGHT
COLOR_WARNING_DIM :: ORANGE_DIM
// Informational / neutral status / passive notice.
COLOR_INFO :: BLUE_BRIGHT
COLOR_INFO_DIM :: BLUE_DIM
// Hyperlinks at rest and on hover (links flip to gold on hover).
COLOR_LINK :: BLUE_BRIGHT
COLOR_LINK_HOVER :: GOLD_BRIGHT
// Keyboard / programmatic focus ring color.
COLOR_FOCUS :: GOLD_BRIGHT
// ============================================================
// SURFACE ROLES
// Semantic aliases for the bg ramp by usage role.
// ============================================================
SURFACE_PAGE :: BG_PAGE // root canvas
SURFACE_RAISED :: BG_SURFACE // cards, panels, inputs
SURFACE_OVERLAY :: BG_SHELL // modals, popovers, deep chrome
SURFACE_HOVER :: BG_ACTIVE // hovered raised surfaces
SURFACE_ACTIVE :: BG_SURFACE // pressed/active raised surfaces
// ============================================================
// BORDER ROLES
// Cybersteel borders are 1px solid, always crisp, always visible.
// Color carries the meaning; weight rarely changes.
// ============================================================
BORDER :: BG_BORDER // structural borders, default
BORDER_SUBTLE :: BG_DISABLED // very faint separators
BORDER_ACCENT :: GOLD_DIM // decorative / active edge
BORDER_FOCUS :: GOLD_BRIGHT // focus rings
BORDER_DANGER :: RED_DIM // destructive states
BORDER_SUCCESS :: GREEN_DIM // success states
// ============================================================
// TRANSLUCENT ACCENT TINTS
// Used for hover fills behind ghost buttons and for warm
// gradient overlays. Alpha encodes the tint strength.
// ============================================================
// 20% gold tint behind a hovered secondary button.
TINT_GOLD_HOVER :: draw.Color{0xd7, 0x99, 0x21, 0x33} // ~20% alpha
// 20% red tint behind a hovered danger ghost button.
TINT_DANGER_HOVER :: draw.Color{0xcc, 0x24, 0x1d, 0x33}
// 20% green tint behind a hovered success ghost button.
TINT_SUCCESS_HOVER :: draw.Color{0x98, 0x97, 0x1a, 0x33}
// 8% gold tint — top of the diagonal "gold fade" feature
// section overlay.
TINT_GOLD_FADE :: draw.Color{0xfa, 0xbd, 0x2f, 0x14} // ~8% alpha
// 6% amber tint — top of the vertical "amber fade" overlay.
TINT_AMBER_FADE :: draw.Color{0xff, 0xb0, 0x00, 0x0f} // ~6% alpha
// 4% gold tint — corner of card gradient.
TINT_GOLD_CARD :: draw.Color{0xfa, 0xbd, 0x2f, 0x0a} // ~4% alpha
// 3% black tint — scanline overlay stripe color.
TINT_SCANLINE :: draw.Color{0x00, 0x00, 0x00, 0x08} // ~3% alpha
// ============================================================
// SHADOWS
// Cybersteel is FLAT — no drop shadows. Elevation is expressed
// through bg + border only. The single permitted shadow use is
// a 1px gold ring as a focus / active indicator. Constants are
// kept here so callers don't reach for ad-hoc shadow values.
// ============================================================
// 1px inset gold ring — only permitted shadow, used as focus
// or selected-state outline. Width is 1px; color follows.
SHADOW_GOLD_RING_WIDTH :: 1
SHADOW_GOLD_RING_COLOR :: GOLD_DIM
// ============================================================
// SPACING SCALE (8px base grid)
// All spacing values are multiples of 4px, with the main scale
// in multiples of 8px. Names describe the scope of the gap, not
// the raw size — pick by intent, not by pixel count.
// ============================================================
// Badge/tag inner padding, icon-label gap, border offsets, micro nudges.
SPACE_CHIP :: 4
// Inline element gaps, chip/pill padding, icon inset, tight row spacing.
SPACE_ELEMENT :: 8
// Button vertical padding, input inset, list row gap, label-to-field gap.
SPACE_COMPONENT :: 12
// Card inset, input horizontal padding, form field gap, default gap.
SPACE_GROUP :: 16
// Grouped nav items, related form section spacing, compact panel inset.
SPACE_CLUSTER :: 20
// Sidebar / panel inset, modal body padding, drawer inset, section
// subheader gap.
SPACE_PANEL :: 24
// Between distinct content blocks, card grid gutter, toolbar height.
SPACE_BLOCK :: 32
// Major content group spacing, dialog padding, page sub-section gap.
SPACE_CONTENT :: 40
// Page section breaks, feature group dividers, hero subheading gap.
SPACE_SECTION :: 48
// Hero vertical padding, layout area spacing, large feature gaps.
SPACE_REGION :: 64
// Page-scale layout spacing, full-width section vertical rhythm.
SPACE_ZONE :: 80
// Page margins, full-bleed hero top padding, maximum layout gutter.
SPACE_CANVAS :: 96
// ============================================================
// CORNER RADIUS
// Cybersteel does not round its corners like a toy. 04px is the
// preferred range; larger radii exist only for chips/pills.
// ============================================================
RADIUS_NONE :: 0 // sharp corners — preferred default for chrome
RADIUS_SM :: 4 // micro-rounding for inline code, small badges
RADIUS_MD :: 6 // default for cards, buttons, inputs
RADIUS_LG :: 10 // rare — used only for prominent containers
RADIUS_PILL :: 999 // fully-rounded chips, status pills, tags
// ============================================================
// BORDER WIDTH
// 1px solid is the standard. Heavier weights are only used for
// the Art Deco hairline accent on pre/code blocks.
// ============================================================
// Standard border weight everywhere — always crisp, always visible.
BORDER_WIDTH_DEFAULT :: 1
// Accent edge on <pre> blocks (left side, gold) and similar
// emphasized rule treatments.
BORDER_WIDTH_ACCENT :: 2
// ============================================================
// MOTION — TRANSITION DURATIONS
// Fast and purposeful. No bounce, no spring, no elastic. UI
// state changes in well under a quarter-second. Animations
// must explain causality; nothing is decorative.
// ============================================================
// Entering active/pressed state. Snap-down feel — must feel
// instant under the finger.
TRANSITION_PRESS :: 55 * time.Millisecond
// Releasing from a pressed state, and slower hover-out cases.
TRANSITION_UI :: 180 * time.Millisecond
// Hover enter / exit color shift on buttons, cards, links.
TRANSITION_HOVER :: 150 * time.Millisecond
// Overlay / modal / popover fade-in. Slightly longer to
// signal "a layer changed", not "a control changed".
TRANSITION_MODAL :: 200 * time.Millisecond
// Cursor / immediate-feedback transitions (caret moves,
// terminal output ticks).
TRANSITION_CURSOR :: 80 * time.Millisecond
// ============================================================
// MOTION — COMPONENT-LEVEL TIMINGS
// Specific named durations for known interactions. Prefer these
// over picking a raw transition for a given component.
// ============================================================
// Button press fade — primary/secondary/danger/success share this.
BUTTON_PRESS_FADE_DUR :: 55 * time.Millisecond
// Button release / hover-out fade.
BUTTON_RELEASE_FADE_DUR :: 180 * time.Millisecond
// Card hover (border + bg crossfade).
CARD_HOVER_FADE_DUR :: 150 * time.Millisecond
// Card press (border + bg snap to active).
CARD_PRESS_FADE_DUR :: 55 * time.Millisecond
// Modal / overlay enter.
MODAL_ENTER_DUR :: 200 * time.Millisecond
// Modal / overlay exit (mirror of enter for symmetry).
MODAL_EXIT_DUR :: 200 * time.Millisecond
// Link color crossfade on hover.
LINK_HOVER_FADE_DUR :: 180 * time.Millisecond
// Terminal scanline flicker tick — single frame of the loop.
SCANLINE_FLICKER_TICK :: 80 * time.Millisecond
// ============================================================
// TYPOGRAPHY — FONT FAMILY NAMES
// Sans: IBM Plex Sans
// Mono: Lilex — IBM Plex Mono with programming ligatures.
// Drop-in Plex Mono replacement; same skeleton, same
// proportions, plus =>, !=, >=, <=, etc. ligatures.
// Plex Sans covers display, body, and condensed roles by
// default. Lilex is for code, terminal output, data values,
// and full mono-mode surfaces.
// ============================================================
// Plain family names
FONT_FAMILY_SANS :: "IBM Plex Sans"
FONT_FAMILY_MONO :: "Lilex"
// IBM Plex Sans raw font data
SANS_THIN_RAW :: #load("fonts/IBMPlexSans-Thin.ttf") // IBM Plex Sans
SANS_THIN_ITALIC_RAW :: #load("fonts/IBMPlexSans-ThinItalic.ttf") // IBM Plex Sans
SANS_EXTRALIGHT_RAW :: #load("fonts/IBMPlexSans-ExtraLight.ttf") // IBM Plex Sans
SANS_EXTRALIGHT_ITALIC_RAW :: #load("fonts/IBMPlexSans-ExtraLightItalic.ttf") // IBM Plex Sans
SANS_LIGHT_RAW :: #load("fonts/IBMPlexSans-Light.ttf") // IBM Plex Sans
SANS_LIGHT_ITALIC_RAW :: #load("fonts/IBMPlexSans-LightItalic.ttf") // IBM Plex Sans
SANS_REGULAR_RAW :: #load("fonts/IBMPlexSans-Regular.ttf") // IBM Plex Sans
SANS_ITALIC_RAW :: #load("fonts/IBMPlexSans-Italic.ttf") // IBM Plex Sans
SANS_MEDIUM_RAW :: #load("fonts/IBMPlexSans-Medium.ttf") // IBM Plex Sans
SANS_MEDIUM_ITALIC_RAW :: #load("fonts/IBMPlexSans-MediumItalic.ttf") // IBM Plex Sans
SANS_SEMIBOLD_RAW :: #load("fonts/IBMPlexSans-SemiBold.ttf") // IBM Plex Sans
SANS_SEMIBOLD_ITALIC_RAW :: #load("fonts/IBMPlexSans-SemiBoldItalic.ttf") // IBM Plex Sans
SANS_BOLD_RAW :: #load("fonts/IBMPlexSans-Bold.ttf") // IBM Plex Sans
SANS_BOLD_ITALIC_RAW :: #load("fonts/IBMPlexSans-BoldItalic.ttf") // IBM Plex Sans
// Lilex raw font data
MONO_THIN_RAW :: #load("fonts/Lilex-Thin.ttf") // Lilex
MONO_THIN_ITALIC_RAW :: #load("fonts/Lilex-ThinItalic.ttf") // Lilex
MONO_EXTRALIGHT_RAW :: #load("fonts/Lilex-ExtraLight.ttf") // Lilex
MONO_EXTRALIGHT_ITALIC_RAW :: #load("fonts/Lilex-ExtraLightItalic.ttf") // Lilex
MONO_LIGHT_RAW :: #load("fonts/Lilex-Light.ttf") // Lilex
MONO_LIGHT_ITALIC_RAW :: #load("fonts/Lilex-LightItalic.ttf") // Lilex
MONO_REGULAR_RAW :: #load("fonts/Lilex-Regular.ttf") // Lilex
MONO_ITALIC_RAW :: #load("fonts/Lilex-Italic.ttf") // Lilex
MONO_MEDIUM_RAW :: #load("fonts/Lilex-Medium.ttf") // Lilex
MONO_MEDIUM_ITALIC_RAW :: #load("fonts/Lilex-MediumItalic.ttf") // Lilex
MONO_SEMIBOLD_RAW :: #load("fonts/Lilex-SemiBold.ttf") // Lilex
MONO_SEMIBOLD_ITALIC_RAW :: #load("fonts/Lilex-SemiBoldItalic.ttf") // Lilex
MONO_BOLD_RAW :: #load("fonts/Lilex-Bold.ttf") // Lilex
MONO_BOLD_ITALIC_RAW :: #load("fonts/Lilex-BoldItalic.ttf") // Lilex
// ============================================================
// TYPOGRAPHY — TYPE SCALE (1.25 modular ratio, base 16px)
// Minimum body size on web is 14px; print is 12pt.
// ============================================================
TEXT_XS :: 11 // status badges, fine print
TEXT_SM :: 13 // secondary labels, captions
TEXT_BASE :: 15 // default body text
TEXT_MD :: 16 // slightly prominent body
TEXT_LG :: 18 // subheadings, emphasized labels
TEXT_XL :: 22 // H3 level
TEXT_2XL :: 28 // H2 level
TEXT_3XL :: 36 // H1 level
TEXT_4XL :: 48 // display / hero
TEXT_5XL :: 64 // hero display
TEXT_6XL :: 96 // max scale; masthead only
// ============================================================
// TYPOGRAPHY — FONT WEIGHTS
// Constrained to the STATIC weights that BOTH faces actually
// ship from Google Fonts — IBM Plex Sans and Lilex share the
// same seven static instances:
// 100 Thin · 200 ExtraLight · 300 Light · 400 Regular ·
// 500 Medium · 600 SemiBold · 700 Bold
// There is no 800 ExtraBold and no 900 Black for either face.
// Do not request a weight outside this set — Google's API
// will fail or substitute, and the design will drift.
// ============================================================
WEIGHT_THIN :: 100
WEIGHT_EXTRALIGHT :: 200
WEIGHT_LIGHT :: 300
WEIGHT_REGULAR :: 400
WEIGHT_MEDIUM :: 500
WEIGHT_SEMIBOLD :: 600
WEIGHT_BOLD :: 700
// ============================================================
// TYPOGRAPHY — LINE HEIGHTS (unitless multipliers)
// Multiply by font size to derive a leading in pixels.
// ============================================================
LEADING_TIGHT :: 1.15 // display headings
LEADING_SNUG :: 1.30 // subheadings
LEADING_NORMAL :: 1.50 // default body prose
LEADING_LOOSE :: 1.70 // long-form reading, sparse density
LEADING_MONO :: 1.40 // code / terminal output
// ============================================================
// TYPOGRAPHY — LETTER SPACING (in EM units)
// Multiply by the resolved font size to get pixel spacing.
// ============================================================
TRACKING_TIGHT :: -0.02 // large headings, tightened display
TRACKING_NORMAL :: 0.00 // body default
TRACKING_WIDE :: 0.05 // H1/H2 ALL CAPS, button labels
TRACKING_WIDER :: 0.10 // H5 caps, section headers
TRACKING_WIDEST :: 0.20 // .label / .label-mono — ALL CAPS chip text
// ============================================================
// HEADING ROLES — paired size + tracking + casing intent
// Casing is documentation only; these are the numbers a
// renderer actually consumes.
// ============================================================
// H1 — page title, masthead. Title Case, ALL CAPS at display.
H1_SIZE :: TEXT_3XL
H1_WEIGHT :: WEIGHT_BOLD
H1_TRACKING :: TRACKING_WIDE
H1_LEADING :: LEADING_TIGHT
// H2 — major section. ALL CAPS.
H2_SIZE :: TEXT_2XL
H2_WEIGHT :: WEIGHT_BOLD
H2_TRACKING :: TRACKING_WIDE
H2_LEADING :: LEADING_TIGHT
// H3 — subsection. Sentence case, condensed semibold.
H3_SIZE :: TEXT_XL
H3_WEIGHT :: WEIGHT_SEMIBOLD
H3_TRACKING :: TRACKING_NORMAL
H3_LEADING :: LEADING_TIGHT
// H4 — minor subsection.
H4_SIZE :: TEXT_LG
H4_WEIGHT :: WEIGHT_SEMIBOLD
H4_TRACKING :: TRACKING_NORMAL
H4_LEADING :: LEADING_SNUG
// H5 — small caps section header (uses FG_SECONDARY).
H5_SIZE :: TEXT_BASE
H5_WEIGHT :: WEIGHT_SEMIBOLD
H5_TRACKING :: TRACKING_WIDER
H5_LEADING :: LEADING_SNUG
// H6 — mono caps eyebrow / overline (uses FG_CAPTION).
H6_SIZE :: TEXT_SM
H6_WEIGHT :: WEIGHT_REGULAR
H6_TRACKING :: TRACKING_WIDEST
H6_LEADING :: LEADING_SNUG
// ============================================================
// LABEL ROLES — small caps annotation chips
// ============================================================
// .label — sans condensed, ALL CAPS, FG_CAPTION.
LABEL_SIZE :: TEXT_XS
LABEL_WEIGHT :: WEIGHT_SEMIBOLD
LABEL_TRACKING :: TRACKING_WIDEST
// .label-mono — mono ALL CAPS, FG_MUTED.
LABEL_MONO_SIZE :: TEXT_XS
LABEL_MONO_WEIGHT :: WEIGHT_REGULAR
LABEL_MONO_TRACKING :: TRACKING_WIDEST
// ============================================================
// FOCUS RING
// 1px solid gold outline at 2px offset. Crisp, never blurry.
// No glow, no box-shadow halo.
// ============================================================
FOCUS_RING_WIDTH :: 1
FOCUS_RING_OFFSET :: 2
FOCUS_RING_COLOR :: BORDER_FOCUS // GOLD_BRIGHT
// ============================================================
// COMPONENT — BUTTONS
// Cybersteel buttons are uppercase, semibold→bold, with wide
// tracking. Default size is "md"; sm/lg shift padding + size.
// ============================================================
// Default (md) padding: vertical / horizontal
BUTTON_PAD_Y :: 8
BUTTON_PAD_X :: 18
BUTTON_FONT_SIZE :: 12
BUTTON_FONT_WEIGHT :: WEIGHT_BOLD
BUTTON_TRACKING :: 0.07 // EM — ALL CAPS button label
BUTTON_RADIUS :: RADIUS_MD
BUTTON_BORDER :: BORDER_WIDTH_DEFAULT
// Small button
BUTTON_SM_PAD_Y :: 5
BUTTON_SM_PAD_X :: 12
BUTTON_SM_FONT_SIZE :: 10
// Large button
BUTTON_LG_PAD_Y :: 11
BUTTON_LG_PAD_X :: 24
BUTTON_LG_FONT_SIZE :: 14
// Primary — solid gold fill, dark text. Hover brightens, press
// flips to fg-heading (cream) fill.
BUTTON_PRIMARY_BG :: GOLD_DIM
BUTTON_PRIMARY_FG :: BG_SHELL
BUTTON_PRIMARY_BORDER :: GOLD_DIM
BUTTON_PRIMARY_BG_HOVER :: GOLD_BRIGHT
BUTTON_PRIMARY_BORDER_HOVER :: GOLD_BRIGHT
BUTTON_PRIMARY_BG_PRESS :: FG_HEADING
BUTTON_PRIMARY_FG_PRESS :: BG_SHELL
BUTTON_PRIMARY_BORDER_PRESS :: FG_HEADING
// Secondary — transparent bg, structural border, hover gains
// gold tint + gold-dim border, press fills with gold-bright.
BUTTON_SECONDARY_BG :: [4]u8{0, 0, 0, 0} // transparent
BUTTON_SECONDARY_FG :: FG_SECONDARY
BUTTON_SECONDARY_BORDER :: BG_BORDER
BUTTON_SECONDARY_BG_HOVER :: TINT_GOLD_HOVER
BUTTON_SECONDARY_BORDER_HOVER :: GOLD_DIM
BUTTON_SECONDARY_FG_HOVER :: FG_BODY
BUTTON_SECONDARY_BG_PRESS :: GOLD_BRIGHT
BUTTON_SECONDARY_FG_PRESS :: [4]u8{0xff, 0xff, 0xff, 0xff}
BUTTON_SECONDARY_BORDER_PRESS :: GOLD_BRIGHT
// Ghost — fully transparent, no border. Hover lifts to BG_ACTIVE.
BUTTON_GHOST_BG :: [4]u8{0, 0, 0, 0}
BUTTON_GHOST_FG :: FG_CAPTION
BUTTON_GHOST_BORDER :: [4]u8{0, 0, 0, 0}
BUTTON_GHOST_BG_HOVER :: BG_ACTIVE
BUTTON_GHOST_FG_HOVER :: FG_BODY
BUTTON_GHOST_BG_PRESS :: GOLD_DIM
BUTTON_GHOST_FG_PRESS :: [4]u8{0xff, 0xff, 0xff, 0xff}
// Danger — destructive ghost button.
BUTTON_DANGER_BG :: [4]u8{0, 0, 0, 0}
BUTTON_DANGER_FG :: RED_BRIGHT
BUTTON_DANGER_BORDER :: RED_DIM
BUTTON_DANGER_BG_HOVER :: TINT_DANGER_HOVER
BUTTON_DANGER_BORDER_HOVER :: RED_BRIGHT
BUTTON_DANGER_FG_HOVER :: FG_BODY
BUTTON_DANGER_BG_PRESS :: RED_BRIGHT
BUTTON_DANGER_FG_PRESS :: [4]u8{0xff, 0xff, 0xff, 0xff}
BUTTON_DANGER_BORDER_PRESS :: RED_BRIGHT
// Success — confirming ghost button.
BUTTON_SUCCESS_BG :: [4]u8{0, 0, 0, 0}
BUTTON_SUCCESS_FG :: GREEN_BRIGHT
BUTTON_SUCCESS_BORDER :: GREEN_DIM
BUTTON_SUCCESS_BG_HOVER :: TINT_SUCCESS_HOVER
BUTTON_SUCCESS_BORDER_HOVER :: GREEN_BRIGHT
BUTTON_SUCCESS_FG_HOVER :: FG_BODY
BUTTON_SUCCESS_BG_PRESS :: GREEN_BRIGHT
BUTTON_SUCCESS_FG_PRESS :: [4]u8{0xff, 0xff, 0xff, 0xff}
BUTTON_SUCCESS_BORDER_PRESS :: GREEN_BRIGHT
// Disabled — flat low-contrast surface, opacity-dimmed.
BUTTON_DISABLED_BG :: BG_ACTIVE
BUTTON_DISABLED_FG :: FG_MUTED
BUTTON_DISABLED_BORDER :: BG_BORDER
BUTTON_DISABLED_OPACITY :: 0.5
// ============================================================
// COMPONENT — CARDS
// Flat, structural, mechanical. Background sits one step above
// page; border is structural by default and shifts to gold-dim
// on hover/press. Corner radius is the default 6px (RADIUS_MD).
// ============================================================
CARD_BG :: BG_SURFACE
CARD_BORDER :: BG_BORDER
CARD_BORDER_HOVER :: GOLD_DIM
CARD_BG_PRESS :: BG_ACTIVE
CARD_BORDER_PRESS :: GOLD_DIM
CARD_RADIUS :: RADIUS_MD
CARD_BORDER_WIDTH :: BORDER_WIDTH_DEFAULT
CARD_PADDING :: SPACE_GROUP // 16px default inset
// ============================================================
// COMPONENT — INPUTS
// Inputs sit on BG_SURFACE with structural borders. Focus
// promotes the border to gold-bright; the focus ring follows.
// ============================================================
INPUT_BG :: BG_SURFACE
INPUT_FG :: FG_BODY
INPUT_PLACEHOLDER :: FG_CAPTION
INPUT_BORDER :: BG_BORDER
INPUT_BORDER_HOVER :: GOLD_DIM
INPUT_BORDER_FOCUS :: GOLD_BRIGHT
INPUT_BORDER_DANGER :: RED_DIM
INPUT_RADIUS :: RADIUS_MD
INPUT_PAD_Y :: SPACE_COMPONENT // 12
INPUT_PAD_X :: SPACE_GROUP // 16
// ============================================================
// COMPONENT — BADGES / STATUS PILLS
// ============================================================
BADGE_FONT_SIZE :: TEXT_XS
BADGE_WEIGHT :: WEIGHT_SEMIBOLD
BADGE_TRACKING :: TRACKING_WIDEST
BADGE_PAD_Y :: SPACE_CHIP // 4
BADGE_PAD_X :: SPACE_ELEMENT // 8
BADGE_RADIUS :: RADIUS_SM
// ============================================================
// COMPONENT — DECO RULE
// Hairline Art Deco horizontal rule: 1px gold-dim top + 1px
// structural drop, with panel-sized vertical margins.
// ============================================================
DECO_RULE_TOP_WIDTH :: 1
DECO_RULE_TOP_COLOR :: GOLD_DIM
DECO_RULE_DROP_WIDTH :: 1
DECO_RULE_DROP_COLOR :: BG_BORDER
DECO_RULE_MARGIN_Y :: SPACE_PANEL // 24
// ============================================================
// LAYOUT — FIXED CHROME WIDTHS
// Sidebar widths are fixed; content lives in 8 or 12 column
// grids. No responsive collapsing for chrome — Cybersteel UIs
// run on real workstations.
// ============================================================
SIDEBAR_WIDTH_NARROW :: 240
SIDEBAR_WIDTH_WIDE :: 280
GRID_COLUMNS_NARROW :: 8
GRID_COLUMNS_WIDE :: 12
// Toolbar height matches SPACE_BLOCK so vertical rhythm aligns.
TOOLBAR_HEIGHT :: SPACE_BLOCK // 32
// ============================================================
// CODE BLOCKS — <pre>
// Mono, BG_SHELL surface with a 1px structural border and a
// 2px gold-dim accent on the left edge.
// ============================================================
CODE_INLINE_BG :: BG_SURFACE
CODE_INLINE_FG :: GOLD_BRIGHT
CODE_INLINE_BORDER :: BG_BORDER
CODE_INLINE_PAD_Y :: 2
CODE_INLINE_PAD_X :: 6
CODE_INLINE_RADIUS :: RADIUS_SM
PRE_BG :: BG_SHELL
PRE_FG :: FG_BODY
PRE_BORDER :: BG_BORDER
PRE_BORDER_LEFT_COLOR :: GOLD_DIM
PRE_BORDER_LEFT_WIDTH :: BORDER_WIDTH_ACCENT // 2
PRE_PAD_Y :: SPACE_GROUP // 16
PRE_PAD_X :: SPACE_PANEL // 24
// ============================================================
// SCANLINE OVERLAY (opt-in, terminal surfaces only)
// Repeating-stripe pattern at very low opacity. Stripe is 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.
+454 -751
View File
File diff suppressed because it is too large Load Diff
+13 -17
View File
@@ -3,10 +3,6 @@ package draw_qr
import draw ".." import draw ".."
import "../../qrcode" 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 // 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). // QR buffer. Equivalent to size*size*4 where size = qrcode.get_size(qrcode_buf).
texture_size :: #force_inline proc(qrcode_buf: []u8) -> int { texture_size :: #force_inline proc(qrcode_buf: []u8) -> int {
@@ -20,13 +16,13 @@ texture_size :: #force_inline proc(qrcode_buf: []u8) -> int {
// //
// Returns ok=false when: // Returns ok=false when:
// - qrcode_buf is invalid (qrcode.get_size returns 0). // - qrcode_buf is invalid (qrcode.get_size returns 0).
// - texture_buf is smaller than texture_size(qrcode_buf). // - texture_buf is smaller than to_texture_size(qrcode_buf).
@(require_results) @(require_results)
to_texture :: proc( to_texture :: proc(
qrcode_buf: []u8, qrcode_buf: []u8,
texture_buf: []u8, texture_buf: []u8,
dark: draw.Color = DFT_QR_DARK, dark: draw.Color = draw.BLACK,
light: draw.Color = DFT_QR_LIGHT, light: draw.Color = draw.WHITE,
) -> ( ) -> (
desc: draw.Texture_Desc, desc: draw.Texture_Desc,
ok: bool, ok: bool,
@@ -69,8 +65,8 @@ to_texture :: proc(
@(require_results) @(require_results)
register_texture_from_raw :: proc( register_texture_from_raw :: proc(
qrcode_buf: []u8, qrcode_buf: []u8,
dark: draw.Color = DFT_QR_DARK, dark: draw.Color = draw.BLACK,
light: draw.Color = DFT_QR_LIGHT, light: draw.Color = draw.WHITE,
temp_allocator := context.temp_allocator, temp_allocator := context.temp_allocator,
) -> ( ) -> (
texture: draw.Texture_Id, texture: draw.Texture_Id,
@@ -100,9 +96,9 @@ register_texture_from_text :: proc(
min_version: int = qrcode.VERSION_MIN, min_version: int = qrcode.VERSION_MIN,
max_version: int = qrcode.VERSION_MAX, max_version: int = qrcode.VERSION_MAX,
mask: Maybe(qrcode.Mask) = nil, mask: Maybe(qrcode.Mask) = nil,
boost_ecl: bool = DFT_QR_BOOST_ECL, boost_ecl: bool = true,
dark: draw.Color = DFT_QR_DARK, dark: draw.Color = draw.BLACK,
light: draw.Color = DFT_QR_LIGHT, light: draw.Color = draw.WHITE,
temp_allocator := context.temp_allocator, temp_allocator := context.temp_allocator,
) -> ( ) -> (
texture: draw.Texture_Id, texture: draw.Texture_Id,
@@ -139,9 +135,9 @@ register_texture_from_binary :: proc(
min_version: int = qrcode.VERSION_MIN, min_version: int = qrcode.VERSION_MIN,
max_version: int = qrcode.VERSION_MAX, max_version: int = qrcode.VERSION_MAX,
mask: Maybe(qrcode.Mask) = nil, mask: Maybe(qrcode.Mask) = nil,
boost_ecl: bool = DFT_QR_BOOST_ECL, boost_ecl: bool = true,
dark: draw.Color = DFT_QR_DARK, dark: draw.Color = draw.BLACK,
light: draw.Color = DFT_QR_LIGHT, light: draw.Color = draw.WHITE,
temp_allocator := context.temp_allocator, temp_allocator := context.temp_allocator,
) -> ( ) -> (
texture: draw.Texture_Id, texture: draw.Texture_Id,
@@ -167,13 +163,13 @@ register_texture_from_binary :: proc(
register_texture_from :: proc { register_texture_from :: proc {
register_texture_from_text, register_texture_from_text,
register_texture_from_binary, register_texture_from_binary
} }
// Default fit=.Fit preserves the QR's square aspect; override as needed. // Default fit=.Fit preserves the QR's square aspect; override as needed.
clay_image :: #force_inline proc( clay_image :: #force_inline proc(
texture: draw.Texture_Id, texture: draw.Texture_Id,
tint: draw.Color = draw.DFT_TINT, tint: draw.Color = draw.WHITE,
) -> draw.Clay_Image_Data { ) -> draw.Clay_Image_Data {
return draw.clay_image_data(texture, fit = .Fit, tint = tint) return draw.clay_image_data(texture, fit = .Fit, tint = tint)
} }
-409
View File
@@ -1,409 +0,0 @@
package examples
import "core:fmt"
import "core:math"
import "core:os"
import sdl "vendor:sdl3"
import "../../draw"
import cyber "../cybersteel"
// Backdrop example.
//
// Exercises the bracket scheduler end-to-end. The demo is structured as three zones in one
// window so we can stress-test the cases that matter:
//
// Zone 1 (top, base layer): animated colorful background + two side-by-side frosted panels
// with DIFFERENT sigmas and DIFFERENT tints. Tests sigma grouping
// and per-primitive tint.
//
// Zone 2 (bottom-left, second layer): a small frosted panel in a NEW layer; its bracket sees
// Zone 1's full content (base layer's bracket output is
// carried forward via source_texture). Tests multi-layer
// backdrop sampling.
//
// Zone 3 (bottom-right, base layer): edge cases. A sigma=0 "mirror" panel (no blur), two
// same-sigma panels stacked (tests sub-batch coalescing
// via append_or_extend_sub_batch), and text drawn ON TOP
// of a backdrop (tests Pass B post-bracket rendering).
//
// Animation: an orbiting gradient stripe plus a few orbiting circles in Zone 1. Motion is the
// only way to visually confirm the blur is Gaussian; a static panel can't tell you whether the
// kernel coefficients are right.
gaussian_blur :: proc() {
if !sdl.Init({.VIDEO}) do os.exit(1)
window := sdl.CreateWindow("Backdrop blur", 800, 600, {.HIGH_PIXEL_DENSITY})
gpu := sdl.CreateGPUDevice(draw.PLATFORM_SHADER_FORMAT, true, nil)
if !sdl.ClaimWindowForGPUDevice(gpu, window) do os.exit(1)
if !draw.init(gpu, window) do os.exit(1)
PLEX_SANS_REGULAR = draw.register_font(cyber.SANS_REGULAR_RAW)
WINDOW_W :: f32(800)
WINDOW_H :: f32(600)
FONT_SIZE :: u16(14)
t: f32 = 0
for {
defer free_all(context.temp_allocator)
ev: sdl.Event
for sdl.PollEvent(&ev) {
if ev.type == .QUIT do return
}
t += 1
base_layer := draw.begin({width = WINDOW_W, height = WINDOW_H})
//----- Background fill ----------------------------------
draw.rectangle(base_layer, {0, 0, WINDOW_W, WINDOW_H}, draw.Color{20, 20, 28, 255})
//----- Zone 1: animated background for the top frosted panels ----------------------------------
// A wide rotating gradient stripe sweeps left-to-right across Zone 1. The angle changes
// over time so the gradient itself shifts visibly.
stripe_angle := t * 0.4
draw.rectangle(
base_layer,
{20, 20, WINDOW_W - 40, 240},
draw.Linear_Gradient {
start_color = {255, 80, 60, 255},
end_color = {60, 120, 255, 255},
angle = stripe_angle,
},
)
// Five orbiting circles inside Zone 1's strip. The blur should smooth their hard edges
// and the gradient behind them into a continuous wash.
for i in 0 ..< 5 {
phase := f32(i) * 1.2 + t * 0.04
cx := 100 + f32(i) * 140 + math.cos(phase) * 30
cy := 140 + math.sin(phase) * 50
circle_color := draw.Color {
u8(clamp(120 + math.cos(phase) * 100, 0, 255)),
u8(clamp(180 + math.sin(phase * 1.3) * 60, 0, 255)),
u8(clamp(220 - math.sin(phase) * 80, 0, 255)),
255,
}
draw.circle(base_layer, {cx, cy}, 22, circle_color)
}
// Bright accent rectangles to give the blur some sharp edges to munch on.
draw.rectangle(base_layer, {200, 60, 60, 12}, draw.Color{255, 255, 200, 255})
draw.rectangle(base_layer, {500, 200, 80, 16}, draw.Color{200, 255, 200, 255})
//----- Zone 1 frosted panels: different sigmas, different tints --------------------------------
// Panel A: heavy blur, cool blue-grey tint. sigma=14 in logical px.
// Both panels share rounded corners.
panel_radii := draw.Rectangle_Radii{16, 16, 16, 16}
// Both zone1 panels share one scope. Different sigmas still trigger separate blur
// passes (cost scales with unique sigmas, not with backdrop count); the scope just
// declares "these draws form one bracket." `backdrop_scope` is the RAII-style API:
// `end_backdrop` fires automatically when the block exits.
{
draw.backdrop_scope(base_layer)
draw.backdrop_blur(
base_layer,
{60, 80, 320, 140},
gaussian_sigma = 30,
tint = draw.Color{170, 200, 240, 200}, // cool blue, strong mix
radii = panel_radii,
)
// Panel B: lighter blur, warm amber tint. sigma=6.
draw.backdrop_blur(
base_layer,
{420, 80, 320, 140},
gaussian_sigma = 6,
tint = draw.Color{255, 220, 160, 200}, // warm amber, strong mix
radii = panel_radii,
)
}
// Text labels for the two panels. Drawn AFTER `end_backdrop` (which fires at the
// scope-block exit above), so they composite on top of both panels.
draw.text(
base_layer,
"sigma = 20, cool tint",
{72, 90},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.Color{30, 35, 50, 255},
)
draw.text(
base_layer,
"sigma = 6, warm tint",
{432, 90},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.Color{60, 40, 20, 255},
)
// Post-bracket verification: a white stripe drawn AFTER `end_backdrop` in the same
// layer. Should render ON TOP of both panels because the backdrop scope (and its
// composite output) is now closed; any non-backdrop draw on this layer composites
// with LOAD on top of whatever the bracket left in source_texture.
draw.rectangle(base_layer, {WINDOW_W * 0.5 - 4, 70, 8, 160}, draw.Color{255, 255, 255, 230})
//----- Zone 2: second layer with its own backdrop --------------------------------
// Zone 2's panel is in a NEW layer. Its bracket samples source_texture as it stands
// after the base layer fully finished (including the base layer's bracket V-composite
// output). So this panel sees Zone 1's frosted panels through its own blur.
zone2 := draw.new_layer(base_layer, {0, 280, WINDOW_W * 0.55, WINDOW_H - 280})
// Pass A content for zone2: a translucent darker overlay to make the panel pop.
draw.rectangle(zone2, {20, 300, WINDOW_W * 0.55 - 40, WINDOW_H - 320}, draw.Color{0, 0, 0, 80})
// Animated diagonal stripe in Zone 2 so the blur in this layer's panel has motion to
// smooth, not just the static base-layer content.
stripe_y := 320 + (math.sin(t * 0.05) * 0.5 + 0.5) * 200
draw.rectangle(zone2, {30, stripe_y, WINDOW_W * 0.55 - 60, 18}, draw.Color{255, 100, 200, 200})
// Zone 2's frosted panel. Single-panel scope; `backdrop_scope` keeps the begin/end
// pair tied to the block.
{
draw.backdrop_scope(zone2)
draw.backdrop_blur(
zone2,
{60, 360, WINDOW_W * 0.55 - 120, 160},
gaussian_sigma = 10,
tint = draw.WHITE, // pure blur (white tint with any alpha is a no-op)
radii = draw.Rectangle_Radii{24, 24, 24, 24},
)
}
draw.text(
zone2,
"Layer 2 backdrop",
{72, 372},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.Color{30, 30, 30, 255},
)
draw.text(
zone2,
"sigma = 10",
{72, 392},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.Color{60, 60, 60, 255},
)
//----- Zone 3: edge cases (back in base layer would also work, but we use zone2 to keep --------
// the demo's two-layer structure simple). Zone 3 lives in a third layer so it gets
// a fresh source snapshot too.
zone3 := draw.new_layer(zone2, {WINDOW_W * 0.55, 280, WINDOW_W * 0.45, WINDOW_H - 280})
// Animated background patch for Zone 3 so its mirror panel has something to reflect.
for i in 0 ..< 4 {
phase := f32(i) * 1.5 + t * 0.06
y := 310 + f32(i) * 60 + math.sin(phase) * 8
draw.rectangle(
zone3,
{WINDOW_W * 0.55 + 20, y, WINDOW_W * 0.45 - 40, 14},
draw.Color {
u8(clamp(200 + math.cos(phase) * 50, 0, 255)),
u8(clamp(150 + math.sin(phase) * 80, 0, 255)),
u8(clamp(220 - math.cos(phase * 1.7) * 60, 0, 255)),
255,
},
)
}
// All three Zone 3 backdrops share one scope. The sigma=0 mirror, then the two
// contiguous sigma=8 panels. The sigma=8 pair stays contiguous in the sub-batch list,
// so `append_or_extend_sub_batch` still coalesces them into a single instanced
// composite draw — scope boundaries don't affect coalescing, only kind/sigma identity.
{
draw.backdrop_scope(zone3)
// Edge case 1: sigma = 0 "mirror" — sharp framebuffer sample, no blur. Should reproduce
// the underlying pixels exactly through the SDF mask. Tinted slightly so it's visible.
draw.backdrop_blur(
zone3,
{WINDOW_W * 0.55 + 30, 310, 150, 70},
gaussian_sigma = 0,
tint = draw.WHITE, // pure mirror (no blur, no tint)
radii = draw.Rectangle_Radii{12, 12, 12, 12},
)
// Edge case 2: two same-sigma panels submitted contiguously. The sub-batch coalescer
// should merge these into a single instanced V-composite draw. Visually, both should
// look identical (modulo position) — same blur radius, same tint.
draw.backdrop_blur(
zone3,
{WINDOW_W * 0.55 + 30, 400, 150, 70},
gaussian_sigma = 8,
tint = draw.Color{160, 255, 160, 200}, // green tint, strong mix
radii = draw.Rectangle_Radii{12, 12, 12, 12},
)
draw.backdrop_blur(
zone3,
{WINDOW_W * 0.55 + 200, 400, 150, 70},
gaussian_sigma = 8,
tint = draw.Color{160, 255, 160, 200}, // identical: tests sub-batch coalescing
radii = draw.Rectangle_Radii{12, 12, 12, 12},
)
}
// Edge case 3: text drawn AFTER `end_backdrop` in the same layer. Composites on top of
// the bracket's V-composite output and should appear sharply over the green panels.
draw.text(
zone3,
"sigma=0 (mirror)",
{WINDOW_W * 0.55 + 38, 318},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.Color{20, 20, 20, 255},
)
draw.text(
zone3,
"sigma=8 (coalesced pair)",
{WINDOW_W * 0.55 + 38, 408},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.Color{20, 40, 20, 255},
)
draw.text(
zone3,
"Post-scope text overlay",
{WINDOW_W * 0.55 + 38, 480},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.WHITE,
)
draw.end(gpu, window, draw.Color{15, 15, 22, 255})
}
}
// Backdrop diagnostic example.
//
// Minimal isolation harness for debugging the blur. ONE panel, ONE sigma, NO animation. The
// fixed background gives the eye a stable reference: the blur should smooth a *known* set of
// hard edges, and any artifacts (crisp circles, ghost mirrors, no apparent change with sigma)
// stand out clearly.
//
// Controls:
// UP / DOWN arrow : adjust sigma by ±1
// LEFT / RIGHT arrow : adjust sigma by ±5
// SPACE : reset to sigma=10
// T : toggle the test rectangle on top of the panel
//
// Sigma is printed to the title bar so you can correlate visual behavior with the numeric
// value as you adjust it.
gaussian_blur_debug :: proc() {
if !sdl.Init({.VIDEO}) do os.exit(1)
window := sdl.CreateWindow("Backdrop debug", 800, 600, {.HIGH_PIXEL_DENSITY})
gpu := sdl.CreateGPUDevice(draw.PLATFORM_SHADER_FORMAT, true, nil)
if !sdl.ClaimWindowForGPUDevice(gpu, window) do os.exit(1)
if !draw.init(gpu, window) do os.exit(1)
defer draw.destroy(gpu)
PLEX_SANS_REGULAR = draw.register_font(cyber.SANS_REGULAR_RAW)
WINDOW_W :: f32(800)
WINDOW_H :: f32(600)
FONT_SIZE :: u16(14)
sigma: f32 = 10
show_test_rect := true
for {
defer free_all(context.temp_allocator)
ev: sdl.Event
for sdl.PollEvent(&ev) {
if ev.type == .QUIT do return
if ev.type == .KEY_DOWN {
#partial switch ev.key.scancode {
case .UP: sigma += 1
case .DOWN: sigma = max(sigma - 1, 0)
case .RIGHT: sigma += 5
case .LEFT: sigma = max(sigma - 5, 0)
case .SPACE: sigma = 10
case .T: show_test_rect = !show_test_rect
}
}
}
// Update title with current sigma so we can correlate visuals to numbers.
title := fmt.ctprintf("Backdrop debug | sigma = %.1f", sigma)
sdl.SetWindowTitle(window, title)
base_layer := draw.begin({width = WINDOW_W, height = WINDOW_H})
// Background: deliberately high-contrast static content. The eye can verify whether
// hard edges (the black grid lines, the crisp circles, the fine vertical bars) get
// smoothed by the panel. NOTHING animates here — every difference between frames is
// caused by user input (sigma change), not by the demo itself.
draw.rectangle(base_layer, {0, 0, WINDOW_W, WINDOW_H}, draw.Color{255, 255, 255, 255})
// Black grid: 8x6 cells with thin lines. Each grid cell is 100x100 logical px.
for x: f32 = 0; x <= WINDOW_W; x += 100 {
draw.rectangle(base_layer, {x - 1, 0, 2, WINDOW_H}, draw.BLACK)
}
for y: f32 = 0; y <= WINDOW_H; y += 100 {
draw.rectangle(base_layer, {0, y - 1, WINDOW_W, 2}, draw.BLACK)
}
// A row of small bright circles across the middle. Their crisp edges are the most
// sensitive blur indicator.
for i in 0 ..< 8 {
cx := f32(i) * 100 + 50
color := draw.Color{u8((i * 32) & 0xff), u8((i * 64) & 0xff), u8(255 - (i * 32) & 0xff), 255}
draw.circle(base_layer, {cx, 350}, 25, color)
}
// Vertical fine-detail stripes on the left edge. At any meaningful sigma these should
// merge into a flat color through the panel.
for i in 0 ..< 20 {
x := 30 + f32(i) * 6
color := draw.RED if i % 2 == 0 else draw.BLUE
draw.rectangle(base_layer, {x, 200, 4, 200}, color)
}
// THE PANEL UNDER TEST. Square, centered, large enough to cover multiple grid cells and
// the circle row. Square shape makes any horizontal-vs-vertical asymmetry purely
// renderer-driven (geometry can't introduce it).
//
// Uses the explicit begin/end form (instead of `backdrop_scope`) to exercise the
// alternative API surface in the diagnostic harness.
panel := draw.Rectangle{250, 150, 300, 300}
draw.begin_backdrop(base_layer)
draw.backdrop_blur(
base_layer,
panel,
gaussian_sigma = sigma,
tint = draw.WHITE,
radii = draw.Rectangle_Radii{20, 20, 20, 20},
)
draw.end_backdrop(base_layer)
// Post-scope test: a bright rectangle drawn AFTER `end_backdrop` in the same layer.
// Should always render on top of the panel. If the panel ever shows a "ghost" of this
// rect inside its blur, the V-composite is sampling the wrong texture state.
if show_test_rect {
draw.rectangle(base_layer, {380, 280, 40, 40}, draw.Color{0, 200, 0, 255})
}
// Sigma label at the bottom in giant text so you can read it from across the room.
draw.text(
base_layer,
fmt.tprintf("sigma = %.1f", sigma),
{20, WINDOW_H - 40},
PLEX_SANS_REGULAR,
28,
color = draw.BLACK,
)
draw.text(
base_layer,
"UP/DOWN ±1 LEFT/RIGHT ±5 SPACE reset T toggle test rect",
{20, WINDOW_H - 70},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.Color{60, 60, 60, 255},
)
draw.end(gpu, window, draw.Color{255, 255, 255, 255})
}
}
-92
View File
@@ -1,92 +0,0 @@
package examples
import "core:fmt"
import "core:log"
import "core:mem"
import "core:os"
EX_HELLOPE_SHAPES :: "hellope-shapes"
EX_HELLOPE_TEXT :: "hellope-text"
EX_HELLOPE_CLAY :: "hellope-clay"
EX_HELLOPE_CUSTOM :: "hellope-custom"
EX_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)
}
}
Binary file not shown.
Binary file not shown.
+71 -94
View File
@@ -1,15 +1,13 @@
package examples package examples
import "../../draw"
import "../../vendor/clay"
import "core:math" import "core:math"
import "core:os" import "core:os"
import sdl "vendor:sdl3" import sdl "vendor:sdl3"
import "../../draw" JETBRAINS_MONO_REGULAR_RAW :: #load("fonts/JetBrainsMono-Regular.ttf")
import "../../draw/tess" JETBRAINS_MONO_REGULAR: draw.Font_Id = max(draw.Font_Id) // Max so we crash if registration is forgotten
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() { hellope_shapes :: proc() {
if !sdl.Init({.VIDEO}) do os.exit(1) if !sdl.Init({.VIDEO}) do os.exit(1)
@@ -30,25 +28,19 @@ hellope_shapes :: proc() {
base_layer := draw.begin({width = 500, height = 500}) base_layer := draw.begin({width = 500, height = 500})
// Background // Background
draw.rectangle(base_layer, {0, 0, 500, 500}, draw.Color{40, 40, 40, 255}) draw.rectangle(base_layer, {0, 0, 500, 500}, {40, 40, 40, 255})
// ----- Shapes without rotation (existing demo) ----- // ----- Shapes without rotation (existing demo) -----
draw.rectangle( draw.rectangle(base_layer, {20, 20, 200, 120}, {80, 120, 200, 255})
base_layer, draw.rectangle_lines(base_layer, {20, 20, 200, 120}, draw.WHITE, thickness = 2)
{20, 20, 200, 120}, draw.rectangle(base_layer, {240, 20, 240, 120}, {200, 80, 80, 255}, roundness = 0.3)
draw.Color{80, 120, 200, 255}, draw.rectangle_gradient(
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, base_layer,
{20, 160, 460, 60}, {20, 160, 460, 60},
draw.Linear_Gradient{start_color = {255, 0, 0, 255}, end_color = {0, 0, 255, 255}, angle = 0}, {255, 0, 0, 255},
{0, 255, 0, 255},
{0, 0, 255, 255},
{255, 255, 0, 255},
) )
// ----- Rotation demos ----- // ----- Rotation demos -----
@@ -58,12 +50,17 @@ hellope_shapes :: proc() {
draw.rectangle( draw.rectangle(
base_layer, base_layer,
rect, rect,
draw.Color{100, 200, 100, 255}, {100, 200, 100, 255},
outline_color = draw.WHITE, origin = draw.center_of(rect),
outline_width = 2, rotation = spin_angle,
)
draw.rectangle_lines(
base_layer,
rect,
draw.WHITE,
thickness = 2,
origin = draw.center_of(rect), origin = draw.center_of(rect),
rotation = spin_angle, rotation = spin_angle,
feather_px = 1,
) )
// Rounded rectangle rotating around its center // Rounded rectangle rotating around its center
@@ -71,46 +68,30 @@ hellope_shapes :: proc() {
draw.rectangle( draw.rectangle(
base_layer, base_layer,
rrect, rrect,
draw.Color{200, 100, 200, 255}, {200, 100, 200, 255},
radii = draw.uniform_radii(rrect, 0.4), roundness = 0.4,
origin = draw.center_of(rrect), origin = draw.center_of(rrect),
rotation = spin_angle, rotation = spin_angle,
) )
// Ellipse rotating around its center (tilted ellipse) // Ellipse rotating around its center (tilted ellipse)
draw.ellipse(base_layer, {410, 340}, 50, 30, draw.Color{255, 200, 50, 255}, rotation = spin_angle) draw.ellipse(base_layer, {410, 340}, 50, 30, {255, 200, 50, 255}, rotation = spin_angle)
// Circle orbiting a point (moon orbiting planet) // Circle orbiting a point (moon orbiting planet)
// Convention B: center = pivot point (planet), origin = offset from moon center to pivot. // 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). // Moon's visual center at rotation=0: planet_pos - origin = (100, 450) - (0, 40) = (100, 410).
planet_pos := draw.Vec2{100, 450} planet_pos := [2]f32{100, 450}
draw.circle(base_layer, planet_pos, 8, draw.Color{200, 200, 200, 255}) // planet (stationary) draw.circle(base_layer, planet_pos, 8, {200, 200, 200, 255}) // planet (stationary)
draw.circle( draw.circle(base_layer, planet_pos, 5, {100, 150, 255, 255}, origin = {0, 40}, rotation = spin_angle) // moon orbiting
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 // Ring arc rotating in place
draw.ring( draw.ring(base_layer, {250, 450}, 15, 30, 0, 270, {100, 100, 220, 255}, rotation = spin_angle)
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 // Triangle rotating around its center
tv1 := draw.Vec2{350, 420} tv1 := [2]f32{350, 420}
tv2 := draw.Vec2{420, 480} tv2 := [2]f32{420, 480}
tv3 := draw.Vec2{340, 480} tv3 := [2]f32{340, 480}
tess.triangle_aa( draw.triangle(
base_layer, base_layer,
tv1, tv1,
tv2, tv2,
@@ -121,16 +102,8 @@ hellope_shapes :: proc() {
) )
// Polygon rotating around its center (already had rotation; now with origin for orbit) // Polygon rotating around its center (already had rotation; now with origin for orbit)
draw.polygon( draw.polygon(base_layer, {460, 450}, 6, 30, {180, 100, 220, 255}, rotation = spin_angle)
base_layer, draw.polygon_lines(base_layer, {460, 450}, 6, 30, draw.WHITE, rotation = spin_angle, thickness = 2)
{460, 450},
6,
30,
draw.Color{180, 100, 220, 255},
outline_color = draw.WHITE,
outline_width = 2,
rotation = spin_angle,
)
draw.end(gpu, window) draw.end(gpu, window)
} }
@@ -147,7 +120,7 @@ hellope_text :: proc() {
gpu := sdl.CreateGPUDevice(draw.PLATFORM_SHADER_FORMAT, true, nil) gpu := sdl.CreateGPUDevice(draw.PLATFORM_SHADER_FORMAT, true, nil)
if !sdl.ClaimWindowForGPUDevice(gpu, window) do os.exit(1) if !sdl.ClaimWindowForGPUDevice(gpu, window) do os.exit(1)
if !draw.init(gpu, window) do os.exit(1) if !draw.init(gpu, window) do os.exit(1)
PLEX_SANS_REGULAR = draw.register_font(cyber.SANS_REGULAR_RAW) JETBRAINS_MONO_REGULAR = draw.register_font(JETBRAINS_MONO_REGULAR_RAW)
FONT_SIZE :: u16(24) FONT_SIZE :: u16(24)
spin_angle: f32 = 0 spin_angle: f32 = 0
@@ -161,6 +134,9 @@ hellope_text :: proc() {
spin_angle += 0.5 spin_angle += 0.5
base_layer := draw.begin({width = 600, height = 600}) base_layer := draw.begin({width = 600, height = 600})
// Grey background
draw.rectangle(base_layer, {0, 0, 600, 600}, {127, 127, 127, 255})
// ----- Text API demos ----- // ----- Text API demos -----
// Cached text with id — TTF_Text reused across frames (good for text-heavy apps) // Cached text with id — TTF_Text reused across frames (good for text-heavy apps)
@@ -168,10 +144,10 @@ hellope_text :: proc() {
base_layer, base_layer,
"Hellope!", "Hellope!",
{300, 80}, {300, 80},
PLEX_SANS_REGULAR, JETBRAINS_MONO_REGULAR,
FONT_SIZE, FONT_SIZE,
color = draw.WHITE, color = draw.WHITE,
origin = draw.center_of("Hellope!", PLEX_SANS_REGULAR, FONT_SIZE), origin = draw.center_of("Hellope!", JETBRAINS_MONO_REGULAR, FONT_SIZE),
id = HELLOPE_ID, id = HELLOPE_ID,
) )
@@ -180,28 +156,35 @@ hellope_text :: proc() {
base_layer, base_layer,
"Hellope World!", "Hellope World!",
{300, 250}, {300, 250},
PLEX_SANS_REGULAR, JETBRAINS_MONO_REGULAR,
FONT_SIZE, FONT_SIZE,
color = {255, 200, 50, 255}, color = {255, 200, 50, 255},
origin = draw.center_of("Hellope World!", PLEX_SANS_REGULAR, FONT_SIZE), origin = draw.center_of("Hellope World!", JETBRAINS_MONO_REGULAR, FONT_SIZE),
rotation = spin_angle, rotation = spin_angle,
id = ROTATING_SENTENCE_ID, id = ROTATING_SENTENCE_ID,
) )
// Uncached text (no id) — created and destroyed each frame, simplest usage // 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) draw.text(
base_layer,
"Top-left anchored",
{20, 450},
JETBRAINS_MONO_REGULAR,
FONT_SIZE,
color = draw.WHITE,
)
// Measure text for manual layout // Measure text for manual layout
size := draw.measure_text("Measured!", PLEX_SANS_REGULAR, FONT_SIZE) size := draw.measure_text("Measured!", JETBRAINS_MONO_REGULAR, FONT_SIZE)
draw.rectangle(base_layer, {300 - size.x / 2, 380, size.x, size.y}, draw.Color{60, 60, 60, 200}) draw.rectangle(base_layer, {300 - size.x / 2, 380, size.x, size.y}, {60, 60, 60, 200})
draw.text( draw.text(
base_layer, base_layer,
"Measured!", "Measured!",
{300, 380}, {300, 380},
PLEX_SANS_REGULAR, JETBRAINS_MONO_REGULAR,
FONT_SIZE, FONT_SIZE,
color = draw.WHITE, color = draw.WHITE,
origin = draw.top_of("Measured!", PLEX_SANS_REGULAR, FONT_SIZE), origin = draw.top_of("Measured!", JETBRAINS_MONO_REGULAR, FONT_SIZE),
id = MEASURED_ID, id = MEASURED_ID,
) )
@@ -210,14 +193,14 @@ hellope_text :: proc() {
base_layer, base_layer,
"Corner spin", "Corner spin",
{150, 530}, {150, 530},
PLEX_SANS_REGULAR, JETBRAINS_MONO_REGULAR,
FONT_SIZE, FONT_SIZE,
color = {100, 200, 255, 255}, color = {100, 200, 255, 255},
rotation = spin_angle, rotation = spin_angle,
id = CORNER_SPIN_ID, id = CORNER_SPIN_ID,
) )
draw.end(gpu, window, draw.Color{127, 127, 127, 255}) draw.end(gpu, window)
} }
} }
@@ -227,10 +210,10 @@ hellope_clay :: proc() {
gpu := sdl.CreateGPUDevice(draw.PLATFORM_SHADER_FORMAT, true, nil) gpu := sdl.CreateGPUDevice(draw.PLATFORM_SHADER_FORMAT, true, nil)
if !sdl.ClaimWindowForGPUDevice(gpu, window) do os.exit(1) if !sdl.ClaimWindowForGPUDevice(gpu, window) do os.exit(1)
if !draw.init(gpu, window) do os.exit(1) if !draw.init(gpu, window) do os.exit(1)
PLEX_SANS_REGULAR = draw.register_font(cyber.SANS_REGULAR_RAW) JETBRAINS_MONO_REGULAR = draw.register_font(JETBRAINS_MONO_REGULAR_RAW)
text_config := clay.TextElementConfig { text_config := clay.TextElementConfig {
fontId = PLEX_SANS_REGULAR, fontId = JETBRAINS_MONO_REGULAR,
fontSize = 36, fontSize = 36,
textColor = {255, 255, 255, 255}, textColor = {255, 255, 255, 255},
} }
@@ -271,10 +254,10 @@ hellope_custom :: proc() {
gpu := sdl.CreateGPUDevice(draw.PLATFORM_SHADER_FORMAT, true, nil) gpu := sdl.CreateGPUDevice(draw.PLATFORM_SHADER_FORMAT, true, nil)
if !sdl.ClaimWindowForGPUDevice(gpu, window) do os.exit(1) if !sdl.ClaimWindowForGPUDevice(gpu, window) do os.exit(1)
if !draw.init(gpu, window) do os.exit(1) if !draw.init(gpu, window) do os.exit(1)
PLEX_SANS_REGULAR = draw.register_font(cyber.SANS_REGULAR_RAW) JETBRAINS_MONO_REGULAR = draw.register_font(JETBRAINS_MONO_REGULAR_RAW)
text_config := clay.TextElementConfig { text_config := clay.TextElementConfig {
fontId = PLEX_SANS_REGULAR, fontId = JETBRAINS_MONO_REGULAR,
fontSize = 24, fontSize = 24,
textColor = {255, 255, 255, 255}, textColor = {255, 255, 255, 255},
} }
@@ -355,21 +338,15 @@ hellope_custom :: proc() {
draw_custom :: proc(layer: ^draw.Layer, bounds: draw.Rectangle, render_data: clay.CustomRenderData) { draw_custom :: proc(layer: ^draw.Layer, bounds: draw.Rectangle, render_data: clay.CustomRenderData) {
gauge := cast(^Gauge)render_data.customData gauge := cast(^Gauge)render_data.customData
border_width: f32 = 2 // Background from clay's backgroundColor
draw.rectangle( draw.rectangle(layer, bounds, draw.color_from_clay(render_data.backgroundColor), roundness = 0.25)
layer,
bounds,
draw.color_from_clay(render_data.backgroundColor),
outline_color = draw.WHITE,
outline_width = border_width,
)
fill := draw.Rectangle { // Fill bar
x = bounds.x, fill := bounds
y = bounds.y, fill.width *= gauge.value
width = bounds.width * gauge.value, draw.rectangle(layer, fill, gauge.color, roundness = 0.25)
height = bounds.height,
} // Border
draw.rectangle(layer, fill, gauge.color) draw.rectangle_lines(layer, bounds, draw.WHITE, thickness = 2, roundness = 0.25)
} }
} }
+75
View File
@@ -0,0 +1,75 @@
package examples
import "core:fmt"
import "core:mem"
import "core:os"
main :: proc() {
//----- Tracking allocator ----------------------------------
{
tracking_temp_allocator := false
// Temp
track_temp: mem.Tracking_Allocator
if tracking_temp_allocator {
mem.tracking_allocator_init(&track_temp, context.temp_allocator)
context.temp_allocator = mem.tracking_allocator(&track_temp)
}
// Default
track: mem.Tracking_Allocator
mem.tracking_allocator_init(&track, context.allocator)
context.allocator = mem.tracking_allocator(&track)
// Log a warning about any memory that was not freed by the end of the program.
// This could be fine for some global state or it could be a memory leak.
defer {
// Temp allocator
if tracking_temp_allocator {
if len(track_temp.allocation_map) > 0 {
fmt.eprintf("=== %v allocations not freed - temp allocator: ===\n", len(track_temp.allocation_map))
for _, entry in track_temp.allocation_map {
fmt.eprintf("- %v bytes @ %v\n", entry.size, entry.location)
}
}
if len(track_temp.bad_free_array) > 0 {
fmt.eprintf("=== %v incorrect frees - temp allocator: ===\n", len(track_temp.bad_free_array))
for entry in track_temp.bad_free_array {
fmt.eprintf("- %p @ %v\n", entry.memory, entry.location)
}
}
mem.tracking_allocator_destroy(&track_temp)
}
// Default allocator
if len(track.allocation_map) > 0 {
fmt.eprintf("=== %v allocations not freed - main allocator: ===\n", len(track.allocation_map))
for _, entry in track.allocation_map {
fmt.eprintf("- %v bytes @ %v\n", entry.size, entry.location)
}
}
if len(track.bad_free_array) > 0 {
fmt.eprintf("=== %v incorrect frees - main allocator: ===\n", len(track.bad_free_array))
for entry in track.bad_free_array {
fmt.eprintf("- %p @ %v\n", entry.memory, entry.location)
}
}
mem.tracking_allocator_destroy(&track)
}
}
args := os.args
if len(args) < 2 {
fmt.eprintln("Usage: examples <example_name>")
fmt.eprintln("Available examples: hellope-shapes, hellope-text, hellope-clay, hellope-custom, textures")
os.exit(1)
}
switch args[1] {
case "hellope-clay": hellope_clay()
case "hellope-custom": hellope_custom()
case "hellope-shapes": hellope_shapes()
case "hellope-text": hellope_text()
case "textures": textures()
case:
fmt.eprintf("Unknown example: %v\n", args[1])
fmt.eprintln("Available examples: hellope-shapes, hellope-text, hellope-clay, hellope-custom, textures")
os.exit(1)
}
}
+41 -179
View File
@@ -1,19 +1,17 @@
package examples package examples
import "core:os"
import sdl "vendor:sdl3"
import "../../draw" import "../../draw"
import "../../draw/draw_qr" import "../../draw/draw_qr"
import cyber "../cybersteel" import "core:os"
import sdl "vendor:sdl3"
textures :: proc() { textures :: proc() {
if !sdl.Init({.VIDEO}) do os.exit(1) if !sdl.Init({.VIDEO}) do os.exit(1)
window := sdl.CreateWindow("Textures", 800, 750, {.HIGH_PIXEL_DENSITY}) window := sdl.CreateWindow("Textures", 800, 600, {.HIGH_PIXEL_DENSITY})
gpu := sdl.CreateGPUDevice(draw.PLATFORM_SHADER_FORMAT, true, nil) gpu := sdl.CreateGPUDevice(draw.PLATFORM_SHADER_FORMAT, true, nil)
if !sdl.ClaimWindowForGPUDevice(gpu, window) do os.exit(1) if !sdl.ClaimWindowForGPUDevice(gpu, window) do os.exit(1)
if !draw.init(gpu, window) do os.exit(1) if !draw.init(gpu, window) do os.exit(1)
PLEX_SANS_REGULAR = draw.register_font(cyber.SANS_REGULAR_RAW) JETBRAINS_MONO_REGULAR = draw.register_font(JETBRAINS_MONO_REGULAR_RAW)
FONT_SIZE :: u16(14) FONT_SIZE :: u16(14)
LABEL_OFFSET :: f32(8) // gap between item and its label LABEL_OFFSET :: f32(8) // gap between item and its label
@@ -88,10 +86,10 @@ textures :: proc() {
} }
spin_angle += 1 spin_angle += 1
base_layer := draw.begin({width = 800, height = 750}) base_layer := draw.begin({width = 800, height = 600})
// Background // Background
draw.rectangle(base_layer, {0, 0, 800, 750}, draw.Color{30, 30, 30, 255}) draw.rectangle(base_layer, {0, 0, 800, 600}, {30, 30, 30, 255})
//----- Row 1: Sampler presets (y=30) ---------------------------------- //----- Row 1: Sampler presets (y=30) ----------------------------------
@@ -103,61 +101,50 @@ textures :: proc() {
COL4 :: f32(480) COL4 :: f32(480)
// Nearest (sharp pixel edges) // Nearest (sharp pixel edges)
draw.rectangle( draw.rectangle_texture(
base_layer, base_layer,
{COL1, ROW1_Y, ITEM_SIZE, ITEM_SIZE}, {COL1, ROW1_Y, ITEM_SIZE, ITEM_SIZE},
draw.Texture_Fill { checker_texture,
id = checker_texture,
tint = draw.WHITE,
uv_rect = {0, 0, 1, 1},
sampler = .Nearest_Clamp, sampler = .Nearest_Clamp,
},
) )
draw.text( draw.text(
base_layer, base_layer,
"Nearest", "Nearest",
{COL1, ROW1_Y + ITEM_SIZE + LABEL_OFFSET}, {COL1, ROW1_Y + ITEM_SIZE + LABEL_OFFSET},
PLEX_SANS_REGULAR, JETBRAINS_MONO_REGULAR,
FONT_SIZE, FONT_SIZE,
color = draw.WHITE, color = draw.WHITE,
) )
// Linear (bilinear blur) // Linear (bilinear blur)
draw.rectangle( draw.rectangle_texture(
base_layer, base_layer,
{COL2, ROW1_Y, ITEM_SIZE, ITEM_SIZE}, {COL2, ROW1_Y, ITEM_SIZE, ITEM_SIZE},
draw.Texture_Fill { checker_texture,
id = checker_texture,
tint = draw.WHITE,
uv_rect = {0, 0, 1, 1},
sampler = .Linear_Clamp, sampler = .Linear_Clamp,
},
) )
draw.text( draw.text(
base_layer, base_layer,
"Linear", "Linear",
{COL2, ROW1_Y + ITEM_SIZE + LABEL_OFFSET}, {COL2, ROW1_Y + ITEM_SIZE + LABEL_OFFSET},
PLEX_SANS_REGULAR, JETBRAINS_MONO_REGULAR,
FONT_SIZE, FONT_SIZE,
color = draw.WHITE, color = draw.WHITE,
) )
// Tiled (4x repeat) // Tiled (4x repeat)
draw.rectangle( draw.rectangle_texture(
base_layer, base_layer,
{COL3, ROW1_Y, ITEM_SIZE, ITEM_SIZE}, {COL3, ROW1_Y, ITEM_SIZE, ITEM_SIZE},
draw.Texture_Fill { checker_texture,
id = checker_texture,
tint = draw.WHITE,
uv_rect = {0, 0, 4, 4},
sampler = .Nearest_Repeat, sampler = .Nearest_Repeat,
}, uv_rect = {0, 0, 4, 4},
) )
draw.text( draw.text(
base_layer, base_layer,
"Tiled 4x", "Tiled 4x",
{COL3, ROW1_Y + ITEM_SIZE + LABEL_OFFSET}, {COL3, ROW1_Y + ITEM_SIZE + LABEL_OFFSET},
PLEX_SANS_REGULAR, JETBRAINS_MONO_REGULAR,
FONT_SIZE, FONT_SIZE,
color = draw.WHITE, color = draw.WHITE,
) )
@@ -167,53 +154,46 @@ textures :: proc() {
ROW2_Y :: f32(190) ROW2_Y :: f32(190)
// QR code (RGBA texture with baked colors, nearest sampling) // 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}, {255, 255, 255, 255}) // white bg
draw.rectangle( draw.rectangle_texture(
base_layer, base_layer,
{COL1, ROW2_Y, ITEM_SIZE, ITEM_SIZE}, {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}, qr_texture,
sampler = .Nearest_Clamp,
) )
draw.text( draw.text(
base_layer, base_layer,
"QR Code", "QR Code",
{COL1, ROW2_Y + ITEM_SIZE + LABEL_OFFSET}, {COL1, ROW2_Y + ITEM_SIZE + LABEL_OFFSET},
PLEX_SANS_REGULAR, JETBRAINS_MONO_REGULAR,
FONT_SIZE, FONT_SIZE,
color = draw.WHITE, color = draw.WHITE,
) )
// Rounded corners // Rounded corners
draw.rectangle( draw.rectangle_texture(
base_layer, base_layer,
{COL2, ROW2_Y, ITEM_SIZE, ITEM_SIZE}, {COL2, ROW2_Y, ITEM_SIZE, ITEM_SIZE},
draw.Texture_Fill { checker_texture,
id = checker_texture,
tint = draw.WHITE,
uv_rect = {0, 0, 1, 1},
sampler = .Nearest_Clamp, sampler = .Nearest_Clamp,
}, roundness = 0.3,
radii = draw.uniform_radii({COL2, ROW2_Y, ITEM_SIZE, ITEM_SIZE}, 0.3),
) )
draw.text( draw.text(
base_layer, base_layer,
"Rounded", "Rounded",
{COL2, ROW2_Y + ITEM_SIZE + LABEL_OFFSET}, {COL2, ROW2_Y + ITEM_SIZE + LABEL_OFFSET},
PLEX_SANS_REGULAR, JETBRAINS_MONO_REGULAR,
FONT_SIZE, FONT_SIZE,
color = draw.WHITE, color = draw.WHITE,
) )
// Rotating // Rotating
rot_rect := draw.Rectangle{COL3, ROW2_Y, ITEM_SIZE, ITEM_SIZE} rot_rect := draw.Rectangle{COL3, ROW2_Y, ITEM_SIZE, ITEM_SIZE}
draw.rectangle( draw.rectangle_texture(
base_layer, base_layer,
rot_rect, rot_rect,
draw.Texture_Fill { checker_texture,
id = checker_texture,
tint = draw.WHITE,
uv_rect = {0, 0, 1, 1},
sampler = .Nearest_Clamp, sampler = .Nearest_Clamp,
},
origin = draw.center_of(rot_rect), origin = draw.center_of(rot_rect),
rotation = spin_angle, rotation = spin_angle,
) )
@@ -221,7 +201,7 @@ textures :: proc() {
base_layer, base_layer,
"Rotating", "Rotating",
{COL3, ROW2_Y + ITEM_SIZE + LABEL_OFFSET}, {COL3, ROW2_Y + ITEM_SIZE + LABEL_OFFSET},
PLEX_SANS_REGULAR, JETBRAINS_MONO_REGULAR,
FONT_SIZE, FONT_SIZE,
color = draw.WHITE, color = draw.WHITE,
) )
@@ -233,174 +213,56 @@ textures :: proc() {
// Stretch // Stretch
uv_s, sampler_s, inner_s := draw.fit_params(.Stretch, {COL1, ROW3_Y, FIT_SIZE, FIT_SIZE}, stripe_texture) 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, {COL1, ROW3_Y, FIT_SIZE, FIT_SIZE}, {60, 60, 60, 255}) // bg
draw.rectangle( draw.rectangle_texture(base_layer, inner_s, stripe_texture, uv_rect = uv_s, sampler = sampler_s)
base_layer,
inner_s,
draw.Texture_Fill{id = stripe_texture, tint = draw.WHITE, uv_rect = uv_s, sampler = sampler_s},
)
draw.text( draw.text(
base_layer, base_layer,
"Stretch", "Stretch",
{COL1, ROW3_Y + FIT_SIZE + LABEL_OFFSET}, {COL1, ROW3_Y + FIT_SIZE + LABEL_OFFSET},
PLEX_SANS_REGULAR, JETBRAINS_MONO_REGULAR,
FONT_SIZE, FONT_SIZE,
color = draw.WHITE, color = draw.WHITE,
) )
// Fill (center-crop) // Fill (center-crop)
uv_f, sampler_f, inner_f := draw.fit_params(.Fill, {COL2, ROW3_Y, FIT_SIZE, FIT_SIZE}, stripe_texture) 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, {COL2, ROW3_Y, FIT_SIZE, FIT_SIZE}, {60, 60, 60, 255})
draw.rectangle( draw.rectangle_texture(base_layer, inner_f, stripe_texture, uv_rect = uv_f, sampler = sampler_f)
base_layer,
inner_f,
draw.Texture_Fill{id = stripe_texture, tint = draw.WHITE, uv_rect = uv_f, sampler = sampler_f},
)
draw.text( draw.text(
base_layer, base_layer,
"Fill", "Fill",
{COL2, ROW3_Y + FIT_SIZE + LABEL_OFFSET}, {COL2, ROW3_Y + FIT_SIZE + LABEL_OFFSET},
PLEX_SANS_REGULAR, JETBRAINS_MONO_REGULAR,
FONT_SIZE, FONT_SIZE,
color = draw.WHITE, color = draw.WHITE,
) )
// Fit (letterbox) // Fit (letterbox)
uv_ft, sampler_ft, inner_ft := draw.fit_params(.Fit, {COL3, ROW3_Y, FIT_SIZE, FIT_SIZE}, stripe_texture) 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, {COL3, ROW3_Y, FIT_SIZE, FIT_SIZE}, {60, 60, 60, 255}) // visible margin bg
draw.rectangle( draw.rectangle_texture(base_layer, inner_ft, stripe_texture, uv_rect = uv_ft, sampler = sampler_ft)
base_layer,
inner_ft,
draw.Texture_Fill{id = stripe_texture, tint = draw.WHITE, uv_rect = uv_ft, sampler = sampler_ft},
)
draw.text( draw.text(
base_layer, base_layer,
"Fit", "Fit",
{COL3, ROW3_Y + FIT_SIZE + LABEL_OFFSET}, {COL3, ROW3_Y + FIT_SIZE + LABEL_OFFSET},
PLEX_SANS_REGULAR, JETBRAINS_MONO_REGULAR,
FONT_SIZE, FONT_SIZE,
color = draw.WHITE, color = draw.WHITE,
) )
// Per-corner radii // Per-corner radii
draw.rectangle( draw.rectangle_texture_corners(
base_layer, base_layer,
{COL4, ROW3_Y, FIT_SIZE, FIT_SIZE}, {COL4, ROW3_Y, FIT_SIZE, FIT_SIZE},
draw.Texture_Fill { {20, 0, 20, 0},
id = checker_texture, checker_texture,
tint = draw.WHITE,
uv_rect = {0, 0, 1, 1},
sampler = .Nearest_Clamp, sampler = .Nearest_Clamp,
},
radii = {20, 0, 20, 0},
) )
draw.text( draw.text(
base_layer, base_layer,
"Per-corner", "Per-corner",
{COL4, ROW3_Y + FIT_SIZE + LABEL_OFFSET}, {COL4, ROW3_Y + FIT_SIZE + LABEL_OFFSET},
PLEX_SANS_REGULAR, JETBRAINS_MONO_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, FONT_SIZE,
color = draw.WHITE, color = draw.WHITE,
) )
+685
View File
@@ -0,0 +1,685 @@
package draw
import "core:c"
import "core:log"
import "core:mem"
import sdl "vendor:sdl3"
Vertex :: struct {
position: [2]f32,
uv: [2]f32,
color: Color,
}
TextBatch :: struct {
atlas_texture: ^sdl.GPUTexture,
vertex_start: u32,
vertex_count: u32,
index_start: u32,
index_count: u32,
}
// ----------------------------------------------------------------------------------------------------------------
// ----- SDF primitive types -----------
// ----------------------------------------------------------------------------------------------------------------
Shape_Kind :: enum u8 {
Solid = 0,
RRect = 1,
Circle = 2,
Ellipse = 3,
Segment = 4,
Ring_Arc = 5,
NGon = 6,
}
Shape_Flag :: enum u8 {
Stroke,
Textured,
}
Shape_Flags :: bit_set[Shape_Flag;u8]
RRect_Params :: struct {
half_size: [2]f32,
radii: [4]f32,
soft_px: f32,
stroke_px: f32,
}
Circle_Params :: struct {
radius: f32,
soft_px: f32,
stroke_px: f32,
_: [5]f32,
}
Ellipse_Params :: struct {
radii: [2]f32,
soft_px: f32,
stroke_px: f32,
_: [4]f32,
}
Segment_Params :: struct {
a: [2]f32,
b: [2]f32,
width: f32,
soft_px: f32,
_: [2]f32,
}
Ring_Arc_Params :: struct {
inner_radius: f32,
outer_radius: f32,
start_rad: f32,
end_rad: f32,
soft_px: f32,
_: [3]f32,
}
NGon_Params :: struct {
radius: f32,
rotation: f32,
sides: f32,
soft_px: f32,
stroke_px: f32,
_: [3]f32,
}
Shape_Params :: struct #raw_union {
rrect: RRect_Params,
circle: Circle_Params,
ellipse: Ellipse_Params,
segment: Segment_Params,
ring_arc: Ring_Arc_Params,
ngon: NGon_Params,
raw: [8]f32,
}
#assert(size_of(Shape_Params) == 32)
// GPU layout: 64 bytes, std430-compatible. The shader declares this as a storage buffer struct.
Primitive :: struct {
bounds: [4]f32, // 0: min_x, min_y, max_x, max_y (world-space, pre-DPI)
color: Color, // 16: u8x4, unpacked in shader via unpackUnorm4x8
kind_flags: u32, // 20: (kind as u32) | (flags as u32 << 8)
rotation: f32, // 24: shader self-rotation in radians (used by RRect, Ellipse)
_pad: f32, // 28: alignment to vec4 boundary
params: Shape_Params, // 32: two vec4s of shape params
uv_rect: [4]f32, // 64: u_min, v_min, u_max, v_max (default {0,0,1,1})
}
#assert(size_of(Primitive) == 80)
pack_kind_flags :: #force_inline proc(kind: Shape_Kind, flags: Shape_Flags) -> u32 {
return u32(kind) | (u32(transmute(u8)flags) << 8)
}
Pipeline_2D_Base :: struct {
sdl_pipeline: ^sdl.GPUGraphicsPipeline,
vertex_buffer: Buffer,
index_buffer: Buffer,
unit_quad_buffer: ^sdl.GPUBuffer,
primitive_buffer: Buffer,
white_texture: ^sdl.GPUTexture,
sampler: ^sdl.GPUSampler,
}
@(private)
create_pipeline_2d_base :: proc(
device: ^sdl.GPUDevice,
window: ^sdl.Window,
sample_count: sdl.GPUSampleCount,
) -> (
pipeline: Pipeline_2D_Base,
ok: bool,
) {
// On failure, clean up any partially-created resources
defer if !ok {
if pipeline.sampler != nil do sdl.ReleaseGPUSampler(device, pipeline.sampler)
if pipeline.white_texture != nil do sdl.ReleaseGPUTexture(device, pipeline.white_texture)
if pipeline.unit_quad_buffer != nil do sdl.ReleaseGPUBuffer(device, pipeline.unit_quad_buffer)
if pipeline.primitive_buffer.gpu != nil do destroy_buffer(device, &pipeline.primitive_buffer)
if pipeline.index_buffer.gpu != nil do destroy_buffer(device, &pipeline.index_buffer)
if pipeline.vertex_buffer.gpu != nil do destroy_buffer(device, &pipeline.vertex_buffer)
if pipeline.sdl_pipeline != nil do sdl.ReleaseGPUGraphicsPipeline(device, pipeline.sdl_pipeline)
}
active_shader_formats := sdl.GetGPUShaderFormats(device)
if PLATFORM_SHADER_FORMAT_FLAG not_in active_shader_formats {
log.errorf(
"draw: no embedded shader matches active GPU formats; this build supports %v but device reports %v",
PLATFORM_SHADER_FORMAT,
active_shader_formats,
)
return pipeline, false
}
log.debug("Loaded", len(BASE_VERT_2D_RAW), "vert bytes")
log.debug("Loaded", len(BASE_FRAG_2D_RAW), "frag bytes")
vert_info := sdl.GPUShaderCreateInfo {
code_size = len(BASE_VERT_2D_RAW),
code = raw_data(BASE_VERT_2D_RAW),
entrypoint = SHADER_ENTRY,
format = {PLATFORM_SHADER_FORMAT_FLAG},
stage = .VERTEX,
num_uniform_buffers = 1,
num_storage_buffers = 1,
}
frag_info := sdl.GPUShaderCreateInfo {
code_size = len(BASE_FRAG_2D_RAW),
code = raw_data(BASE_FRAG_2D_RAW),
entrypoint = SHADER_ENTRY,
format = {PLATFORM_SHADER_FORMAT_FLAG},
stage = .FRAGMENT,
num_samplers = 1,
}
vert_shader := sdl.CreateGPUShader(device, vert_info)
if vert_shader == nil {
log.errorf("Could not create draw vertex shader: %s", sdl.GetError())
return pipeline, false
}
frag_shader := sdl.CreateGPUShader(device, frag_info)
if frag_shader == nil {
sdl.ReleaseGPUShader(device, vert_shader)
log.errorf("Could not create draw fragment shader: %s", sdl.GetError())
return pipeline, false
}
vertex_attributes: [3]sdl.GPUVertexAttribute = {
// position (GLSL location 0)
sdl.GPUVertexAttribute{buffer_slot = 0, location = 0, format = .FLOAT2, offset = 0},
// uv (GLSL location 1)
sdl.GPUVertexAttribute{buffer_slot = 0, location = 1, format = .FLOAT2, offset = size_of([2]f32)},
// color (GLSL location 2, u8x4 normalized to float by GPU)
sdl.GPUVertexAttribute{buffer_slot = 0, location = 2, format = .UBYTE4_NORM, offset = size_of([2]f32) * 2},
}
pipeline_info := sdl.GPUGraphicsPipelineCreateInfo {
vertex_shader = vert_shader,
fragment_shader = frag_shader,
primitive_type = .TRIANGLELIST,
multisample_state = sdl.GPUMultisampleState{sample_count = sample_count},
target_info = sdl.GPUGraphicsPipelineTargetInfo {
color_target_descriptions = &sdl.GPUColorTargetDescription {
format = sdl.GetGPUSwapchainTextureFormat(device, window),
blend_state = sdl.GPUColorTargetBlendState {
enable_blend = true,
enable_color_write_mask = true,
src_color_blendfactor = .SRC_ALPHA,
dst_color_blendfactor = .ONE_MINUS_SRC_ALPHA,
color_blend_op = .ADD,
src_alpha_blendfactor = .SRC_ALPHA,
dst_alpha_blendfactor = .ONE_MINUS_SRC_ALPHA,
alpha_blend_op = .ADD,
color_write_mask = sdl.GPUColorComponentFlags{.R, .G, .B, .A},
},
},
num_color_targets = 1,
},
vertex_input_state = sdl.GPUVertexInputState {
vertex_buffer_descriptions = &sdl.GPUVertexBufferDescription {
slot = 0,
input_rate = .VERTEX,
pitch = size_of(Vertex),
},
num_vertex_buffers = 1,
vertex_attributes = raw_data(vertex_attributes[:]),
num_vertex_attributes = 3,
},
}
pipeline.sdl_pipeline = sdl.CreateGPUGraphicsPipeline(device, pipeline_info)
// Shaders are no longer needed regardless of pipeline creation success
sdl.ReleaseGPUShader(device, vert_shader)
sdl.ReleaseGPUShader(device, frag_shader)
if pipeline.sdl_pipeline == nil {
log.errorf("Failed to create draw graphics pipeline: %s", sdl.GetError())
return pipeline, false
}
// Create vertex buffer
vert_buf_ok: bool
pipeline.vertex_buffer, vert_buf_ok = create_buffer(
device,
size_of(Vertex) * BUFFER_INIT_SIZE,
sdl.GPUBufferUsageFlags{.VERTEX},
)
if !vert_buf_ok do return pipeline, false
// Create index buffer (used by text)
idx_buf_ok: bool
pipeline.index_buffer, idx_buf_ok = create_buffer(
device,
size_of(c.int) * BUFFER_INIT_SIZE,
sdl.GPUBufferUsageFlags{.INDEX},
)
if !idx_buf_ok do return pipeline, false
// Create primitive storage buffer (used by SDF instanced drawing)
prim_buf_ok: bool
pipeline.primitive_buffer, prim_buf_ok = create_buffer(
device,
size_of(Primitive) * BUFFER_INIT_SIZE,
sdl.GPUBufferUsageFlags{.GRAPHICS_STORAGE_READ},
)
if !prim_buf_ok do return pipeline, false
// Create static 6-vertex unit quad buffer (two triangles, TRIANGLELIST)
pipeline.unit_quad_buffer = sdl.CreateGPUBuffer(
device,
sdl.GPUBufferCreateInfo{usage = {.VERTEX}, size = 6 * size_of(Vertex)},
)
if pipeline.unit_quad_buffer == nil {
log.errorf("Failed to create unit quad buffer: %s", sdl.GetError())
return pipeline, false
}
// Create 1x1 white pixel texture
pipeline.white_texture = sdl.CreateGPUTexture(
device,
sdl.GPUTextureCreateInfo {
type = .D2,
format = .R8G8B8A8_UNORM,
usage = {.SAMPLER},
width = 1,
height = 1,
layer_count_or_depth = 1,
num_levels = 1,
sample_count = ._1,
},
)
if pipeline.white_texture == nil {
log.errorf("Failed to create white pixel texture: %s", sdl.GetError())
return pipeline, false
}
// Upload white pixel and unit quad data in a single command buffer
white_pixel := [4]u8{255, 255, 255, 255}
white_transfer_buf := sdl.CreateGPUTransferBuffer(
device,
sdl.GPUTransferBufferCreateInfo{usage = .UPLOAD, size = size_of(white_pixel)},
)
if white_transfer_buf == nil {
log.errorf("Failed to create white pixel transfer buffer: %s", sdl.GetError())
return pipeline, false
}
defer sdl.ReleaseGPUTransferBuffer(device, white_transfer_buf)
white_ptr := sdl.MapGPUTransferBuffer(device, white_transfer_buf, false)
if white_ptr == nil {
log.errorf("Failed to map white pixel transfer buffer: %s", sdl.GetError())
return pipeline, false
}
mem.copy(white_ptr, &white_pixel, size_of(white_pixel))
sdl.UnmapGPUTransferBuffer(device, white_transfer_buf)
quad_verts := [6]Vertex {
{position = {0, 0}},
{position = {1, 0}},
{position = {0, 1}},
{position = {0, 1}},
{position = {1, 0}},
{position = {1, 1}},
}
quad_transfer_buf := sdl.CreateGPUTransferBuffer(
device,
sdl.GPUTransferBufferCreateInfo{usage = .UPLOAD, size = size_of(quad_verts)},
)
if quad_transfer_buf == nil {
log.errorf("Failed to create unit quad transfer buffer: %s", sdl.GetError())
return pipeline, false
}
defer sdl.ReleaseGPUTransferBuffer(device, quad_transfer_buf)
quad_ptr := sdl.MapGPUTransferBuffer(device, quad_transfer_buf, false)
if quad_ptr == nil {
log.errorf("Failed to map unit quad transfer buffer: %s", sdl.GetError())
return pipeline, false
}
mem.copy(quad_ptr, &quad_verts, size_of(quad_verts))
sdl.UnmapGPUTransferBuffer(device, quad_transfer_buf)
upload_cmd_buffer := sdl.AcquireGPUCommandBuffer(device)
if upload_cmd_buffer == nil {
log.errorf("Failed to acquire command buffer for init upload: %s", sdl.GetError())
return pipeline, false
}
upload_pass := sdl.BeginGPUCopyPass(upload_cmd_buffer)
sdl.UploadToGPUTexture(
upload_pass,
sdl.GPUTextureTransferInfo{transfer_buffer = white_transfer_buf},
sdl.GPUTextureRegion{texture = pipeline.white_texture, w = 1, h = 1, d = 1},
false,
)
sdl.UploadToGPUBuffer(
upload_pass,
sdl.GPUTransferBufferLocation{transfer_buffer = quad_transfer_buf},
sdl.GPUBufferRegion{buffer = pipeline.unit_quad_buffer, offset = 0, size = size_of(quad_verts)},
false,
)
sdl.EndGPUCopyPass(upload_pass)
if !sdl.SubmitGPUCommandBuffer(upload_cmd_buffer) {
log.errorf("Failed to submit init upload command buffer: %s", sdl.GetError())
return pipeline, false
}
log.debug("White pixel texture and unit quad buffer created and uploaded")
// Create sampler (shared by shapes and text)
pipeline.sampler = sdl.CreateGPUSampler(
device,
sdl.GPUSamplerCreateInfo {
min_filter = .LINEAR,
mag_filter = .LINEAR,
mipmap_mode = .LINEAR,
address_mode_u = .CLAMP_TO_EDGE,
address_mode_v = .CLAMP_TO_EDGE,
address_mode_w = .CLAMP_TO_EDGE,
},
)
if pipeline.sampler == nil {
log.errorf("Could not create GPU sampler: %s", sdl.GetError())
return pipeline, false
}
log.debug("Done creating unified draw pipeline")
return pipeline, true
}
@(private)
upload :: proc(device: ^sdl.GPUDevice, pass: ^sdl.GPUCopyPass) {
// Upload vertices (shapes then text into one buffer)
shape_vert_count := u32(len(GLOB.tmp_shape_verts))
text_vert_count := u32(len(GLOB.tmp_text_verts))
total_vert_count := shape_vert_count + text_vert_count
if total_vert_count > 0 {
total_vert_size := total_vert_count * size_of(Vertex)
shape_vert_size := shape_vert_count * size_of(Vertex)
text_vert_size := text_vert_count * size_of(Vertex)
grow_buffer_if_needed(
device,
&GLOB.pipeline_2d_base.vertex_buffer,
total_vert_size,
sdl.GPUBufferUsageFlags{.VERTEX},
)
vert_array := sdl.MapGPUTransferBuffer(device, GLOB.pipeline_2d_base.vertex_buffer.transfer, false)
if vert_array == nil {
log.panicf("Failed to map vertex transfer buffer: %s", sdl.GetError())
}
if shape_vert_size > 0 {
mem.copy(vert_array, raw_data(GLOB.tmp_shape_verts), int(shape_vert_size))
}
if text_vert_size > 0 {
mem.copy(
rawptr(uintptr(vert_array) + uintptr(shape_vert_size)),
raw_data(GLOB.tmp_text_verts),
int(text_vert_size),
)
}
sdl.UnmapGPUTransferBuffer(device, GLOB.pipeline_2d_base.vertex_buffer.transfer)
sdl.UploadToGPUBuffer(
pass,
sdl.GPUTransferBufferLocation{transfer_buffer = GLOB.pipeline_2d_base.vertex_buffer.transfer},
sdl.GPUBufferRegion{buffer = GLOB.pipeline_2d_base.vertex_buffer.gpu, offset = 0, size = total_vert_size},
false,
)
}
// Upload text indices
index_count := u32(len(GLOB.tmp_text_indices))
if index_count > 0 {
index_size := index_count * size_of(c.int)
grow_buffer_if_needed(
device,
&GLOB.pipeline_2d_base.index_buffer,
index_size,
sdl.GPUBufferUsageFlags{.INDEX},
)
idx_array := sdl.MapGPUTransferBuffer(device, GLOB.pipeline_2d_base.index_buffer.transfer, false)
if idx_array == nil {
log.panicf("Failed to map index transfer buffer: %s", sdl.GetError())
}
mem.copy(idx_array, raw_data(GLOB.tmp_text_indices), int(index_size))
sdl.UnmapGPUTransferBuffer(device, GLOB.pipeline_2d_base.index_buffer.transfer)
sdl.UploadToGPUBuffer(
pass,
sdl.GPUTransferBufferLocation{transfer_buffer = GLOB.pipeline_2d_base.index_buffer.transfer},
sdl.GPUBufferRegion{buffer = GLOB.pipeline_2d_base.index_buffer.gpu, offset = 0, size = index_size},
false,
)
}
// Upload SDF primitives
prim_count := u32(len(GLOB.tmp_primitives))
if prim_count > 0 {
prim_size := prim_count * size_of(Primitive)
grow_buffer_if_needed(
device,
&GLOB.pipeline_2d_base.primitive_buffer,
prim_size,
sdl.GPUBufferUsageFlags{.GRAPHICS_STORAGE_READ},
)
prim_array := sdl.MapGPUTransferBuffer(device, GLOB.pipeline_2d_base.primitive_buffer.transfer, false)
if prim_array == nil {
log.panicf("Failed to map primitive transfer buffer: %s", sdl.GetError())
}
mem.copy(prim_array, raw_data(GLOB.tmp_primitives), int(prim_size))
sdl.UnmapGPUTransferBuffer(device, GLOB.pipeline_2d_base.primitive_buffer.transfer)
sdl.UploadToGPUBuffer(
pass,
sdl.GPUTransferBufferLocation{transfer_buffer = GLOB.pipeline_2d_base.primitive_buffer.transfer},
sdl.GPUBufferRegion{buffer = GLOB.pipeline_2d_base.primitive_buffer.gpu, offset = 0, size = prim_size},
false,
)
}
}
@(private)
draw_layer :: proc(
device: ^sdl.GPUDevice,
window: ^sdl.Window,
cmd_buffer: ^sdl.GPUCommandBuffer,
render_texture: ^sdl.GPUTexture,
swapchain_width: u32,
swapchain_height: u32,
clear_color: [4]f32,
layer: ^Layer,
) {
if layer.sub_batch_len == 0 {
if !GLOB.cleared {
pass := sdl.BeginGPURenderPass(
cmd_buffer,
&sdl.GPUColorTargetInfo {
texture = render_texture,
clear_color = sdl.FColor{clear_color[0], clear_color[1], clear_color[2], clear_color[3]},
load_op = .CLEAR,
store_op = .STORE,
},
1,
nil,
)
sdl.EndGPURenderPass(pass)
GLOB.cleared = true
}
return
}
render_pass := sdl.BeginGPURenderPass(
cmd_buffer,
&sdl.GPUColorTargetInfo {
texture = render_texture,
clear_color = sdl.FColor{clear_color[0], clear_color[1], clear_color[2], clear_color[3]},
load_op = GLOB.cleared ? .LOAD : .CLEAR,
store_op = .STORE,
},
1,
nil,
)
GLOB.cleared = true
sdl.BindGPUGraphicsPipeline(render_pass, GLOB.pipeline_2d_base.sdl_pipeline)
// Bind storage buffer (read by vertex shader in SDF mode)
sdl.BindGPUVertexStorageBuffers(
render_pass,
0,
([^]^sdl.GPUBuffer)(&GLOB.pipeline_2d_base.primitive_buffer.gpu),
1,
)
// Always bind index buffer — harmless if no indexed draws are issued
sdl.BindGPUIndexBuffer(
render_pass,
sdl.GPUBufferBinding{buffer = GLOB.pipeline_2d_base.index_buffer.gpu, offset = 0},
._32BIT,
)
// Shorthand aliases for frequently-used pipeline resources
main_vert_buf := GLOB.pipeline_2d_base.vertex_buffer.gpu
unit_quad := GLOB.pipeline_2d_base.unit_quad_buffer
white_texture := GLOB.pipeline_2d_base.white_texture
sampler := GLOB.pipeline_2d_base.sampler
width := f32(swapchain_width)
height := f32(swapchain_height)
// Initial GPU state: tessellated mode, main vertex buffer, no atlas bound yet
push_globals(cmd_buffer, width, height, .Tessellated)
sdl.BindGPUVertexBuffers(render_pass, 0, &sdl.GPUBufferBinding{buffer = main_vert_buf, offset = 0}, 1)
current_mode: Draw_Mode = .Tessellated
current_vert_buf := main_vert_buf
current_atlas: ^sdl.GPUTexture
current_sampler := sampler
// Text vertices live after shape vertices in the GPU vertex buffer
text_vertex_gpu_base := u32(len(GLOB.tmp_shape_verts))
for &scissor in GLOB.scissors[layer.scissor_start:][:layer.scissor_len] {
sdl.SetGPUScissor(render_pass, scissor.bounds)
for &batch in GLOB.tmp_sub_batches[scissor.sub_batch_start:][:scissor.sub_batch_len] {
switch batch.kind {
case .Shapes:
if current_mode != .Tessellated {
push_globals(cmd_buffer, width, height, .Tessellated)
current_mode = .Tessellated
}
if current_vert_buf != main_vert_buf {
sdl.BindGPUVertexBuffers(render_pass, 0, &sdl.GPUBufferBinding{buffer = main_vert_buf, offset = 0}, 1)
current_vert_buf = main_vert_buf
}
// Determine texture and sampler for this batch
batch_texture: ^sdl.GPUTexture = white_texture
batch_sampler: ^sdl.GPUSampler = sampler
if batch.texture_id != INVALID_TEXTURE {
if bound_texture := texture_gpu_handle(batch.texture_id); bound_texture != nil {
batch_texture = bound_texture
}
batch_sampler = get_sampler(batch.sampler)
}
if current_atlas != batch_texture || current_sampler != batch_sampler {
sdl.BindGPUFragmentSamplers(
render_pass,
0,
&sdl.GPUTextureSamplerBinding{texture = batch_texture, sampler = batch_sampler},
1,
)
current_atlas = batch_texture
current_sampler = batch_sampler
}
sdl.DrawGPUPrimitives(render_pass, batch.count, 1, batch.offset, 0)
case .Text:
if current_mode != .Tessellated {
push_globals(cmd_buffer, width, height, .Tessellated)
current_mode = .Tessellated
}
if current_vert_buf != main_vert_buf {
sdl.BindGPUVertexBuffers(render_pass, 0, &sdl.GPUBufferBinding{buffer = main_vert_buf, offset = 0}, 1)
current_vert_buf = main_vert_buf
}
text_batch := &GLOB.tmp_text_batches[batch.offset]
if current_atlas != text_batch.atlas_texture {
sdl.BindGPUFragmentSamplers(
render_pass,
0,
&sdl.GPUTextureSamplerBinding{texture = text_batch.atlas_texture, sampler = sampler},
1,
)
current_atlas = text_batch.atlas_texture
}
sdl.DrawGPUIndexedPrimitives(
render_pass,
text_batch.index_count,
1,
text_batch.index_start,
i32(text_vertex_gpu_base + text_batch.vertex_start),
0,
)
case .SDF:
if current_mode != .SDF {
push_globals(cmd_buffer, width, height, .SDF)
current_mode = .SDF
}
if current_vert_buf != unit_quad {
sdl.BindGPUVertexBuffers(render_pass, 0, &sdl.GPUBufferBinding{buffer = unit_quad, offset = 0}, 1)
current_vert_buf = unit_quad
}
// Determine texture and sampler for this batch
batch_texture: ^sdl.GPUTexture = white_texture
batch_sampler: ^sdl.GPUSampler = sampler
if batch.texture_id != INVALID_TEXTURE {
if bound_texture := texture_gpu_handle(batch.texture_id); bound_texture != nil {
batch_texture = bound_texture
}
batch_sampler = get_sampler(batch.sampler)
}
if current_atlas != batch_texture || current_sampler != batch_sampler {
sdl.BindGPUFragmentSamplers(
render_pass,
0,
&sdl.GPUTextureSamplerBinding{texture = batch_texture, sampler = batch_sampler},
1,
)
current_atlas = batch_texture
current_sampler = batch_sampler
}
sdl.DrawGPUPrimitives(render_pass, 6, batch.count, 0, batch.offset)
}
}
}
sdl.EndGPURenderPass(render_pass)
}
destroy_pipeline_2d_base :: proc(device: ^sdl.GPUDevice, pipeline: ^Pipeline_2D_Base) {
destroy_buffer(device, &pipeline.vertex_buffer)
destroy_buffer(device, &pipeline.index_buffer)
destroy_buffer(device, &pipeline.primitive_buffer)
if pipeline.unit_quad_buffer != nil {
sdl.ReleaseGPUBuffer(device, pipeline.unit_quad_buffer)
}
sdl.ReleaseGPUTexture(device, pipeline.white_texture)
sdl.ReleaseGPUSampler(device, pipeline.sampler)
sdl.ReleaseGPUGraphicsPipeline(device, pipeline.sdl_pipeline)
}
@@ -1,118 +0,0 @@
#pragma clang diagnostic ignored "-Wmissing-prototypes"
#include <metal_stdlib>
#include <simd/simd.h>
using namespace metal;
struct Uniforms
{
float2 inv_working_size;
uint pair_count;
uint mode;
float2 direction;
float inv_downsample_factor;
float _pad0;
float4 kernel0[32];
};
struct main0_out
{
float4 out_color [[color(0)]];
};
struct main0_in
{
float2 p_local [[user(locn0)]];
float4 f_color [[user(locn1)]];
float2 f_half_size [[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.
@@ -1,123 +0,0 @@
#pragma clang diagnostic ignored "-Wmissing-prototypes"
#pragma clang diagnostic ignored "-Wmissing-braces"
#include <metal_stdlib>
#include <simd/simd.h>
using namespace metal;
template<typename T, size_t Num>
struct spvUnsafeArray
{
T elements[Num ? Num : 1];
thread T& operator [] (size_t pos) thread
{
return elements[pos];
}
constexpr const thread T& operator [] (size_t pos) const thread
{
return elements[pos];
}
device T& operator [] (size_t pos) device
{
return elements[pos];
}
constexpr const device T& operator [] (size_t pos) const device
{
return elements[pos];
}
constexpr const constant T& operator [] (size_t pos) const constant
{
return elements[pos];
}
threadgroup T& operator [] (size_t pos) threadgroup
{
return elements[pos];
}
constexpr const threadgroup T& operator [] (size_t pos) const threadgroup
{
return elements[pos];
}
};
struct Uniforms
{
float4x4 projection;
float dpi_scale;
uint mode;
float2 _pad0;
};
struct Gaussian_Blur_Primitive
{
float4 bounds;
float4 radii;
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.
@@ -1,47 +0,0 @@
#include <metal_stdlib>
#include <simd/simd.h>
using namespace metal;
struct Uniforms
{
float2 inv_source_size;
uint downsample_factor;
uint _pad0;
};
struct main0_out
{
float4 out_color [[color(0)]];
};
fragment main0_out main0(constant Uniforms& _18 [[buffer(0)]], texture2d<float> source_tex [[texture(0)]], sampler source_texSmplr [[sampler(0)]], float4 gl_FragCoord [[position]])
{
main0_out out = {};
float2 src_block_center = gl_FragCoord.xy * float(_18.downsample_factor);
if (_18.downsample_factor == 1u)
{
float2 uv = src_block_center * _18.inv_source_size;
out.out_color = source_tex.sample(source_texSmplr, uv);
}
else
{
if (_18.downsample_factor == 2u)
{
float2 uv_1 = src_block_center * _18.inv_source_size;
out.out_color = source_tex.sample(source_texSmplr, uv_1);
}
else
{
float off = float(_18.downsample_factor) * 0.25;
float2 uv_tl = (src_block_center + float2(-off, -off)) * _18.inv_source_size;
float2 uv_tr = (src_block_center + float2(off, -off)) * _18.inv_source_size;
float2 uv_bl = (src_block_center + float2(-off, off)) * _18.inv_source_size;
float2 uv_br = (src_block_center + float2(off)) * _18.inv_source_size;
float4 c = ((source_tex.sample(source_texSmplr, uv_tl) + source_tex.sample(source_texSmplr, uv_tr)) + source_tex.sample(source_texSmplr, uv_bl)) + source_tex.sample(source_texSmplr, uv_br);
out.out_color = c * 0.25;
}
}
return out;
}
Binary file not shown.
@@ -1,18 +0,0 @@
#include <metal_stdlib>
#include <simd/simd.h>
using namespace metal;
struct main0_out
{
float4 gl_Position [[position]];
};
vertex main0_out main0(uint gl_VertexIndex [[vertex_id]])
{
main0_out out = {};
float2 ndc = float2((int(gl_VertexIndex) == 1) ? 3.0 : (-1.0), (int(gl_VertexIndex) == 2) ? 3.0 : (-1.0));
out.gl_Position = float4(ndc, 0.0, 1.0);
return out;
}
Binary file not shown.
+216 -143
View File
@@ -23,220 +23,293 @@ struct main0_in
float2 f_local_or_uv [[user(locn1)]]; float2 f_local_or_uv [[user(locn1)]];
float4 f_params [[user(locn2)]]; float4 f_params [[user(locn2)]];
float4 f_params2 [[user(locn3)]]; float4 f_params2 [[user(locn3)]];
uint f_flags [[user(locn4)]]; uint f_kind_flags [[user(locn4)]];
float f_rotation [[user(locn5), flat]];
float4 f_uv_rect [[user(locn6), flat]]; float4 f_uv_rect [[user(locn6), flat]];
uint4 f_effects [[user(locn7)]];
}; };
static inline __attribute__((always_inline)) static inline __attribute__((always_inline))
float sdRoundedBox(thread const float2& p, thread const float2& b, thread const float4& r) float2 apply_rotation(thread const float2& p, thread const float& angle)
{ {
float2 _48; float cr = cos(-angle);
float sr = sin(-angle);
return float2x2(float2(cr, sr), float2(-sr, cr)) * p;
}
static inline __attribute__((always_inline))
float sdRoundedBox(thread const float2& p, thread const float2& b, thread float4& r)
{
float2 _61;
if (p.x > 0.0) if (p.x > 0.0)
{ {
_48 = r.xy; _61 = r.xy;
} }
else else
{ {
_48 = r.zw; _61 = r.zw;
} }
float2 rxy = _48; r.x = _61.x;
float _62; r.y = _61.y;
float _78;
if (p.y > 0.0) if (p.y > 0.0)
{ {
_62 = rxy.x; _78 = r.x;
} }
else else
{ {
_62 = rxy.y; _78 = r.y;
} }
float rr = _62; r.x = _78;
float2 q = abs(p) - b; float2 q = (abs(p) - b) + float2(r.x);
if (rr == 0.0) return (fast::min(fast::max(q.x, q.y), 0.0) + length(fast::max(q, float2(0.0)))) - r.x;
}
static inline __attribute__((always_inline))
float sdf_stroke(thread const float& d, thread const float& stroke_width)
{
return abs(d) - (stroke_width * 0.5);
}
static inline __attribute__((always_inline))
float sdf_alpha(thread const float& d, thread const float& soft)
{
return 1.0 - smoothstep(-soft, soft, d);
}
static inline __attribute__((always_inline))
float sdCircle(thread const float2& p, thread const float& r)
{
return length(p) - r;
}
static inline __attribute__((always_inline))
float sdEllipse(thread float2& p, thread float2& ab)
{
p = abs(p);
if (p.x > p.y)
{ {
return fast::max(q.x, q.y); p = p.yx;
ab = ab.yx;
} }
q += float2(rr); float l = (ab.y * ab.y) - (ab.x * ab.x);
return (fast::min(fast::max(q.x, q.y), 0.0) + length(fast::max(q, float2(0.0)))) - rr; float m = (ab.x * p.x) / l;
float m2 = m * m;
float n = (ab.y * p.y) / l;
float n2 = n * n;
float c = ((m2 + n2) - 1.0) / 3.0;
float c3 = (c * c) * c;
float q = c3 + ((m2 * n2) * 2.0);
float d = c3 + (m2 * n2);
float g = m + (m * n2);
float co;
if (d < 0.0)
{
float h = acos(q / c3) / 3.0;
float s = cos(h);
float t = sin(h) * 1.73205077648162841796875;
float rx = sqrt(((-c) * ((s + t) + 2.0)) + m2);
float ry = sqrt(((-c) * ((s - t) + 2.0)) + m2);
co = (((ry + (sign(l) * rx)) + (abs(g) / (rx * ry))) - m) / 2.0;
}
else
{
float h_1 = ((2.0 * m) * n) * sqrt(d);
float s_1 = sign(q + h_1) * powr(abs(q + h_1), 0.3333333432674407958984375);
float u = sign(q - h_1) * powr(abs(q - h_1), 0.3333333432674407958984375);
float rx_1 = (((-s_1) - u) - (c * 4.0)) + (2.0 * m2);
float ry_1 = (s_1 - u) * 1.73205077648162841796875;
float rm = sqrt((rx_1 * rx_1) + (ry_1 * ry_1));
co = (((ry_1 / sqrt(rm - rx_1)) + ((2.0 * g) / rm)) - m) / 2.0;
}
float2 r = ab * float2(co, sqrt(1.0 - (co * co)));
return length(r - p) * sign(p.y - r.y);
} }
static inline __attribute__((always_inline)) static inline __attribute__((always_inline))
float sdRegularPolygon(thread const float2& p, thread const float& r, thread const float& n) float sdSegment(thread const float2& p, thread const float2& a, thread const float2& b)
{ {
float an = 3.1415927410125732421875 / n; float2 pa = p - a;
float bn = mod(precise::atan2(p.y, p.x), 2.0 * an) - an; float2 ba = b - a;
return (length(p) * cos(bn)) - r; float h = fast::clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
} return length(pa - (ba * h));
static inline __attribute__((always_inline))
float sdEllipseApprox(thread const float2& p, thread const float2& ab)
{
float 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)]]) fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[texture(0)]], sampler texSmplr [[sampler(0)]])
{ {
main0_out out = {}; main0_out out = {};
uint kind = in.f_flags & 255u; uint kind = in.f_kind_flags & 255u;
uint flags = (in.f_flags >> 8u) & 255u; uint flags = (in.f_kind_flags >> 8u) & 255u;
if (kind == 0u) if (kind == 0u)
{ {
float4 t = tex.sample(texSmplr, in.f_local_or_uv); out.out_color = in.f_color * 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; return out;
} }
float d = 1000000015047466219876688855040.0; float d = 1000000015047466219876688855040.0;
float h = 0.5; float soft = 1.0;
float2 half_size = in.f_params.xy;
float2 p_local = in.f_local_or_uv;
if (kind == 1u) if (kind == 1u)
{ {
float4 corner_radii = float4(in.f_params.zw, in.f_params2.xy); float2 b = in.f_params.xy;
h = in.f_params2.z; float4 r = float4(in.f_params.zw, in.f_params2.xy);
soft = fast::max(in.f_params2.z, 1.0);
float stroke_px = in.f_params2.w;
float2 p_local = in.f_local_or_uv;
if (in.f_rotation != 0.0)
{
float2 param = p_local; float2 param = p_local;
float2 param_1 = half_size; float param_1 = in.f_rotation;
float4 param_2 = corner_radii; p_local = apply_rotation(param, param_1);
d = sdRoundedBox(param, param_1, param_2); }
float2 param_2 = p_local;
float2 param_3 = b;
float4 param_4 = r;
float _491 = sdRoundedBox(param_2, param_3, param_4);
d = _491;
if ((flags & 1u) != 0u)
{
float param_5 = d;
float param_6 = stroke_px;
d = sdf_stroke(param_5, param_6);
}
float4 shape_color = in.f_color;
if ((flags & 2u) != 0u)
{
float2 p_for_uv = in.f_local_or_uv;
if (in.f_rotation != 0.0)
{
float2 param_7 = p_for_uv;
float param_8 = in.f_rotation;
p_for_uv = apply_rotation(param_7, param_8);
}
float2 local_uv = ((p_for_uv / b) * 0.5) + float2(0.5);
float2 uv = mix(in.f_uv_rect.xy, in.f_uv_rect.zw, local_uv);
shape_color *= tex.sample(texSmplr, uv);
}
float param_9 = d;
float param_10 = soft;
float alpha = sdf_alpha(param_9, param_10);
out.out_color = float4(shape_color.xyz, shape_color.w * alpha);
return out;
} }
else else
{ {
if (kind == 2u) if (kind == 2u)
{ {
float radius = in.f_params.x; float radius = in.f_params.x;
float sides = in.f_params.y; soft = fast::max(in.f_params.y, 1.0);
h = in.f_params.z; float stroke_px_1 = in.f_params.z;
float2 param_3 = p_local; float2 param_11 = in.f_local_or_uv;
float param_4 = radius; float param_12 = radius;
float param_5 = sides; d = sdCircle(param_11, param_12);
d = sdRegularPolygon(param_3, param_4, param_5); if ((flags & 1u) != 0u)
half_size = float2(radius); {
float param_13 = d;
float param_14 = stroke_px_1;
d = sdf_stroke(param_13, param_14);
}
} }
else else
{ {
if (kind == 3u) if (kind == 3u)
{ {
float2 ab = in.f_params.xy; float2 ab = in.f_params.xy;
h = in.f_params.z; soft = fast::max(in.f_params.z, 1.0);
float2 param_6 = p_local; float stroke_px_2 = in.f_params.w;
float2 param_7 = ab; float2 p_local_1 = in.f_local_or_uv;
d = sdEllipseApprox(param_6, param_7); if (in.f_rotation != 0.0)
half_size = ab; {
float2 param_15 = p_local_1;
float param_16 = in.f_rotation;
p_local_1 = apply_rotation(param_15, param_16);
}
float2 param_17 = p_local_1;
float2 param_18 = ab;
float _616 = sdEllipse(param_17, param_18);
d = _616;
if ((flags & 1u) != 0u)
{
float param_19 = d;
float param_20 = stroke_px_2;
d = sdf_stroke(param_19, param_20);
}
} }
else else
{ {
if (kind == 4u) if (kind == 4u)
{
float2 a = in.f_params.xy;
float2 b_1 = in.f_params.zw;
float width = in.f_params2.x;
soft = fast::max(in.f_params2.y, 1.0);
float2 param_21 = in.f_local_or_uv;
float2 param_22 = a;
float2 param_23 = b_1;
d = sdSegment(param_21, param_22, param_23) - (width * 0.5);
}
else
{
if (kind == 5u)
{ {
float inner = in.f_params.x; float inner = in.f_params.x;
float outer = in.f_params.y; float outer = in.f_params.y;
float2 n_start = in.f_params.zw; float start_rad = in.f_params.z;
float2 n_end = in.f_params2.xy; float end_rad = in.f_params.w;
uint arc_bits = (flags >> 5u) & 3u; soft = fast::max(in.f_params2.x, 1.0);
h = in.f_params2.z; float r_1 = length(in.f_local_or_uv);
float r = length(p_local); float d_ring = fast::max(inner - r_1, r_1 - outer);
d = fast::max(inner - r, r - outer); float angle = precise::atan2(in.f_local_or_uv.y, in.f_local_or_uv.x);
if (arc_bits != 0u) if (angle < 0.0)
{ {
float d_start = dot(p_local, n_start); angle += 6.283185482025146484375;
float d_end = dot(p_local, n_end); }
float _338; float ang_start = mod(start_rad, 6.283185482025146484375);
if (arc_bits == 1u) float ang_end = mod(end_rad, 6.283185482025146484375);
float _710;
if (ang_end > ang_start)
{ {
_338 = fast::max(d_start, d_end); _710 = float((angle >= ang_start) && (angle <= ang_end));
} }
else else
{ {
_338 = fast::min(d_start, d_end); _710 = float((angle >= ang_start) || (angle <= ang_end));
} }
float d_wedge = _338; float in_arc = _710;
d = fast::max(d, d_wedge); if (abs(ang_end - ang_start) >= 6.282185077667236328125)
}
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; in_arc = 1.0;
float4 gradient_end = unpack_unorm4x8_to_float(in.f_effects.x); }
if ((flags & 4u) != 0u) d = (in_arc > 0.5) ? d_ring : 1000000015047466219876688855040.0;
{
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 else
{ {
float2 direction = float2(as_type<half2>(in.f_effects.z)); if (kind == 6u)
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
{ {
float radius_1 = in.f_params.x;
float rotation = in.f_params.y;
float sides = in.f_params.z;
soft = fast::max(in.f_params.w, 1.0);
float stroke_px_3 = in.f_params2.x;
float2 p = in.f_local_or_uv;
float c = cos(rotation);
float s = sin(rotation);
p = float2x2(float2(c, -s), float2(s, c)) * p;
float an = 3.1415927410125732421875 / sides;
float bn = mod(precise::atan2(p.y, p.x), 2.0 * an) - an;
d = (length(p) * cos(bn)) - radius_1;
if ((flags & 1u) != 0u) if ((flags & 1u) != 0u)
{ {
float4 uv_rect = in.f_uv_rect; float param_24 = d;
float2 local_uv = ((p_local / half_size) * 0.5) + float2(0.5); float param_25 = stroke_px_3;
float2 uv = mix(uv_rect.xy, uv_rect.zw, local_uv); d = sdf_stroke(param_24, param_25);
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);
} }
}
}
}
float param_26 = d;
float param_27 = soft;
float alpha_1 = sdf_alpha(param_26, param_27);
out.out_color = float4(in.f_color.xyz, in.f_color.w * alpha_1);
return out; return out;
} }
Binary file not shown.
+26 -37
View File
@@ -10,35 +10,33 @@ struct Uniforms
uint mode; uint mode;
}; };
struct Core_2D_Primitive struct Primitive
{ {
float4 bounds; float4 bounds;
uint color; uint color;
uint flags; uint kind_flags;
uint rotation_sc; float rotation;
float _pad; float _pad;
float4 params; float4 params;
float4 params2; float4 params2;
float4 uv_rect; float4 uv_rect;
uint4 effects;
}; };
struct Core_2D_Primitive_1 struct Primitive_1
{ {
float4 bounds; float4 bounds;
uint color; uint color;
uint flags; uint kind_flags;
uint rotation_sc; float rotation;
float _pad; float _pad;
float4 params; float4 params;
float4 params2; float4 params2;
float4 uv_rect; float4 uv_rect;
uint4 effects;
}; };
struct Core_2D_Primitives struct Primitives
{ {
Core_2D_Primitive_1 primitives[1]; Primitive_1 primitives[1];
}; };
struct main0_out struct main0_out
@@ -47,9 +45,9 @@ struct main0_out
float2 f_local_or_uv [[user(locn1)]]; float2 f_local_or_uv [[user(locn1)]];
float4 f_params [[user(locn2)]]; float4 f_params [[user(locn2)]];
float4 f_params2 [[user(locn3)]]; float4 f_params2 [[user(locn3)]];
uint f_flags [[user(locn4)]]; uint f_kind_flags [[user(locn4)]];
float f_rotation [[user(locn5)]];
float4 f_uv_rect [[user(locn6)]]; float4 f_uv_rect [[user(locn6)]];
uint4 f_effects [[user(locn7)]];
float4 gl_Position [[position]]; float4 gl_Position [[position]];
}; };
@@ -60,7 +58,7 @@ struct main0_in
float4 v_color [[attribute(2)]]; float4 v_color [[attribute(2)]];
}; };
vertex main0_out main0(main0_in in [[stage_in]], constant Uniforms& _12 [[buffer(0)]], const device Core_2D_Primitives& _75 [[buffer(1)]], uint gl_InstanceIndex [[instance_id]]) vertex main0_out main0(main0_in in [[stage_in]], constant Uniforms& _12 [[buffer(0)]], const device Primitives& _74 [[buffer(1)]], uint gl_InstanceIndex [[instance_id]])
{ {
main0_out out = {}; main0_out out = {};
if (_12.mode == 0u) if (_12.mode == 0u)
@@ -69,42 +67,33 @@ vertex main0_out main0(main0_in in [[stage_in]], constant Uniforms& _12 [[buffer
out.f_local_or_uv = in.v_uv; out.f_local_or_uv = in.v_uv;
out.f_params = float4(0.0); out.f_params = float4(0.0);
out.f_params2 = float4(0.0); out.f_params2 = float4(0.0);
out.f_flags = 0u; out.f_kind_flags = 0u;
out.f_uv_rect = float4(0.0); out.f_rotation = 0.0;
out.f_effects = uint4(0u); out.f_uv_rect = float4(0.0, 0.0, 1.0, 1.0);
out.gl_Position = _12.projection * float4(in.v_position * _12.dpi_scale, 0.0, 1.0); out.gl_Position = _12.projection * float4(in.v_position * _12.dpi_scale, 0.0, 1.0);
} }
else else
{ {
Core_2D_Primitive p; Primitive p;
p.bounds = _75.primitives[int(gl_InstanceIndex)].bounds; p.bounds = _74.primitives[int(gl_InstanceIndex)].bounds;
p.color = _75.primitives[int(gl_InstanceIndex)].color; p.color = _74.primitives[int(gl_InstanceIndex)].color;
p.flags = _75.primitives[int(gl_InstanceIndex)].flags; p.kind_flags = _74.primitives[int(gl_InstanceIndex)].kind_flags;
p.rotation_sc = _75.primitives[int(gl_InstanceIndex)].rotation_sc; p.rotation = _74.primitives[int(gl_InstanceIndex)].rotation;
p._pad = _75.primitives[int(gl_InstanceIndex)]._pad; p._pad = _74.primitives[int(gl_InstanceIndex)]._pad;
p.params = _75.primitives[int(gl_InstanceIndex)].params; p.params = _74.primitives[int(gl_InstanceIndex)].params;
p.params2 = _75.primitives[int(gl_InstanceIndex)].params2; p.params2 = _74.primitives[int(gl_InstanceIndex)].params2;
p.uv_rect = _75.primitives[int(gl_InstanceIndex)].uv_rect; p.uv_rect = _74.primitives[int(gl_InstanceIndex)].uv_rect;
p.effects = _75.primitives[int(gl_InstanceIndex)].effects;
float2 corner = in.v_position; float2 corner = in.v_position;
float2 world_pos = mix(p.bounds.xy, p.bounds.zw, corner); float2 world_pos = mix(p.bounds.xy, p.bounds.zw, corner);
float2 center = (p.bounds.xy + p.bounds.zw) * 0.5; 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_color = unpack_unorm4x8_to_float(p.color);
out.f_local_or_uv = local; out.f_local_or_uv = (world_pos - center) * _12.dpi_scale;
out.f_params = p.params; out.f_params = p.params;
out.f_params2 = p.params2; out.f_params2 = p.params2;
out.f_flags = p.flags; out.f_kind_flags = p.kind_flags;
out.f_rotation = p.rotation;
out.f_uv_rect = p.uv_rect; 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); out.gl_Position = _12.projection * float4(world_pos * _12.dpi_scale, 0.0, 1.0);
} }
return out; return out;
} }
Binary file not shown.
-155
View File
@@ -1,155 +0,0 @@
#version 450 core
// Unified backdrop blur fragment shader.
// Handles both the 1D separable blur passes (mode 0, used for BOTH the H-pass and V-pass;
// `direction` picks the axis) and the composite pass (mode 1, reads the fully-blurred
// working texture, masks via RRect SDF, applies tint, and writes to source_texture with
// premultiplied-over blending). Working textures are sized at the full swapchain resolution;
// downsampled content occupies only a sub-rect at downsample factor > 1 (set via viewport).
//
// The composite blends with source_texture via the standard premultiplied-over blend state
// (ONE, ONE_MINUS_SRC_ALPHA).
//
// Backdrop primitives are tint-only — there is no outline. A specialized edge effect
// (e.g. liquid-glass-style refraction outlines) would be implemented as a dedicated
// primitive type with its own pipeline.
//
// Two modes, structurally distinct:
//
// Mode 0: 1D separable blur. Used for BOTH the H-pass and V-pass; `direction` (set in the
// per-pass uniforms) picks (1,0) for H or (0,1) for V. Reads the previous working-
// res texture and writes the next working-res texture. Fullscreen-triangle vertex
// output; gl_FragCoord.xy is in working-res target pixel space; UV =
// gl_FragCoord.xy * inv_working_size.
//
// Mode 1: composite. Reads the fully-blurred working-res texture, applies the SDF mask and
// tint, writes to source_texture. Instanced unit-quad vertex output covering the
// per-primitive bounds; gl_FragCoord.xy is in the full-resolution render target;
// UV into the blurred working texture =
// (gl_FragCoord.xy * inv_downsample_factor) * inv_working_size.
// No kernel is applied here — the blur is already complete.
//
// V-blur is run as its own working→working pass rather than folded into the composite. The
// folded variant produced a horizontal-vs-vertical asymmetry artifact: when V-blur sampled
// the H-blur output through the bilinear-upsample/SDF-mask/tint pipeline in one shader
// invocation, horizontal source features ended up looking sharper than vertical ones.
// Matching V's structure exactly to H's restores symmetry.
const uint MAX_KERNEL_PAIRS = 32;
// --- Inputs from vertex shader ---
layout(location = 0) in vec2 p_local;
layout(location = 1) in mediump vec4 f_color;
layout(location = 2) flat in vec2 f_half_size;
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);
}
-110
View File
@@ -1,110 +0,0 @@
#version 450 core
// Unified backdrop blur vertex shader.
// Handles both the 1D separable blur passes (fullscreen triangle, mode 0; used for
// BOTH the H-pass and V-pass) and the composite pass (instanced unit-quad over
// Gaussian_Blur_Primitive storage buffer, mode 1) for the second PSO of the backdrop bracket.
// The first PSO (downsample) uses backdrop_fullscreen.vert.
//
// No vertex buffer for either mode. Mode 0 uses gl_VertexIndex 0..2 for a single
// fullscreen triangle; mode 1 uses gl_VertexIndex 0..5 for a unit-quad (two
// triangles, TRIANGLELIST topology) and gl_InstanceIndex to select the primitive.
//
// Mode 0 viewport+scissor are CPU-set per sigma group to the work region (union AABB
// of that group's backdrop primitives + halo, clamped to swapchain bounds). Mode 1
// renders into source_texture with the screen-space orthographic projection; the
// per-primitive bounds drive the quad in screen space.
//
// Backdrop primitives have NO rotation — backdrop sampling is in screen space, so
// a rotated mask over a stationary blur sample would look wrong.
// --- Outputs to fragment shader ---
// p_local: shape-local position in physical pixels (origin at shape center).
// Only meaningful in mode 1 (V-composite). Zero-init for mode 0.
layout(location = 0) out vec2 p_local;
// f_color: tint, unpacked from primitive.color. Only meaningful in mode 1.
layout(location = 1) out mediump vec4 f_color;
// f_half_size: 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);
}
}
@@ -1,67 +0,0 @@
#version 450 core
// Backdrop downsample fragment shader.
// Reads source_texture (full-resolution snapshot of pre-bracket framebuffer contents) and
// writes a downsampled copy at factor 1, 2, or 4. The output is the working texture (sized
// at full swapchain resolution); larger factors only fill a sub-rect of it via the CPU-set
// viewport. See backdrop.odin for the factor selection table (Flutter-style).
//
// Shader paths by factor:
//
// factor=1: identity copy. One bilinear tap aligned to the source pixel center. Useful
// when sigma is small enough that any downsample round-trip would visibly soften
// the output (Flutter does this for sigma_phys ≤ 4).
//
// factor=2: each output covers a 2×2 source block. Single bilinear tap at the shared
// corner reads all 4 source pixels with 0.25 weight.
//
// factor=4: each output covers a 4×4 source block. We use 4 bilinear taps, each at the
// shared corner of a 2×2 sub-block. Each tap reads 4 source pixels uniformly;
// combined, the 4 taps sample 16 source pixels arranged uniformly across the
// block (full coverage at factor=4). The factor>=4 path is structured so the
// same shader code would extend to factor=8 (16 pixels of 64) or factor=16 (16
// of 256) if the CPU-side cap is ever raised, though the current cap is 4.
//
// The viewport+scissor are set by the CPU to limit output to the layer's work region in
// working-texture coords (work_region_phys / factor), clamped to the texture bounds.
layout(set = 3, binding = 0) uniform Uniforms {
vec2 inv_source_size; // 1.0 / source_texture pixel dimensions
uint downsample_factor; // 1, 2, 4, 8, or 16
uint _pad0;
};
layout(set = 2, binding = 0) uniform sampler2D source_tex;
layout(location = 0) out vec4 out_color;
void main() {
// Output pixel index (i): gl_FragCoord.xy - 0.5. Source-pixel block top-left for this
// output: i * factor. Center of the block: i*factor + factor/2 = gl_FragCoord.xy * factor.
vec2 src_block_center = gl_FragCoord.xy * float(downsample_factor);
if (downsample_factor == 1u) {
// Identity copy. UV at src_block_center hits the source pixel center directly.
vec2 uv = src_block_center * inv_source_size;
out_color = texture(source_tex, uv);
} else if (downsample_factor == 2u) {
// Single tap at the shared corner of the 2×2 source block; one bilinear sample reads
// all 4 source pixels with equal 0.25 weights — uniform 2×2 box filter for free.
vec2 uv = src_block_center * inv_source_size;
out_color = texture(source_tex, uv);
} else {
// Four taps at offsets ±(factor/4) from the block center. Each tap lands on a corner
// shared by 4 source pixels of a (factor/2)×(factor/2) sub-block (equivalent at the
// bilinear level), giving a 4-tap = 16-source-pixel uniform sample of the block.
float off = float(downsample_factor) * 0.25;
vec2 uv_tl = (src_block_center + vec2(-off, -off)) * inv_source_size;
vec2 uv_tr = (src_block_center + vec2(off, -off)) * inv_source_size;
vec2 uv_bl = (src_block_center + vec2(-off, off)) * inv_source_size;
vec2 uv_br = (src_block_center + vec2(off, off)) * inv_source_size;
vec4 c = texture(source_tex, uv_tl)
+ texture(source_tex, uv_tr)
+ texture(source_tex, uv_bl)
+ texture(source_tex, uv_br);
out_color = c * 0.25;
}
}
@@ -1,21 +0,0 @@
#version 450 core
// Fullscreen-triangle vertex shader for the backdrop downsample and H-blur sub-passes.
// Emits a single triangle covering NDC [-1,1]^2; the rasterizer clips edges outside.
// No vertex buffer; uses gl_VertexIndex to pick corners.
//
// The CPU sets the viewport (and matching scissor) per layer-bracket to limit work to
// the union AABB of the layer's backdrop primitives, expanded by 3*max_sigma and
// clamped to swapchain bounds. The fragment shader uses gl_FragCoord (absolute pixel
// space in the bound target) plus an inv-size uniform to compute its own UVs — see
// each fragment shader for the per-pass sampling math.
void main() {
// gl_VertexIndex 0 -> ( -1, -1)
// gl_VertexIndex 1 -> ( 3, -1)
// gl_VertexIndex 2 -> ( -1, 3)
vec2 ndc = vec2(
(gl_VertexIndex == 1) ? 3.0 : -1.0,
(gl_VertexIndex == 2) ? 3.0 : -1.0);
gl_Position = vec4(ndc, 0.0, 1.0);
}
+170 -133
View File
@@ -1,13 +1,13 @@
#version 450 core #version 450 core
// --- Inputs from vertex shader --- // --- Inputs from vertex shader ---
layout(location = 0) in mediump vec4 f_color; layout(location = 0) in vec4 f_color;
layout(location = 1) in vec2 f_local_or_uv; layout(location = 1) in vec2 f_local_or_uv;
layout(location = 2) in vec4 f_params; layout(location = 2) in vec4 f_params;
layout(location = 3) in vec4 f_params2; layout(location = 3) in vec4 f_params2;
layout(location = 4) flat in uint f_flags; layout(location = 4) flat in uint f_kind_flags;
layout(location = 5) flat in float f_rotation;
layout(location = 6) flat in vec4 f_uv_rect; layout(location = 6) flat in vec4 f_uv_rect;
layout(location = 7) flat in uvec4 f_effects;
// --- Output --- // --- Output ---
layout(location = 0) out vec4 out_color; layout(location = 0) out vec4 out_color;
@@ -20,43 +20,77 @@ layout(set = 2, binding = 0) uniform sampler2D tex;
// All operate in physical pixel space — no dpi_scale needed here. // All operate in physical pixel space — no dpi_scale needed here.
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const float PI = 3.14159265358979;
float sdCircle(vec2 p, float r) {
return length(p) - r;
}
float sdRoundedBox(vec2 p, vec2 b, vec4 r) { float sdRoundedBox(vec2 p, vec2 b, vec4 r) {
vec2 rxy = (p.x > 0.0) ? r.xy : r.zw; r.xy = (p.x > 0.0) ? r.xy : r.zw;
float rr = (p.y > 0.0) ? rxy.x : rxy.y; r.x = (p.y > 0.0) ? r.x : r.y;
vec2 q = abs(p) - b; vec2 q = abs(p) - b + r.x;
if (rr == 0.0) { return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - r.x;
return max(q.x, q.y); }
float sdSegment(vec2 p, vec2 a, vec2 b) {
vec2 pa = p - a, ba = b - a;
float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
return length(pa - ba * h);
}
float sdEllipse(vec2 p, vec2 ab) {
p = abs(p);
if (p.x > p.y) {
p = p.yx;
ab = ab.yx;
} }
q += rr; float l = ab.y * ab.y - ab.x * ab.x;
return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - rr; float m = ab.x * p.x / l;
float m2 = m * m;
float n = ab.y * p.y / l;
float n2 = n * n;
float c = (m2 + n2 - 1.0) / 3.0;
float c3 = c * c * c;
float q = c3 + m2 * n2 * 2.0;
float d = c3 + m2 * n2;
float g = m + m * n2;
float co;
if (d < 0.0) {
float h = acos(q / c3) / 3.0;
float s = cos(h);
float t = sin(h) * sqrt(3.0);
float rx = sqrt(-c * (s + t + 2.0) + m2);
float ry = sqrt(-c * (s - t + 2.0) + m2);
co = (ry + sign(l) * rx + abs(g) / (rx * ry) - m) / 2.0;
} else {
float h = 2.0 * m * n * sqrt(d);
float s = sign(q + h) * pow(abs(q + h), 1.0 / 3.0);
float u = sign(q - h) * pow(abs(q - h), 1.0 / 3.0);
float rx = -s - u - c * 4.0 + 2.0 * m2;
float ry = (s - u) * sqrt(3.0);
float rm = sqrt(rx * rx + ry * ry);
co = (ry / sqrt(rm - rx) + 2.0 * g / rm - m) / 2.0;
}
vec2 r = ab * vec2(co, sqrt(1.0 - co * co));
return length(r - p) * sign(p.y - r.y);
} }
// Approximate ellipse SDF — fast, suitable for UI, NOT a true Euclidean distance. float sdf_alpha(float d, float soft) {
float sdEllipseApprox(vec2 p, vec2 ab) { return 1.0 - smoothstep(-soft, soft, d);
float k0 = length(p / ab);
float k1 = length(p / (ab * ab));
return k0 * (k0 - 1.0) / k1;
} }
// Regular N-gon SDF (Inigo Quilez). float sdf_stroke(float d, float stroke_width) {
float sdRegularPolygon(vec2 p, float r, float n) { return abs(d) - stroke_width * 0.5;
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). // Rotate a 2D point by the negative of the given angle (inverse rotation).
// Produces a symmetric transition centered on d=0: smoothstep(-h, h, d). // Used to rotate the sampling frame opposite to the shape's rotation so that
float sdf_alpha(float d, float h) { // the SDF evaluates correctly for the rotated shape.
return 1.0 - smoothstep(-h, h, d); vec2 apply_rotation(vec2 p, float angle) {
} float cr = cos(-angle);
float sr = sin(-angle);
// --------------------------------------------------------------------------- return mat2(cr, sr, -sr, cr) * p;
// 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));
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -64,128 +98,131 @@ mediump vec4 gradient_2color(mediump vec4 start_color, mediump vec4 end_color, m
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
void main() { void main() {
uint kind = f_flags & 0xFFu; uint kind = f_kind_flags & 0xFFu;
uint flags = (f_flags >> 8u) & 0xFFu; uint flags = (f_kind_flags >> 8u) & 0xFFu;
// Kind 0: Tessellated path — vertex colors arrive premultiplied from CPU. // -----------------------------------------------------------------------
// Texture samples are straight-alpha (SDL_ttf glyph atlas: rgb=1, a=coverage; // Kind 0: Tessellated path. Texture multiply for text atlas,
// or the 1x1 white texture: rgba=1). Convert to premultiplied form so the // white pixel for solid shapes.
// blend state (ONE, ONE_MINUS_SRC_ALPHA) composites correctly. // -----------------------------------------------------------------------
if (kind == 0u) { if (kind == 0u) {
vec4 t = texture(tex, f_local_or_uv); out_color = f_color * texture(tex, f_local_or_uv);
t.rgb *= t.a;
out_color = f_color * t;
return; return;
} }
// SDF path — dispatch on kind // -----------------------------------------------------------------------
// SDF path. f_local_or_uv = shape-centered position in physical pixels.
// All dimensional params are already in physical pixels (CPU pre-scaled).
// -----------------------------------------------------------------------
float d = 1e30; float d = 1e30;
float h = 0.5; // half-feather width; overwritten per shape kind float soft = 1.0;
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) { if (kind == 1u) {
// RRect — half_feather in params2.z // RRect: rounded box
vec4 corner_radii = vec4(f_params.zw, f_params2.xy); vec2 b = f_params.xy; // half_size (phys px)
h = f_params2.z; vec4 r = vec4(f_params.zw, f_params2.xy); // corner radii: tr, br, tl, bl
d = sdRoundedBox(p_local, half_size, corner_radii); soft = max(f_params2.z, 1.0);
float stroke_px = f_params2.w;
vec2 p_local = f_local_or_uv;
if (f_rotation != 0.0) {
p_local = apply_rotation(p_local, f_rotation);
}
d = sdRoundedBox(p_local, b, r);
if ((flags & 1u) != 0u) d = sdf_stroke(d, stroke_px);
// Texture sampling for textured SDF primitives
vec4 shape_color = f_color;
if ((flags & 2u) != 0u) {
// Compute UV from local position and half_size
vec2 p_for_uv = f_local_or_uv;
if (f_rotation != 0.0) {
p_for_uv = apply_rotation(p_for_uv, f_rotation);
}
vec2 local_uv = p_for_uv / b * 0.5 + 0.5;
vec2 uv = mix(f_uv_rect.xy, f_uv_rect.zw, local_uv);
shape_color *= texture(tex, uv);
}
float alpha = sdf_alpha(d, soft);
out_color = vec4(shape_color.rgb, shape_color.a * alpha);
return;
} }
else if (kind == 2u) { else if (kind == 2u) {
// NGon — half_feather in params.z // Circle — rotationally symmetric, no rotation needed
float radius = f_params.x; float radius = f_params.x;
float sides = f_params.y; soft = max(f_params.y, 1.0);
h = f_params.z; float stroke_px = f_params.z;
d = sdRegularPolygon(p_local, radius, sides);
half_size = vec2(radius); // for gradient UV computation d = sdCircle(f_local_or_uv, radius);
if ((flags & 1u) != 0u) d = sdf_stroke(d, stroke_px);
} }
else if (kind == 3u) { else if (kind == 3u) {
// Ellipse — half_feather in params.z // Ellipse
vec2 ab = f_params.xy; vec2 ab = f_params.xy;
h = f_params.z; soft = max(f_params.z, 1.0);
d = sdEllipseApprox(p_local, ab); float stroke_px = f_params.w;
half_size = ab; // for gradient UV computation
vec2 p_local = f_local_or_uv;
if (f_rotation != 0.0) {
p_local = apply_rotation(p_local, f_rotation);
}
d = sdEllipse(p_local, ab);
if ((flags & 1u) != 0u) d = sdf_stroke(d, stroke_px);
} }
else if (kind == 4u) { else if (kind == 4u) {
// Ring_Arc — half_feather in params2.z // Segment (capsule line) — no rotation (excluded)
// Arc mode from flag bits 5-6: 0 = full, 1 = narrow (≤π), 2 = wide (>π) vec2 a = f_params.xy; // already in local physical pixels
vec2 b = f_params.zw;
float width = f_params2.x;
soft = max(f_params2.y, 1.0);
d = sdSegment(f_local_or_uv, a, b) - width * 0.5;
}
else if (kind == 5u) {
// Ring / Arc — rotation handled by CPU angle offset, no shader rotation
float inner = f_params.x; float inner = f_params.x;
float outer = f_params.y; float outer = f_params.y;
vec2 n_start = f_params.zw; float start_rad = f_params.z;
vec2 n_end = f_params2.xy; float end_rad = f_params.w;
uint arc_bits = (flags >> 5u) & 3u; soft = max(f_params2.x, 1.0);
h = f_params2.z; float r = length(f_local_or_uv);
float d_ring = max(inner - r, r - outer);
float r = length(p_local); // Angular clip
d = max(inner - r, r - outer); float angle = atan(f_local_or_uv.y, f_local_or_uv.x);
if (angle < 0.0) angle += 2.0 * PI;
float ang_start = mod(start_rad, 2.0 * PI);
float ang_end = mod(end_rad, 2.0 * PI);
if (arc_bits != 0u) { float in_arc = (ang_end > ang_start)
float d_start = dot(p_local, n_start); ? ((angle >= ang_start && angle <= ang_end) ? 1.0 : 0.0) : ((angle >= ang_start || angle <= ang_end) ? 1.0 : 0.0);
float d_end = dot(p_local, n_end); if (abs(ang_end - ang_start) >= 2.0 * PI - 0.001) in_arc = 1.0;
float d_wedge = (arc_bits == 1u)
? max(d_start, d_end) // arc ≤ π: intersect half-planes d = in_arc > 0.5 ? d_ring : 1e30;
: min(d_start, d_end); // arc > π: union half-planes }
d = max(d, d_wedge); else if (kind == 6u) {
// Regular N-gon — has its own rotation in params, no Primitive.rotation used
float radius = f_params.x;
float rotation = f_params.y;
float sides = f_params.z;
soft = max(f_params.w, 1.0);
float stroke_px = f_params2.x;
vec2 p = f_local_or_uv;
float c = cos(rotation), s = sin(rotation);
p = mat2(c, -s, s, c) * p;
float an = PI / sides;
float bn = mod(atan(p.y, p.x), 2.0 * an) - an;
d = length(p) * cos(bn) - radius;
if ((flags & 1u) != 0u) d = sdf_stroke(d, stroke_px);
} }
half_size = vec2(outer); // for gradient UV computation float alpha = sdf_alpha(d, soft);
} out_color = vec4(f_color.rgb, f_color.a * alpha);
// --- 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);
}
} }
+22 -41
View File
@@ -6,14 +6,13 @@ layout(location = 1) in vec2 v_uv;
layout(location = 2) in vec4 v_color; layout(location = 2) in vec4 v_color;
// ---------- Outputs to fragment shader ---------- // ---------- Outputs to fragment shader ----------
layout(location = 0) out mediump vec4 f_color; layout(location = 0) out vec4 f_color;
layout(location = 1) out vec2 f_local_or_uv; layout(location = 1) out vec2 f_local_or_uv;
layout(location = 2) out vec4 f_params; layout(location = 2) out vec4 f_params;
layout(location = 3) out vec4 f_params2; layout(location = 3) out vec4 f_params2;
layout(location = 4) flat out uint f_flags; layout(location = 4) flat out uint f_kind_flags;
layout(location = 5) flat out float f_rotation;
layout(location = 6) flat out vec4 f_uv_rect; 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) ---------- // ---------- Uniforms (single block — avoids spirv-cross reordering on Metal) ----------
layout(set = 1, binding = 0) uniform Uniforms { layout(set = 1, binding = 0) uniform Uniforms {
@@ -23,67 +22,49 @@ layout(set = 1, binding = 0) uniform Uniforms {
}; };
// ---------- SDF primitive storage buffer ---------- // ---------- SDF primitive storage buffer ----------
// Mirrors the CPU-side Core_2D_Primitive in core_2d.odin. Named with the struct Primitive {
// subsystem prefix so a project-wide grep on the type name matches both the GLSL vec4 bounds; // 0-15: min_x, min_y, max_x, max_y
// declaration and the Odin declaration. uint color; // 16-19: packed u8x4 (unpack with unpackUnorm4x8)
struct Core_2D_Primitive { uint kind_flags; // 20-23: kind | (flags << 8)
vec4 bounds; // 0-15 float rotation; // 24-27: shader self-rotation in radians
uint color; // 16-19 float _pad; // 28-31: alignment padding
uint flags; // 20-23 vec4 params; // 32-47: shape params part 1
uint rotation_sc; // 24-27: packed f16 pair (sin, cos) vec4 params2; // 48-63: shape params part 2
float _pad; // 28-31 vec4 uv_rect; // 64-79: u_min, v_min, u_max, v_max
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 { layout(std430, set = 0, binding = 0) readonly buffer Primitives {
Core_2D_Primitive primitives[]; Primitive primitives[];
}; };
// ---------- Entry point ---------- // ---------- Entry point ----------
void main() { void main() {
if (mode == 0u) { if (mode == 0u) {
// ---- Mode 0: Tessellated (used for text and arbitrary user geometry) ---- // ---- Mode 0: Tessellated (legacy) ----
f_color = v_color; f_color = v_color;
f_local_or_uv = v_uv; f_local_or_uv = v_uv;
f_params = vec4(0.0); f_params = vec4(0.0);
f_params2 = vec4(0.0); f_params2 = vec4(0.0);
f_flags = 0u; f_kind_flags = 0u;
f_uv_rect = vec4(0.0); f_rotation = 0.0;
f_effects = uvec4(0); f_uv_rect = vec4(0.0, 0.0, 1.0, 1.0);
gl_Position = projection * vec4(v_position * dpi_scale, 0.0, 1.0); gl_Position = projection * vec4(v_position * dpi_scale, 0.0, 1.0);
} else { } else {
// ---- Mode 1: SDF instanced quads ---- // ---- Mode 1: SDF instanced quads ----
Core_2D_Primitive p = primitives[gl_InstanceIndex]; Primitive p = primitives[gl_InstanceIndex];
vec2 corner = v_position; // unit quad corners: (0,0)-(1,1) vec2 corner = v_position; // unit quad corners: (0,0)-(1,1)
vec2 world_pos = mix(p.bounds.xy, p.bounds.zw, corner); vec2 world_pos = mix(p.bounds.xy, p.bounds.zw, corner);
vec2 center = 0.5 * (p.bounds.xy + p.bounds.zw); 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_color = unpackUnorm4x8(p.color);
f_local_or_uv = local; // shape-local physical pixels (rotated if .Rotated set) f_local_or_uv = (world_pos - center) * dpi_scale; // shape-centered physical pixels
f_params = p.params; f_params = p.params;
f_params2 = p.params2; f_params2 = p.params2;
f_flags = p.flags; f_kind_flags = p.kind_flags;
f_rotation = p.rotation;
f_uv_rect = p.uv_rect; f_uv_rect = p.uv_rect;
f_effects = p.effects;
gl_Position = projection * vec4(world_pos * dpi_scale, 0.0, 1.0); gl_Position = projection * vec4(world_pos * dpi_scale, 0.0, 1.0);
} }
+1167
View File
File diff suppressed because it is too large Load Diff
-339
View File
@@ -1,339 +0,0 @@
package tess
import "core:math"
import draw ".."
//INTERNAL
SMOOTH_CIRCLE_ERROR_RATE :: 0.1
auto_segments :: proc(radius: f32, arc_degrees: f32) -> int {
if radius <= 0 do return 4
phys_radius := radius * draw.GLOB.dpi_scaling
acos_arg := clamp(2 * math.pow(1 - SMOOTH_CIRCLE_ERROR_RATE / phys_radius, 2) - 1, -1, 1)
theta := math.acos(acos_arg)
if theta <= 0 do return 4
full_circle_segments := int(math.ceil(2 * math.PI / theta))
segments := int(f32(full_circle_segments) * arc_degrees / 360.0)
min_segments := max(int(math.ceil(f64(arc_degrees / 90.0))), 4)
return max(segments, min_segments)
}
// ----- Internal helpers -----
// 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)
}
+19 -27
View File
@@ -8,25 +8,21 @@ import sdl_ttf "vendor:sdl3/ttf"
Font_Id :: u16 Font_Id :: u16
//INTERNAL
Font_Key :: struct { Font_Key :: struct {
id: Font_Id, id: Font_Id,
size: u16, size: u16,
} }
//INTERNAL
Cache_Source :: enum u8 { Cache_Source :: enum u8 {
Custom, Custom,
Clay, Clay,
} }
//INTERNAL
Cache_Key :: struct { Cache_Key :: struct {
id: u32, id: u32,
source: Cache_Source, source: Cache_Source,
} }
//INTERNAL
Text_Cache :: struct { Text_Cache :: struct {
engine: ^sdl_ttf.TextEngine, engine: ^sdl_ttf.TextEngine,
font_bytes: [dynamic][]u8, font_bytes: [dynamic][]u8,
@@ -34,8 +30,7 @@ Text_Cache :: struct {
cache: map[Cache_Key]^sdl_ttf.Text, cache: map[Cache_Key]^sdl_ttf.Text,
} }
// Fetch SDL TTF font pointer for rendering. // Internal for fetching SDL TTF font pointer for rendering
//INTERNAL
get_font :: proc(id: Font_Id, size: u16) -> ^sdl_ttf.Font { get_font :: proc(id: Font_Id, size: u16) -> ^sdl_ttf.Font {
assert(int(id) < len(GLOB.text_cache.font_bytes), "Invalid font ID.") assert(int(id) < len(GLOB.text_cache.font_bytes), "Invalid font ID.")
key := Font_Key{id, size} key := Font_Key{id, size}
@@ -82,10 +77,9 @@ register_font :: proc(bytes: []u8) -> (id: Font_Id, ok: bool) #optional_ok {
return Font_Id(len(GLOB.text_cache.font_bytes) - 1), true return Font_Id(len(GLOB.text_cache.font_bytes) - 1), true
} }
//INTERNAL
Text :: struct { Text :: struct {
sdl_text: ^sdl_ttf.Text, sdl_text: ^sdl_ttf.Text,
position: Vec2, position: [2]f32,
color: Color, color: Color,
} }
@@ -95,7 +89,7 @@ Text :: struct {
// Shared cache lookup/create/update logic used by both the `text` proc and the Clay render path. // 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. // Returns the cached (or newly created) TTF_Text pointer.
//INTERNAL @(private)
cache_get_or_update :: proc(key: Cache_Key, c_str: cstring, font: ^sdl_ttf.Font) -> ^sdl_ttf.Text { 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] existing, found := GLOB.text_cache.cache[key]
if !found { if !found {
@@ -135,11 +129,11 @@ cache_get_or_update :: proc(key: Cache_Key, c_str: cstring, font: ^sdl_ttf.Font)
text :: proc( text :: proc(
layer: ^Layer, layer: ^Layer,
text_string: string, text_string: string,
position: Vec2, position: [2]f32,
font_id: Font_Id, font_id: Font_Id,
font_size: u16 = DFT_FONT_SIZE, font_size: u16 = 44,
color: Color = DFT_TEXT_COLOR, color: Color = BLACK,
origin: Vec2 = {}, origin: [2]f32 = {0, 0},
rotation: f32 = 0, rotation: f32 = 0,
id: Maybe(u32) = nil, id: Maybe(u32) = nil,
temp_allocator := context.temp_allocator, temp_allocator := context.temp_allocator,
@@ -183,9 +177,9 @@ text :: proc(
measure_text :: proc( measure_text :: proc(
text_string: string, text_string: string,
font_id: Font_Id, font_id: Font_Id,
font_size: u16 = DFT_FONT_SIZE, font_size: u16 = 44,
allocator := context.temp_allocator, allocator := context.temp_allocator,
) -> Vec2 { ) -> [2]f32 {
c_str := strings.clone_to_cstring(text_string, allocator) c_str := strings.clone_to_cstring(text_string, allocator)
defer delete(c_str, allocator) defer delete(c_str, allocator)
width, height: c.int width, height: c.int
@@ -199,46 +193,46 @@ measure_text :: proc(
// ----- Text anchor helpers ----------- // ----- Text anchor helpers -----------
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
center_of_text :: proc(text_string: string, font_id: Font_Id, font_size: u16 = DFT_FONT_SIZE) -> Vec2 { center_of_text :: proc(text_string: string, font_id: Font_Id, font_size: u16 = 44) -> [2]f32 {
size := measure_text(text_string, font_id, font_size) size := measure_text(text_string, font_id, font_size)
return size * 0.5 return size * 0.5
} }
top_left_of_text :: proc(text_string: string, font_id: Font_Id, font_size: u16 = DFT_FONT_SIZE) -> Vec2 { top_left_of_text :: proc(text_string: string, font_id: Font_Id, font_size: u16 = 44) -> [2]f32 {
return {0, 0} return {0, 0}
} }
top_of_text :: proc(text_string: string, font_id: Font_Id, font_size: u16 = DFT_FONT_SIZE) -> Vec2 { top_of_text :: proc(text_string: string, font_id: Font_Id, font_size: u16 = 44) -> [2]f32 {
size := measure_text(text_string, font_id, font_size) size := measure_text(text_string, font_id, font_size)
return {size.x * 0.5, 0} 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 { top_right_of_text :: proc(text_string: string, font_id: Font_Id, font_size: u16 = 44) -> [2]f32 {
size := measure_text(text_string, font_id, font_size) size := measure_text(text_string, font_id, font_size)
return {size.x, 0} return {size.x, 0}
} }
left_of_text :: proc(text_string: string, font_id: Font_Id, font_size: u16 = DFT_FONT_SIZE) -> Vec2 { left_of_text :: proc(text_string: string, font_id: Font_Id, font_size: u16 = 44) -> [2]f32 {
size := measure_text(text_string, font_id, font_size) size := measure_text(text_string, font_id, font_size)
return {0, size.y * 0.5} return {0, size.y * 0.5}
} }
right_of_text :: proc(text_string: string, font_id: Font_Id, font_size: u16 = DFT_FONT_SIZE) -> Vec2 { right_of_text :: proc(text_string: string, font_id: Font_Id, font_size: u16 = 44) -> [2]f32 {
size := measure_text(text_string, font_id, font_size) size := measure_text(text_string, font_id, font_size)
return {size.x, size.y * 0.5} 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 { bottom_left_of_text :: proc(text_string: string, font_id: Font_Id, font_size: u16 = 44) -> [2]f32 {
size := measure_text(text_string, font_id, font_size) size := measure_text(text_string, font_id, font_size)
return {0, size.y} return {0, size.y}
} }
bottom_of_text :: proc(text_string: string, font_id: Font_Id, font_size: u16 = DFT_FONT_SIZE) -> Vec2 { bottom_of_text :: proc(text_string: string, font_id: Font_Id, font_size: u16 = 44) -> [2]f32 {
size := measure_text(text_string, font_id, font_size) size := measure_text(text_string, font_id, font_size)
return {size.x * 0.5, size.y} 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 { bottom_right_of_text :: proc(text_string: string, font_id: Font_Id, font_size: u16 = 44) -> [2]f32 {
size := measure_text(text_string, font_id, font_size) size := measure_text(text_string, font_id, font_size)
return size return size
} }
@@ -274,8 +268,7 @@ clear_text_cache_entry :: proc(id: u32) {
// ----- Internal cache lifecycle ------ // ----- Internal cache lifecycle ------
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
//INTERNAL @(private, require_results)
@(require_results)
init_text_cache :: proc( init_text_cache :: proc(
device: ^sdl.GPUDevice, device: ^sdl.GPUDevice,
allocator := context.allocator, allocator := context.allocator,
@@ -306,7 +299,6 @@ init_text_cache :: proc(
return text_cache, true return text_cache, true
} }
//INTERNAL
destroy_text_cache :: proc() { destroy_text_cache :: proc() {
for _, font in GLOB.text_cache.sdl_fonts { for _, font in GLOB.text_cache.sdl_fonts {
sdl_ttf.CloseFont(font) sdl_ttf.CloseFont(font)
+13 -12
View File
@@ -14,8 +14,8 @@ Texture_Kind :: enum u8 {
} }
Sampler_Preset :: enum u8 { Sampler_Preset :: enum u8 {
Linear_Clamp,
Nearest_Clamp, Nearest_Clamp,
Linear_Clamp,
Nearest_Repeat, Nearest_Repeat,
Linear_Repeat, Linear_Repeat,
} }
@@ -41,7 +41,8 @@ Texture_Desc :: struct {
kind: Texture_Kind, kind: Texture_Kind,
} }
//INTERNAL // Internal slot — not exported.
@(private)
Texture_Slot :: struct { Texture_Slot :: struct {
gpu_texture: ^sdl.GPUTexture, gpu_texture: ^sdl.GPUTexture,
desc: Texture_Desc, desc: Texture_Desc,
@@ -318,8 +319,8 @@ texture_kind :: proc(id: Texture_Id) -> Texture_Kind {
return GLOB.texture_slots[u32(id)].desc.kind return GLOB.texture_slots[u32(id)].desc.kind
} }
// Get the raw GPU texture pointer for binding during draw. // Internal: get the raw GPU texture pointer for binding during draw.
//INTERNAL @(private)
texture_gpu_handle :: proc(id: Texture_Id) -> ^sdl.GPUTexture { texture_gpu_handle :: proc(id: Texture_Id) -> ^sdl.GPUTexture {
if id == INVALID_TEXTURE do return nil if id == INVALID_TEXTURE do return nil
idx := u32(id) idx := u32(id)
@@ -327,8 +328,8 @@ texture_gpu_handle :: proc(id: Texture_Id) -> ^sdl.GPUTexture {
return GLOB.texture_slots[idx].gpu_texture return GLOB.texture_slots[idx].gpu_texture
} }
// Deferred release (called from end / clear_global). // Deferred release (called from draw.end / clear_global)
//INTERNAL @(private)
process_pending_texture_releases :: proc() { process_pending_texture_releases :: proc() {
device := GLOB.device device := GLOB.device
for id in GLOB.pending_texture_releases { for id in GLOB.pending_texture_releases {
@@ -345,7 +346,7 @@ process_pending_texture_releases :: proc() {
clear(&GLOB.pending_texture_releases) clear(&GLOB.pending_texture_releases)
} }
//INTERNAL @(private)
get_sampler :: proc(preset: Sampler_Preset) -> ^sdl.GPUSampler { get_sampler :: proc(preset: Sampler_Preset) -> ^sdl.GPUSampler {
idx := int(preset) idx := int(preset)
if GLOB.samplers[idx] != nil do return GLOB.samplers[idx] if GLOB.samplers[idx] != nil do return GLOB.samplers[idx]
@@ -378,15 +379,15 @@ get_sampler :: proc(preset: Sampler_Preset) -> ^sdl.GPUSampler {
) )
if sampler == nil { if sampler == nil {
log.errorf("Failed to create sampler preset %v: %s", preset, sdl.GetError()) log.errorf("Failed to create sampler preset %v: %s", preset, sdl.GetError())
return GLOB.core_2d.sampler // fallback to existing default sampler return GLOB.pipeline_2d_base.sampler // fallback to existing default sampler
} }
GLOB.samplers[idx] = sampler GLOB.samplers[idx] = sampler
return sampler return sampler
} }
// Destroy all sampler pool entries. Called from destroy(). // Internal: destroy all sampler pool entries. Called from draw.destroy().
//INTERNAL @(private)
destroy_sampler_pool :: proc() { destroy_sampler_pool :: proc() {
device := GLOB.device device := GLOB.device
for &s in GLOB.samplers { for &s in GLOB.samplers {
@@ -397,8 +398,8 @@ destroy_sampler_pool :: proc() {
} }
} }
// Destroy all registered textures. Called from destroy(). // Internal: destroy all registered textures. Called from draw.destroy().
//INTERNAL @(private)
destroy_all_textures :: proc() { destroy_all_textures :: proc() {
device := GLOB.device device := GLOB.device
for &slot in GLOB.texture_slots { for &slot in GLOB.texture_slots {
+28 -34
View File
@@ -2,7 +2,6 @@ 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"
@@ -26,20 +25,15 @@ Bits :: struct {
length: int, // Total number of bits being stored length: int, // Total number of bits being stored
} }
destroy :: proc(bits: Bits, allocator := context.allocator) -> runtime.Allocator_Error { delete :: proc(bits: Bits, allocator := context.allocator) {
return delete_slice(bits.int_array, allocator) delete_slice(bits.int_array, allocator)
} }
create :: proc( make :: proc(#any_int length: int, allocator := context.allocator) -> Bits {
#any_int length: int, return Bits {
allocator := context.allocator, int_array = make_slice([]Int_Bits, ((length - 1) >> INDEX_SHIFT) + 1, 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)
@@ -513,8 +507,8 @@ import "core:testing"
@(test) @(test)
test_set :: proc(t: ^testing.T) { test_set :: proc(t: ^testing.T) {
bits := create(128) bits := make(128)
defer destroy(bits) defer delete(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})
@@ -530,8 +524,8 @@ test_set :: proc(t: ^testing.T) {
@(test) @(test)
test_get :: proc(t: ^testing.T) { test_get :: proc(t: ^testing.T) {
bits := create(128) bits := make(128)
defer destroy(bits) defer delete(bits)
// Default is false // Default is false
testing.expect(t, !get(bits, 0)) testing.expect(t, !get(bits, 0))
@@ -566,8 +560,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 := create(128) bits := make(128)
defer destroy(bits) defer delete(bits)
// set_true within first uint // set_true within first uint
set_true(bits, 0) set_true(bits, 0)
@@ -611,8 +605,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 := create(132) bits := make(132)
defer destroy(bits) defer delete(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
@@ -622,8 +616,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 := create(1) bits2 := make(1)
defer destroy(bits2) defer delete(bits2)
bits2.int_array[0] = {0} bits2.int_array[0] = {0}
testing.expect(t, all_true(bits2)) testing.expect(t, all_true(bits2))
@@ -634,8 +628,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 := create(192) bits := make(192)
defer destroy(bits) defer delete(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))
@@ -682,7 +676,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 := create(128, context.temp_allocator) bits := make(128, context.temp_allocator)
set_true(bits, 0) set_true(bits, 0)
set_true(bits, 10) set_true(bits, 10)
@@ -716,7 +710,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 := create(128, context.temp_allocator) bits := make(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 {
@@ -755,7 +749,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 := create(192, context.temp_allocator) bits := make(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 {
@@ -779,7 +773,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 := create(192, context.temp_allocator) bits := make(192, context.temp_allocator)
set_true(bits, 5) set_true(bits, 5)
set_true(bits, 130) set_true(bits, 130)
@@ -796,7 +790,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 := create(130, context.temp_allocator) bits := make(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 {
@@ -817,7 +811,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 := create(130, context.temp_allocator) bits := make(130, context.temp_allocator)
set_true(bits, 0) set_true(bits, 0)
set_true(bits, 129) set_true(bits, 129)
@@ -834,7 +828,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 := create(100, context.temp_allocator) bits := make(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)
@@ -909,7 +903,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 := create(100, context.temp_allocator) bits := make(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
@@ -950,7 +944,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 := create(100, context.temp_allocator) bits := make(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)
-44
View File
@@ -1,8 +1,6 @@
package meta package meta
import "core:fmt" import "core:fmt"
import "core:log"
import "core:mem"
import "core:os" import "core:os"
Command :: struct { Command :: struct {
@@ -22,48 +20,6 @@ COMMANDS :: []Command {
} }
main :: proc() { 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:] args := os.args[1:]
if len(args) == 0 { if len(args) == 0 {
+19 -22
View File
@@ -4,8 +4,7 @@
package phased_executor package phased_executor
import "base:intrinsics" import "base:intrinsics"
import "base:runtime" import q "core:container/queue"
import que "core:container/queue"
import "core:prof/spall" import "core:prof/spall"
import "core:sync" import "core:sync"
import "core:thread" import "core:thread"
@@ -19,7 +18,7 @@ DEFT_SPIN_LIMIT :: 2_500_000
Harness :: struct($T: typeid) where intrinsics.type_has_nil(T) { Harness :: struct($T: typeid) where intrinsics.type_has_nil(T) {
mutex: sync.Mutex, mutex: sync.Mutex,
condition: sync.Cond, condition: sync.Cond,
cmd_queue: que.Queue(T), cmd_queue: q.Queue(T),
spin: bool, spin: bool,
lock: levsync.Spinlock, lock: levsync.Spinlock,
_pad: [64 - size_of(uint)]u8, // We want join_count to have its own cache line _pad: [64 - size_of(uint)]u8, // We want join_count to have its own cache line
@@ -43,13 +42,13 @@ Executor :: struct($T: typeid) where intrinsics.type_has_nil(T) {
} }
//TODO: Provide a way to set some aspects of context for the executor threads. Namely a logger. //TODO: Provide a way to set some aspects of context for the executor threads. Namely a logger.
init :: proc( init_executor :: proc(
executor: ^Executor($T), executor: ^Executor($T),
#any_int num_threads: int, #any_int num_threads: int,
$on_command_received: proc(command: T), $on_command_received: proc(command: T),
#any_int spin_limit: uint = DEFT_SPIN_LIMIT, #any_int spin_limit: uint = DEFT_SPIN_LIMIT,
allocator := context.allocator, allocator := context.allocator,
) -> runtime.Allocator_Error { ) {
was_initialized, _ := intrinsics.atomic_compare_exchange_strong_explicit( was_initialized, _ := intrinsics.atomic_compare_exchange_strong_explicit(
&executor.initialized, &executor.initialized,
false, false,
@@ -61,9 +60,9 @@ init :: proc(
slave_task := build_task(on_command_received) slave_task := build_task(on_command_received)
executor.spin_limit = spin_limit executor.spin_limit = spin_limit
executor.harnesses = make([]Harness(T), num_threads, allocator) or_return executor.harnesses = make([]Harness(T), num_threads, allocator)
for &harness in executor.harnesses { for &harness in executor.harnesses {
que.init(&harness.cmd_queue, allocator = allocator) or_return q.init(&harness.cmd_queue, allocator = allocator)
harness.spin = true harness.spin = true
} }
@@ -73,11 +72,11 @@ init :: proc(
} }
thread.pool_start(&executor.thread_pool) thread.pool_start(&executor.thread_pool)
return nil return
} }
// Cleanly shuts down all executor tasks then destroys the executor // Cleanly shuts down all executor tasks then destroys the executor
destroy :: proc(executor: ^Executor($T), allocator := context.allocator) -> runtime.Allocator_Error { destroy_executor :: proc(executor: ^Executor($T), allocator := context.allocator) {
was_initialized, _ := intrinsics.atomic_compare_exchange_strong_explicit( was_initialized, _ := intrinsics.atomic_compare_exchange_strong_explicit(
&executor.initialized, &executor.initialized,
true, true,
@@ -91,7 +90,7 @@ destroy :: proc(executor: ^Executor($T), allocator := context.allocator) -> runt
for &harness in executor.harnesses { for &harness in executor.harnesses {
for { for {
if levsync.try_lock(&harness.lock) { if levsync.try_lock(&harness.lock) {
que.push_back(&harness.cmd_queue, nil) q.push_back(&harness.cmd_queue, nil)
if !harness.spin { if !harness.spin {
sync.mutex_lock(&harness.mutex) sync.mutex_lock(&harness.mutex)
sync.cond_signal(&harness.condition) sync.cond_signal(&harness.condition)
@@ -106,11 +105,9 @@ destroy :: proc(executor: ^Executor($T), allocator := context.allocator) -> runt
thread.pool_join(&executor.thread_pool) thread.pool_join(&executor.thread_pool)
thread.pool_destroy(&executor.thread_pool) thread.pool_destroy(&executor.thread_pool)
for &harness in executor.harnesses { for &harness in executor.harnesses {
que.destroy(&harness.cmd_queue) q.destroy(&harness.cmd_queue)
} }
delete(executor.harnesses, allocator) or_return delete(executor.harnesses, allocator)
return nil
} }
build_task :: proc( build_task :: proc(
@@ -134,10 +131,10 @@ build_task :: proc(
spin_count: uint = 0 spin_count: uint = 0
spin_loop: for { spin_loop: for {
if levsync.try_lock(&harness.lock) { if levsync.try_lock(&harness.lock) {
if que.len(harness.cmd_queue) > 0 { if q.len(harness.cmd_queue) > 0 {
// Execute command // Execute command
command := que.pop_front(&harness.cmd_queue) command := q.pop_front(&harness.cmd_queue)
levsync.unlock(&harness.lock) levsync.unlock(&harness.lock)
if command == nil do return if command == nil do return
on_command_received(command) on_command_received(command)
@@ -166,7 +163,7 @@ build_task :: proc(
defer intrinsics.cpu_relax() defer intrinsics.cpu_relax()
if levsync.try_lock(&harness.lock) { if levsync.try_lock(&harness.lock) {
defer levsync.unlock(&harness.lock) defer levsync.unlock(&harness.lock)
if que.len(harness.cmd_queue) > 0 { if q.len(harness.cmd_queue) > 0 {
harness.spin = true harness.spin = true
break cond_loop break cond_loop
} else { } else {
@@ -193,9 +190,9 @@ exec_command :: proc(executor: ^Executor($T), command: T) {
} }
harness := &executor.harnesses[executor.harness_index] harness := &executor.harnesses[executor.harness_index]
if levsync.try_lock(&harness.lock) { if levsync.try_lock(&harness.lock) {
if que.len(harness.cmd_queue) <= executor.cmd_queue_floor { if q.len(harness.cmd_queue) <= executor.cmd_queue_floor {
que.push_back(&harness.cmd_queue, command) q.push_back(&harness.cmd_queue, command)
executor.cmd_queue_floor = que.len(harness.cmd_queue) executor.cmd_queue_floor = q.len(harness.cmd_queue)
slave_sleeping := !harness.spin slave_sleeping := !harness.spin
// Must release lock before signalling to avoid race from slave spurious wakeup // Must release lock before signalling to avoid race from slave spurious wakeup
levsync.unlock(&harness.lock) levsync.unlock(&harness.lock)
@@ -261,7 +258,7 @@ stress_test_executor :: proc(t: ^testing.T) {
defer free(exec_counts) defer free(exec_counts)
executor: Executor(Stress_Cmd) executor: Executor(Stress_Cmd)
init(&executor, STRESS_NUM_THREADS, stress_handler, spin_limit = 500) init_executor(&executor, STRESS_NUM_THREADS, stress_handler, spin_limit = 500)
for round in 0 ..< STRESS_NUM_ROUNDS { for round in 0 ..< STRESS_NUM_ROUNDS {
base := round * STRESS_CMDS_PER_ROUND base := round * STRESS_CMDS_PER_ROUND
@@ -284,6 +281,6 @@ stress_test_executor :: proc(t: ^testing.T) {
// Explicitly destroy to verify clean shutdown. // Explicitly destroy to verify clean shutdown.
// If destroy_executor returns, all threads received the nil sentinel and exited, // If destroy_executor returns, all threads received the nil sentinel and exited,
// and thread.pool_join completed without deadlock. // and thread.pool_join completed without deadlock.
destroy(&executor) destroy_executor(&executor)
testing.expect(t, !executor.initialized, "Executor still marked initialized after destroy") testing.expect(t, !executor.initialized, "Executor still marked initialized after destroy")
} }
+14 -9
View File
@@ -1,32 +1,40 @@
package examples package examples
import "core:fmt" import "core:fmt"
import "core:log"
import "core:mem" import "core:mem"
import "core:os" import "core:os"
import qr ".." import qr ".."
main :: proc() { main :: proc() {
//----- General setup ---------------------------------- //----- Tracking allocator ----------------------------------
{
tracking_temp_allocator := false
// Temp // Temp
track_temp: mem.Tracking_Allocator track_temp: mem.Tracking_Allocator
if tracking_temp_allocator {
mem.tracking_allocator_init(&track_temp, context.temp_allocator) mem.tracking_allocator_init(&track_temp, context.temp_allocator)
context.temp_allocator = mem.tracking_allocator(&track_temp) context.temp_allocator = mem.tracking_allocator(&track_temp)
}
// Default // Default
track: mem.Tracking_Allocator track: mem.Tracking_Allocator
mem.tracking_allocator_init(&track, context.allocator) mem.tracking_allocator_init(&track, context.allocator)
context.allocator = mem.tracking_allocator(&track) 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 { defer {
// Temp allocator // Temp allocator
if tracking_temp_allocator {
if len(track_temp.allocation_map) > 0 {
fmt.eprintf("=== %v allocations not freed - temp allocator: ===\n", len(track_temp.allocation_map))
for _, entry in track_temp.allocation_map {
fmt.eprintf("- %v bytes @ %v\n", entry.size, entry.location)
}
}
if len(track_temp.bad_free_array) > 0 { if len(track_temp.bad_free_array) > 0 {
fmt.eprintf("=== %v incorrect frees - temp allocator: ===\n", len(track_temp.bad_free_array)) fmt.eprintf("=== %v incorrect frees - temp allocator: ===\n", len(track_temp.bad_free_array))
for entry in track_temp.bad_free_array { for entry in track_temp.bad_free_array {
fmt.eprintf("- %p @ %v\n", entry.memory, entry.location) fmt.eprintf("- %p @ %v\n", entry.memory, entry.location)
} }
}
mem.tracking_allocator_destroy(&track_temp) mem.tracking_allocator_destroy(&track_temp)
} }
// Default allocator // Default allocator
@@ -44,10 +52,7 @@ main :: proc() {
} }
mem.tracking_allocator_destroy(&track) mem.tracking_allocator_destroy(&track)
} }
// Logger }
context.logger = log.create_console_logger()
defer log.destroy_console_logger(context.logger)
args := os.args args := os.args
if len(args) < 2 { if len(args) < 2 {
+99 -269
View File
@@ -1,139 +1,103 @@
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($E: typeid) { Ring :: struct($T: typeid) {
data: []E, data: []T,
next_write_index, len: int, _end_index, len: int,
} }
Ring_Soa :: struct($E: typeid) { Ring_Soa :: struct($T: typeid) {
data: #soa[]E, data: #soa[]T,
next_write_index, len: int, _end_index, len: int,
} }
destroy_aos :: #force_inline proc( from_slice_raos :: #force_inline proc(data: $T/[]$E) -> Ring(E) {
ring: ^Ring($E), return {data = data, _end_index = -1}
allocator := context.allocator,
) -> runtime.Allocator_Error {
return delete(ring.data)
} }
destroy_soa :: #force_inline proc( from_slice_rsoa :: #force_inline proc(data: $T/#soa[]$E) -> Ring_Soa(E) {
ring: ^Ring_Soa($E), return {data = data, _end_index = -1}
allocator := context.allocator,
) -> runtime.Allocator_Error {
return delete(ring.data)
} }
destroy :: proc { from_slice :: proc {
destroy_aos, from_slice_raos,
destroy_soa, from_slice_rsoa,
} }
create_aos :: #force_inline proc(
$E: typeid,
capacity: int,
allocator := context.allocator,
) -> (
ring: Ring(E),
err: runtime.Allocator_Error,
) #optional_allocator_error {
ring.data, err = make([]E, capacity, allocator)
return ring, err
}
create_soa :: #force_inline proc(
$E: typeid,
capacity: int,
allocator := context.allocator,
) -> (
ring: Ring_Soa(E),
err: runtime.Allocator_Error,
) #optional_allocator_error {
ring.data, err = make(#soa[]E, capacity, allocator)
return ring, err
}
// All contents of `data` will be completely ignored, `data` is treated as an empty slice.
init_from_slice_aos :: #force_inline proc(ring: ^Ring($E), data: $T/[]E) {
ring.data = data
ring.len = 0
ring.next_write_index = 0
return
}
// All contents of `data` will be completely ignored, `data` is treated as an empty slice.
init_from_slice_soa :: #force_inline proc(ring: ^Ring_Soa($E), data: $T/#soa[]E) {
ring.data = data
ring.len = 0
ring.next_write_index = 0
return
}
init_from_slice :: proc {
init_from_slice_aos,
init_from_slice_soa,
}
// Internal
// Index in the backing array where the ring starts // Index in the backing array where the ring starts
start_index_aos :: #force_inline proc(ring: Ring($E)) -> int { _start_index_raos :: proc(ring: Ring($T)) -> int {
return ring.len < len(ring.data) ? 0 : ring.next_write_index if ring.len < len(ring.data) {
return 0
} else {
start_index := ring._end_index + 1
return 0 if start_index == len(ring.data) else start_index
}
} }
// Internal
// Index in the backing array where the ring starts // Index in the backing array where the ring starts
start_index_soa :: #force_inline proc(ring: Ring_Soa($E)) -> int { _start_index_rsoa :: proc(ring: Ring_Soa($T)) -> int {
return ring.len < len(ring.data) ? 0 : ring.next_write_index if ring.len < len(ring.data) {
return 0
} else {
start_index := ring._end_index + 1
return 0 if start_index == len(ring.data) else start_index
}
} }
advance_aos :: #force_inline proc(ring: ^Ring($E)) { advance_raos :: proc(ring: ^Ring($T)) {
// Length // Length
if ring.len != len(ring.data) do ring.len += 1 if ring.len != len(ring.data) do ring.len += 1
// Write index // End index
ring.next_write_index += 1 if ring._end_index == len(ring.data) - 1 { // If we are at the end of the backing array
if ring.next_write_index == len(ring.data) do ring.next_write_index = 0 ring._end_index = 0 // Overflow end to 0
} else {
ring._end_index += 1
}
} }
advance_soa :: #force_inline proc(ring: ^Ring_Soa($E)) { advance_rsoa :: proc(ring: ^Ring_Soa($T)) {
// Length // Length
if ring.len != len(ring.data) do ring.len += 1 if ring.len != len(ring.data) do ring.len += 1
// Write index // End index
ring.next_write_index += 1 if ring._end_index == len(ring.data) - 1 { // If we are at the end of the backing array
if ring.next_write_index == len(ring.data) do ring.next_write_index = 0 ring._end_index = 0 // Overflow end to 0
} else {
ring._end_index += 1
}
} }
advance :: proc { advance :: proc {
advance_aos, advance_raos,
advance_soa, advance_rsoa,
} }
append_aos :: #force_inline proc(ring: ^Ring($E), element: E) { append_raos :: proc(ring: ^Ring($T), element: T) {
ring.data[ring.next_write_index] = element
advance(ring) advance(ring)
ring.data[ring._end_index] = element
} }
append_soa :: #force_inline proc(ring: ^Ring_Soa($E), element: E) { append_rsoa :: proc(ring: ^Ring_Soa($T), element: T) {
ring.data[ring.next_write_index] = element
advance(ring) advance(ring)
ring.data[ring._end_index] = element
} }
append :: proc { append :: proc {
append_aos, append_raos,
append_soa, append_rsoa,
} }
get_aos :: #force_inline proc(ring: Ring($E), index: int) -> ^E { get_raos :: proc(ring: Ring($T), index: int) -> ^T {
when ODIN_BOUNDS_CHECK { when ODIN_BOUNDS_CHECK {
fmt.assertf(index < ring.len, "Ring index %i out of bounds for length %i", index, ring.len) if index >= ring.len {
panic(fmt.tprintf("Ring index %i out of bounds for length %i", index, ring.len))
}
} }
array_index := start_index_aos(ring) + index array_index := _start_index_raos(ring) + index
if array_index < len(ring.data) { if array_index < len(ring.data) {
return &ring.data[array_index] return &ring.data[array_index]
} else { } else {
@@ -143,12 +107,14 @@ get_aos :: #force_inline proc(ring: Ring($E), index: int) -> ^E {
} }
// SOA can't return soa pointer to parapoly T. // SOA can't return soa pointer to parapoly T.
get_soa :: #force_inline proc(ring: Ring_Soa($E), index: int) -> E { get_rsoa :: proc(ring: Ring_Soa($T), index: int) -> T {
when ODIN_BOUNDS_CHECK { when ODIN_BOUNDS_CHECK {
fmt.assertf(index < ring.len, "Ring index %i out of bounds for length %i", index, ring.len) if index >= ring.len {
panic(fmt.tprintf("Ring index %i out of bounds for length %i", index, ring.len))
}
} }
array_index := start_index_soa(ring) + index array_index := _start_index_rsoa(ring) + index
if array_index < len(ring.data) { if array_index < len(ring.data) {
return ring.data[array_index] return ring.data[array_index]
} else { } else {
@@ -158,36 +124,36 @@ get_soa :: #force_inline proc(ring: Ring_Soa($E), index: int) -> E {
} }
get :: proc { get :: proc {
get_aos, get_raos,
get_soa, get_rsoa,
} }
get_last_aos :: #force_inline proc(ring: Ring($E)) -> ^E { get_last_raos :: #force_inline proc(ring: Ring($T)) -> ^T {
return get(ring, ring.len - 1) return get(ring, ring.len - 1)
} }
get_last_soa :: #force_inline proc(ring: Ring_Soa($E)) -> E { get_last_rsoa :: #force_inline proc(ring: Ring_Soa($T)) -> T {
return get(ring, ring.len - 1) return get(ring, ring.len - 1)
} }
get_last :: proc { get_last :: proc {
get_last_aos, get_last_raos,
get_last_soa, get_last_rsoa,
} }
clear_aos :: #force_inline proc "contextless" (ring: ^Ring($E)) { clear_raos :: #force_inline proc "contextless" (ring: ^Ring($T)) {
ring.len = 0 ring.len = 0
ring.next_write_index = 0 ring._end_index = -1
} }
clear_soa :: #force_inline proc "contextless" (ring: ^Ring_Soa($E)) { clear_rsoa :: #force_inline proc "contextless" (ring: ^Ring_Soa($T)) {
ring.len = 0 ring.len = 0
ring.next_write_index = 0 ring._end_index = -1
} }
clear :: proc { clear :: proc {
clear_aos, clear_raos,
clear_soa, clear_rsoa,
} }
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
@@ -198,27 +164,28 @@ import "core:testing"
@(test) @(test)
test_ring_aos :: proc(t: ^testing.T) { test_ring_aos :: proc(t: ^testing.T) {
ring := create_aos(int, 10) data := make_slice([]int, 10)
defer destroy(&ring) ring := from_slice(data)
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_aos(ring)) log.debug("Start index:", _start_index_raos(ring))
log.debug("Next write index:", ring.next_write_index) log.debug("End index:", ring._end_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.next_write_index, 5) testing.expect_value(t, ring._end_index, 4)
testing.expect_value(t, start_index_aos(ring), 0) testing.expect_value(t, _start_index_raos(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_aos(ring)) log.debug("Start index:", _start_index_raos(ring))
log.debug("Next write index:", ring.next_write_index) log.debug("End index:", ring._end_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)
@@ -226,18 +193,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.next_write_index, 5) testing.expect_value(t, ring._end_index, 4)
testing.expect_value(t, start_index_aos(ring), 5) testing.expect_value(t, _start_index_raos(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_aos(ring)) log.debug("Start index:", _start_index_raos(ring))
log.debug("Next write index:", ring.next_write_index) log.debug("End index:", ring._end_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.next_write_index, 6) testing.expect_value(t, ring._end_index, 5)
testing.expect_value(t, get_last(ring)^, 25) testing.expect_value(t, get_last(ring)^, 25)
clear(&ring) clear(&ring)
@@ -252,27 +219,28 @@ test_ring_soa :: proc(t: ^testing.T) {
x, y: int, x, y: int,
} }
ring := create_soa(Ints, 10) data := make_soa_slice(#soa[]Ints, 10)
defer destroy(&ring) ring := from_slice(data)
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_soa(ring)) log.debug("Start index:", _start_index_rsoa(ring))
log.debug("Next write index:", ring.next_write_index) log.debug("End index:", ring._end_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.next_write_index, 5) testing.expect_value(t, ring._end_index, 4)
testing.expect_value(t, start_index_soa(ring), 0) testing.expect_value(t, _start_index_rsoa(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_soa(ring)) log.debug("Start index:", _start_index_rsoa(ring))
log.debug("Next write index:", ring.next_write_index) log.debug("End index:", ring._end_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})
@@ -280,18 +248,18 @@ test_ring_soa :: proc(t: ^testing.T) {
testing.expect_value(t, get(ring, 9), Ints{15, 15}) testing.expect_value(t, get(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.next_write_index, 5) testing.expect_value(t, ring._end_index, 4)
testing.expect_value(t, start_index_soa(ring), 5) testing.expect_value(t, _start_index_rsoa(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_soa(ring)) log.debug("Start index:", _start_index_rsoa(ring))
log.debug("Next write index:", ring.next_write_index) log.debug("End index:", ring._end_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.next_write_index, 6) testing.expect_value(t, ring._end_index, 5)
testing.expect_value(t, get_last(ring), Ints{25, 25}) testing.expect_value(t, get_last(ring), Ints{25, 25})
clear(&ring) clear(&ring)
@@ -299,141 +267,3 @@ 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})
}
+906 -812
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+12 -55
View File
@@ -1,11 +1,8 @@
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
@@ -13,74 +10,34 @@ 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() {
//----- General setup ----------------------------------
// Temp
track_temp: mem.Tracking_Allocator
mem.tracking_allocator_init(&track_temp, context.temp_allocator)
context.temp_allocator = mem.tracking_allocator(&track_temp)
// Default
track: mem.Tracking_Allocator
mem.tracking_allocator_init(&track, context.allocator)
context.allocator = mem.tracking_allocator(&track)
// Log a warning about any memory that was not freed by the end of the program.
// This could be fine for some global state or it could be a memory leak.
defer {
// Temp allocator
if len(track_temp.bad_free_array) > 0 {
fmt.eprintf("=== %v incorrect frees - temp allocator: ===\n", len(track_temp.bad_free_array))
for entry in track_temp.bad_free_array {
fmt.eprintf("- %p @ %v\n", entry.memory, entry.location)
}
mem.tracking_allocator_destroy(&track_temp)
}
// Default allocator
if len(track.allocation_map) > 0 {
fmt.eprintf("=== %v allocations not freed - main allocator: ===\n", len(track.allocation_map))
for _, entry in track.allocation_map {
fmt.eprintf("- %v bytes @ %v\n", entry.size, entry.location)
}
}
if len(track.bad_free_array) > 0 {
fmt.eprintf("=== %v incorrect frees - main allocator: ===\n", len(track.bad_free_array))
for entry in track.bad_free_array {
fmt.eprintf("- %p @ %v\n", entry.memory, entry.location)
}
}
mem.tracking_allocator_destroy(&track)
}
// Logger
context.logger = log.create_console_logger()
defer log.destroy_console_logger(context.logger)
environment: ^mdb.Env environment: ^mdb.Env
// Create environment for lmdb // Create environment for lmdb
mdb.panic_on_err(mdb.env_create(&environment)) mdb.panic_on_err(mdb.env_create(&environment))
// Create directory for databases. Won't do anything if it already exists. // Create directory for databases. Won't do anything if it already exists.
os.make_directory(DB_PATH) // 0o774 gives all permissions for owner and group, read for everyone else.
os.make_directory(DB_PATH, 0o774)
// Open the database files (creates them if they don't already exist) // Open the database files (creates them if they don't already exist)
mdb.panic_on_err(mdb.env_open(environment, DB_PATH, {}, DB_MODE)) mdb.panic_on_err(mdb.env_open(environment, DB_PATH, 0, DB_MODE))
// Transactions // Transactions
txn_handle: ^mdb.Txn txn_handle: ^mdb.Txn
db_handle: mdb.Dbi db_handle: mdb.Dbi
// Put transaction // Put transaction
key := 7 key := 7
key_val := mdb.blittable_val(&key) key_val := mdb.autoval(&key)
put_data := 12 put_data := 12
put_data_val := mdb.blittable_val(&put_data) put_data_val := mdb.autoval(&put_data)
mdb.panic_on_err(mdb.txn_begin(environment, nil, {}, &txn_handle)) mdb.panic_on_err(mdb.txn_begin(environment, nil, 0, &txn_handle))
mdb.panic_on_err(mdb.dbi_open(txn_handle, nil, {}, &db_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, &put_data_val, {})) 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)) mdb.panic_on_err(mdb.txn_commit(txn_handle))
// Get transaction // Get transaction
data_val: mdb.Val get_data_val := mdb.nil_autoval(int)
mdb.panic_on_err(mdb.txn_begin(environment, nil, {}, &txn_handle)) mdb.panic_on_err(mdb.txn_begin(environment, nil, 0, &txn_handle))
mdb.panic_on_err(mdb.get(txn_handle, db_handle, &key_val, &data_val)) mdb.panic_on_err(mdb.get(txn_handle, db_handle, &key_val.raw, &get_data_val.raw))
data_cpy := mdb.blittable_copy(&data_val, int)
mdb.panic_on_err(mdb.txn_commit(txn_handle)) mdb.panic_on_err(mdb.txn_commit(txn_handle))
data_cpy := mdb.autoval_get_data(&get_data_val)^
fmt.println("Get result:", data_cpy) fmt.println("Get result:", data_cpy)
} }
+150 -192
View File
@@ -164,123 +164,24 @@
*/ */
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
filehandle_t :: rawptr
} else when ODIN_OS ==
.Linux || ODIN_OS == .Darwin || ODIN_OS == .FreeBSD || ODIN_OS == .OpenBSD || ODIN_OS == .NetBSD {
foreign import lib "system:lmdb"
mode_t :: posix.mode_t
filehandle_t :: c.int
} else { } else {
#panic("levlib/vendor/lmdb: unsupported OS target") mode_t :: posix.mode_t
}
when ODIN_OS == .Windows {
filehandle_t :: rawptr
} else {
filehandle_t :: c.int
} }
Env :: struct {} Env :: struct {}
@@ -288,7 +189,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 :: c.uint Dbi :: u32
Cursor :: struct {} Cursor :: struct {}
@@ -304,8 +205,33 @@ 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 {
size: uint, /**< size of the data item */ mv_size: uint, /**< size of the data item */
data: rawptr, /**< address of the data item */ mv_data: rawptr, /**< address of the data item */
}
// Automatic `Val` handling for a given type 'T'.
// Will not traverse pointers. If `T` stores pointers, you probably don't want to use this.
Auto_Val :: struct($T: typeid) {
raw: Val,
}
autoval :: #force_inline proc "contextless" (val_ptr: ^$T) -> Auto_Val(T) {
return Auto_Val(T){Val{size_of(T), val_ptr}}
}
nil_autoval :: #force_inline proc "contextless" ($T: typeid) -> Auto_Val(T) {
return Auto_Val(T){Val{size_of(T), nil}}
}
autoval_get_data :: #force_inline proc "contextless" (val: ^Auto_Val($T)) -> ^T {
return cast(^T)val.raw.mv_data
}
// Panic if there is an error
panic_on_err :: #force_inline proc(error: Error) {
if error != .NONE {
fmt.panicf("Irrecoverable LMDB error", strerror(i32(error)))
}
} }
/** @brief A callback function used to compare two keys in a database */ /** @brief A callback function used to compare two keys in a database */
@@ -330,62 +256,82 @@ Rel_Func :: #type proc "c" (item: ^Val, oldptr, newptr, relctx: rawptr)
/** @defgroup mdb_env Environment Flags /** @defgroup mdb_env Environment Flags
* @{ * @{
*/ */
Env_Flag :: enum u32 { /** mmap at a fixed address (experimental) */
FIXEDMAP = 0, /**< mmap at a fixed address (experimental) */ ENV_FIXEDMAP :: 0x01
NOSUBDIR = 14, /**< no environment directory */ /** no environment directory */
NOSYNC = 16, /**< don't fsync after commit */ ENV_NOSUBDIR :: 0x4000
RDONLY = 17, /**< read only */ /** don't fsync after commit */
NOMETASYNC = 18, /**< don't fsync metapage after commit */ ENV_NOSYNC :: 0x10000
WRITEMAP = 19, /**< use writable mmap */ /** read only */
MAPASYNC = 20, /**< use asynchronous msync when WRITEMAP is used */ ENV_RDONLY :: 0x20000
NOTLS = 21, /**< tie reader locktable slots to Txn objects instead of to threads */ /** don't fsync metapage after commit */
NOLOCK = 22, /**< don't do any locking, caller must manage their own locks */ ENV_NOMETASYNC :: 0x40000
NORDAHEAD = 23, /**< don't do readahead (no effect on Windows) */ /** use writable mmap */
NOMEMINIT = 24, /**< don't initialize malloc'd memory before writing to datafile */ ENV_WRITEMAP :: 0x80000
PREVSNAPSHOT = 25, /**< use the previous snapshot rather than the latest one */ /** use asynchronous msync when #MDB_WRITEMAP is used */
} ENV_MAPASYNC :: 0x100000
Env_Flags :: distinct bit_set[Env_Flag;c.uint] /** tie reader locktable slots to #MDB_txn objects instead of to threads */
ENV_NOTLS :: 0x200000
/** don't do any locking, caller must manage their own locks */
ENV_NOLOCK :: 0x400000
/** don't do readahead (no effect on Windows) */
ENV_NORDAHEAD :: 0x800000
/** don't initialize malloc'd memory before writing to datafile */
ENV_NOMEMINIT :: 0x1000000
/** @} */ /** @} */
/** @defgroup mdb_dbi_open Database Flags /** @defgroup mdb_dbi_open Database Flags
* @{ * @{
*/ */
Db_Flag :: enum u32 { /** use reverse string keys */
REVERSEKEY = 1, /**< use reverse string keys */ DB_REVERSEKEY :: 0x02
DUPSORT = 2, /**< use sorted duplicates */ /** use sorted duplicates */
INTEGERKEY = 3, /**< numeric keys in native byte order */ DB_DUPSORT :: 0x04
DUPFIXED = 4, /**< with DUPSORT, sorted dup items have fixed size */ /** numeric keys in native byte order: either unsigned int or size_t.
INTEGERDUP = 5, /**< with DUPSORT, dups are INTEGERKEY-style integers */ * The keys must all be of the same size. */
REVERSEDUP = 6, /**< with DUPSORT, use reverse string dups */ DB_INTEGERKEY :: 0x08
CREATE = 18, /**< create DB if not already existing */ /** with #MDB_DUPSORT, sorted dup items have fixed size */
} DB_DUPFIXED :: 0x10
Db_Flags :: distinct bit_set[Db_Flag;c.uint] /** with #MDB_DUPSORT, dups are #MDB_INTEGERKEY-style integers */
DB_INTEGERDUP :: 0x20
/** with #MDB_DUPSORT, use reverse string dups */
DB_REVERSEDUP :: 0x40
/** create DB if not already existing */
DB_CREATE :: 0x40000
/** @} */ /** @} */
/** @defgroup mdb_put Write Flags /** @defgroup mdb_put Write Flags
* @{ * @{
*/ */
Write_Flag :: enum u32 { /** For put: Don't write if the key already exists. */
NOOVERWRITE = 4, /**< For put: Don't write if the key already exists */ WRITE_NOOVERWRITE :: 0x10
NODUPDATA = 5, /**< For DUPSORT: don't write if the key and data pair already exist. /** Only for #MDB_DUPSORT<br>
For mdb_cursor_del: remove all duplicate data items. */ * For put: don't write if the key and data pair already exist.<br>
CURRENT = 6, /**< For mdb_cursor_put: overwrite the current key/data pair */ * For mdb_cursor_del: remove all duplicate data items.
RESERVE = 16, /**< For put: Just reserve space for data, don't copy it */ */
APPEND = 17, /**< Data is being appended, don't split full pages */ WRITE_NODUPDATA :: 0x20
APPENDDUP = 18, /**< Duplicate data is being appended, don't split full pages */ /** For mdb_cursor_put: overwrite the current key/data pair */
MULTIPLE = 19, /**< Store multiple data items in one call. Only for DUPFIXED. */ WRITE_CURRENT :: 0x40
} /** For put: Just reserve space for data, don't copy it. Return a
Write_Flags :: distinct bit_set[Write_Flag;c.uint] * pointer to the reserved space.
/** @} */ */
WRITE_RESERVE :: 0x10000
/** Data is being appended, don't split full pages. */
WRITE_APPEND :: 0x20000
/** Duplicate data is being appended, don't split full pages. */
WRITE_APPENDDUP :: 0x40000
/** Store multiple data items in one call. Only for #MDB_DUPFIXED. */
WRITE_MULTIPLE :: 0x80000
/* @} */
/** @defgroup mdb_copy Copy Flags /** @defgroup mdb_copy Copy Flags
* @{ * @{
*/ */
Copy_Flag :: enum u32 { /** Compacting copy: Omit free space from copy, and renumber all
COMPACT = 0, /**< Compacting copy: Omit free space from copy, and renumber all pages sequentially. */ * pages sequentially.
} */
Copy_Flags :: distinct bit_set[Copy_Flag;c.uint] CP_COMPACT :: 0x01
/** @} */ /* @} */
/** @brief Cursor Get operations. /** @brief Cursor Get operations.
* *
@@ -394,24 +340,33 @@ Copy_Flags :: distinct bit_set[Copy_Flag;c.uint]
*/ */
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. Only for DUPSORT */ FIRST_DUP, /**< Position at first data item of current key.
GET_BOTH, /**< Position at key/data pair. Only for DUPSORT */ Only for #MDB_DUPSORT */
GET_BOTH_RANGE, /**< Position at key, nearest data. Only for DUPSORT */ GET_BOTH, /**< Position at key/data pair. Only for #MDB_DUPSORT */
GET_BOTH_RANGE, /**< position at key, nearest data. Only for #MDB_DUPSORT */
GET_CURRENT, /**< Return key/data at current cursor position */ GET_CURRENT, /**< Return key/data at current cursor position */
GET_MULTIPLE, /**< Return up to a page of duplicate data items from current cursor position. Only for DUPFIXED */ GET_MULTIPLE, /**< Return up to a page of duplicate data items
from current cursor position. Move cursor to prepare
for #MDB_NEXT_MULTIPLE. Only for #MDB_DUPFIXED */
LAST, /**< Position at last key/data item */ LAST, /**< Position at last key/data item */
LAST_DUP, /**< Position at last data item of current key. Only for DUPSORT */ LAST_DUP, /**< Position at last data item of current key.
Only for #MDB_DUPSORT */
NEXT, /**< Position at next data item */ NEXT, /**< Position at next data item */
NEXT_DUP, /**< Position at next data item of current key. Only for DUPSORT */ NEXT_DUP, /**< Position at next data item of current key.
NEXT_MULTIPLE, /**< Return up to a page of duplicate data items from next cursor position. Only for DUPFIXED */ Only for #MDB_DUPSORT */
NEXT_MULTIPLE, /**< Return up to a page of duplicate data items
from next cursor position. Move cursor to prepare
for #MDB_NEXT_MULTIPLE. Only for #MDB_DUPFIXED */
NEXT_NODUP, /**< Position at first data item of next key */ 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. Only for DUPSORT */ PREV_DUP, /**< Position at previous data item of current key.
Only for #MDB_DUPSORT */
PREV_NODUP, /**< Position at last data item of previous key */ 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 a page of duplicate data items. Only for DUPFIXED */ PREV_MULTIPLE, /**< Position at previous page and return up to
a page of duplicate data items. Only for #MDB_DUPFIXED */
} }
Error :: enum c.int { Error :: enum c.int {
@@ -464,28 +419,33 @@ 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 {
psize: u32, /**< Size of a database page. This is currently the same for all databases. */ ms_psize: u32,
depth: u32, /**< Depth (height) of the B-tree */ /**< Size of a database page.
branch_pages: uint, /**< Number of internal (non-leaf) pages */ This is currently the same for all databases. */
leaf_pages: uint, /**< Number of leaf pages */ ms_depth: u32,
overflow_pages: uint, /**< Number of overflow pages */ /**< Depth (height) of the B-tree */
entries: uint, /**< Number of data items */ ms_branch_pages: uint,
/**< Number of internal (non-leaf) pages */
ms_leaf_pages: uint,
/**< Number of leaf pages */
ms_overflow_pages: uint,
/**< Number of overflow pages */
ms_entries: uint,
/**< Number of data items */
} }
/** @brief Information about the environment */ /** @brief Information about the environment */
Env_Info :: struct { Env_Info :: struct {
mapaddr: rawptr, /**< Address of map, if fixed */ me_mapaddr: rawptr, /**< Address of map, if fixed */
mapsize: uint, /**< Size of the data memory map */ me_mapsize: uint, /**< Size of the data memory map */
last_pgno: uint, /**< ID of the last used page */ me_last_pgno: uint, /**< ID of the last used page */
last_txnid: uint, /**< ID of the last committed transaction */ me_last_txnid: uint, /**< ID of the last committed transaction */
maxreaders: u32, /**< max reader slots in the environment */ me_maxreaders: u32, /**< max reader slots in the environment */
numreaders: u32, /**< max reader slots used in the environment */ me_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,
@@ -494,7 +454,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 :: #type proc "c" (_: ^Env, _: cstring) Assert_Func :: 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.
* *
@@ -502,7 +462,7 @@ Assert_Func :: #type 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 :: #type proc "c" (_: cstring, _: rawptr) -> i32 Msg_Func :: proc "c" (_: cstring, _: rawptr) -> i32
@(default_calling_convention = "c", link_prefix = "mdb_") @(default_calling_convention = "c", link_prefix = "mdb_")
foreign lib { foreign lib {
@@ -663,7 +623,7 @@ foreign lib {
* </ul> * </ul>
*/ */
@(require_results) @(require_results)
env_open :: proc(env: ^Env, path: cstring, flags: Env_Flags, mode: mode_t) -> Error --- env_open :: proc(env: ^Env, path: cstring, flags: u32, mode: mode_t) -> Error ---
/** @brief Copy an LMDB environment to the specified path. /** @brief Copy an LMDB environment to the specified path.
* *
@@ -722,7 +682,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: Copy_Flags) -> Error --- env_copy2 :: proc(env: ^Env, path: cstring, flags: u32) -> 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.
@@ -742,7 +702,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: Copy_Flags) -> Error --- env_copyfd2 :: proc(env: ^Env, fd: filehandle_t, flags: u32) -> Error ---
/** @brief Return statistics about the LMDB environment. /** @brief Return statistics about the LMDB environment.
* *
@@ -807,7 +767,7 @@ foreign lib {
* </ul> * </ul>
*/ */
@(require_results) @(require_results)
env_set_flags :: proc(env: ^Env, flags: Env_Flags, onoff: i32) -> Error --- env_set_flags :: proc(env: ^Env, flags: u32, onoff: i32) -> Error ---
/** @brief Get environment flags. /** @brief Get environment flags.
* *
@@ -820,7 +780,7 @@ foreign lib {
* </ul> * </ul>
*/ */
@(require_results) @(require_results)
env_get_flags :: proc(env: ^Env, flags: ^Env_Flags) -> Error --- env_get_flags :: proc(env: ^Env, flags: ^u32) -> Error ---
/** @brief Return the path that was used in #mdb_env_open(). /** @brief Return the path that was used in #mdb_env_open().
* *
@@ -1013,7 +973,7 @@ foreign lib {
* </ul> * </ul>
*/ */
@(require_results) @(require_results)
txn_begin :: proc(env: ^Env, parent: ^Txn, flags: Env_Flags, txn: ^^Txn) -> Error --- txn_begin :: proc(env: ^Env, parent: ^Txn, flags: u32, txn: ^^Txn) -> Error ---
/** @brief Returns the transaction's #MDB_env /** @brief Returns the transaction's #MDB_env
* *
@@ -1166,7 +1126,7 @@ foreign lib {
* </ul> * </ul>
*/ */
@(require_results) @(require_results)
dbi_open :: proc(txn: ^Txn, name: cstring, flags: Db_Flags, dbi: ^Dbi) -> Error --- dbi_open :: proc(txn: ^Txn, name: cstring, flags: u32, dbi: ^Dbi) -> Error ---
/** @brief Retrieve statistics for a database. /** @brief Retrieve statistics for a database.
* *
@@ -1191,7 +1151,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: ^Db_Flags) -> Error --- dbi_flags :: proc(txn: ^Txn, dbi: Dbi, flags: ^u32) -> Error ---
/** @brief Close a database handle. Normally unnecessary. Use with care: /** @brief Close a database handle. Normally unnecessary. Use with care:
* *
@@ -1269,7 +1229,6 @@ 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
@@ -1291,7 +1250,6 @@ 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.
@@ -1386,7 +1344,7 @@ foreign lib {
* </ul> * </ul>
*/ */
@(require_results) @(require_results)
put :: proc(txn: ^Txn, dbi: Dbi, key: ^Val, data: ^Val, flags: Write_Flags) -> Error --- put :: proc(txn: ^Txn, dbi: Dbi, key: ^Val, data: ^Val, flags: u32) -> Error ---
/** @brief Delete items from a database. /** @brief Delete items from a database.
* *
@@ -1559,7 +1517,7 @@ foreign lib {
* </ul> * </ul>
*/ */
@(require_results) @(require_results)
cursor_put :: proc(cursor: ^Cursor, key: ^Val, data: ^Val, flags: Write_Flags) -> Error --- cursor_put :: proc(cursor: ^Cursor, key: ^Val, data: ^Val, flags: u32) -> Error ---
/** @brief Delete current key/data pair /** @brief Delete current key/data pair
* *
@@ -1583,7 +1541,7 @@ foreign lib {
* </ul> * </ul>
*/ */
@(require_results) @(require_results)
cursor_del :: proc(cursor: ^Cursor, flags: Write_Flags) -> Error --- cursor_del :: proc(cursor: ^Cursor, flags: u32) -> Error ---
/** @brief Return count of duplicates for current key. /** @brief Return count of duplicates for current key.
* *