Compare commits
4 Commits
master
..
dd1329e2af
| Author | SHA1 | Date | |
|---|---|---|---|
| dd1329e2af | |||
| a0552febf6 | |||
| 6a82b4b733 | |||
| 6a97884067 |
@@ -75,21 +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 clay-borders example",
|
|
||||||
"command": "odin run draw/examples -debug -out=out/debug/draw-examples -- clay-borders",
|
|
||||||
"cwd": "$ZED_WORKTREE_ROOT",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Run draw gaussian-blur example",
|
|
||||||
"command": "odin run draw/examples -debug -out=out/debug/draw-examples -- gaussian-blur",
|
|
||||||
"cwd": "$ZED_WORKTREE_ROOT",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Run draw gaussian-blur-debug example",
|
|
||||||
"command": "odin run draw/examples -debug -out=out/debug/draw-examples -- gaussian-blur-debug",
|
|
||||||
"cwd": "$ZED_WORKTREE_ROOT",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"label": "Run qrcode basic example",
|
"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",
|
||||||
|
|||||||
+234
-437
@@ -5,73 +5,54 @@ Clay UI integration.
|
|||||||
|
|
||||||
## Current state
|
## Current state
|
||||||
|
|
||||||
The renderer uses a single unified `Core_2D` (`TRIANGLELIST` pipeline) with three submission
|
The renderer uses a single unified `Pipeline_2D_Base` (`TRIANGLELIST` pipeline) with two submission
|
||||||
modes dispatched by a push constant. The split is by **vertex coordinate space**, not by what the
|
modes dispatched by a push constant:
|
||||||
fragment shader does — modes 0 and 2 share the same fragment-shader path (kind 0) and differ only
|
|
||||||
in whether the vertex shader applies `dpi_scale` to incoming positions:
|
|
||||||
|
|
||||||
- **Mode 0 (Tessellated):** Vertex buffer contains real geometry in _logical_ pixels. The vertex
|
- **Mode 0 (Tessellated):** Vertex buffer contains real geometry. Used for text (indexed draws into
|
||||||
shader scales by `dpi_scale` before projecting. Used for single-pixel points (`tess.pixel`),
|
SDL_ttf atlas textures), single-pixel points (`tes_pixel`), arbitrary user geometry (`tes_triangle`,
|
||||||
arbitrary user geometry (`tess.triangle`, `tess.triangle_aa`, `tess.triangle_lines`,
|
`tes_triangle_fan`, `tes_triangle_strip`), and shapes without a closed-form rounded-rectangle
|
||||||
`tess.triangle_fan`, `tess.triangle_strip`), and any raw vertex geometry submitted via
|
reduction: ellipses (`tes_ellipse`), regular polygons (`tes_polygon`), and circle sectors
|
||||||
`prepare_shape`. The fragment shader premultiplies the texture sample (`t.rgb *= t.a`) and
|
(`tes_sector`). The fragment shader computes `out = color * texture(tex, uv)`.
|
||||||
computes `out = color * t`.
|
|
||||||
|
|
||||||
- **Mode 2 (Text):** Vertex buffer contains real geometry in _physical_ pixels. SDL_ttf's GPU text
|
|
||||||
engine lays out glyphs in physical pixels (`TTF_SetFontSizeDPI` is called with `72 * dpi_scale`),
|
|
||||||
so `prepare_text` adds an anchor offset that is itself snapped to integer physical pixels for
|
|
||||||
atlas-aligned bilinear sampling, then writes vertices straight to the buffer. The vertex shader
|
|
||||||
must NOT rescale these vertices. Same fragment-shader kind as Tessellated; same indexed draws
|
|
||||||
into SDL_ttf atlas textures; the only difference is the coordinate space of the input. Mode 2
|
|
||||||
exists because integer-physical-pixel snapping is the load-bearing property of crisp glyph
|
|
||||||
rendering and CPU is the only place that snap can happen once-per-text-element instead of
|
|
||||||
per-vertex.
|
|
||||||
|
|
||||||
- **Mode 1 (SDF):** A static 6-vertex unit-quad buffer is drawn instanced, with per-primitive
|
- **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 (80 bytes each) uploaded each frame to a GPU storage buffer. The vertex shader
|
||||||
shader reads `primitives[gl_InstanceIndex]`, computes world-space position from unit quad corners +
|
reads `primitives[gl_InstanceIndex]`, computes world-space position from unit quad corners +
|
||||||
primitive bounds. The fragment shader dispatches on `Shape_Kind` (encoded in the low byte of
|
primitive bounds. The fragment shader always evaluates `sdRoundedBox` — there is no per-primitive
|
||||||
`Core_2D_Primitive.flags`) to evaluate one of four signed distance functions:
|
kind dispatch.
|
||||||
- **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
|
The SDF path handles all shapes that are algebraically reducible to a rounded rectangle:
|
||||||
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
|
- **Rounded rectangles** — per-corner radii via `sdRoundedBox` (iq). Covers filled, stroked,
|
||||||
own 16-byte slots in `Core_2D_Primitive`, so a primitive can carry texture and outline simultaneously.
|
textured, and gradient-filled rectangles.
|
||||||
Gradient and texture remain mutually exclusive at the fill-source level (a Brush variant chooses one
|
- **Circles** — uniform radii equal to half-size. Covers filled, stroked, and radial-gradient circles.
|
||||||
or the other) since they share the worst-case fragment-shader register path.
|
- **Line segments / capsules** — rotated RRect with uniform radii equal to half-thickness (stadium shape).
|
||||||
|
- **Full rings / annuli** — stroked circle (mid-radius with stroke thickness = outer - inner).
|
||||||
|
|
||||||
|
All SDF shapes support fill, stroke, solid color, bilinear 4-corner gradients, radial 2-color
|
||||||
|
gradients, and texture fills via `Shape_Flags`. Gradient colors are packed into the same 16 bytes as
|
||||||
|
the texture UV rect via a `Uv_Or_Gradient` raw union — zero size increase to the 80-byte `Primitive`
|
||||||
|
struct. Gradient and texture are mutually exclusive.
|
||||||
|
|
||||||
All SDF shapes produce mathematically exact curves with analytical anti-aliasing via `smoothstep` —
|
All SDF shapes produce mathematically exact curves with analytical anti-aliasing via `smoothstep` —
|
||||||
no tessellation, no piecewise-linear approximation. A rounded rectangle is 1 primitive (96 bytes)
|
no tessellation, no piecewise-linear approximation. A rounded rectangle is 1 primitive (80 bytes)
|
||||||
instead of ~250 vertices (~5000 bytes).
|
instead of ~250 vertices (~5000 bytes).
|
||||||
|
|
||||||
The main pipeline's register budget is **≤24 registers** (see "Main/effects split: register pressure"
|
The fragment shader's estimated register footprint is ~20–23 VGPRs via static live-range analysis.
|
||||||
in the pipeline plan below for the full cliff/margin analysis and SBC architecture context).
|
RRect and Ring_Arc are roughly tied at peak pressure — RRect carries `corner_radii` (4 regs) plus
|
||||||
The fragment shader's estimated peak footprint is ~22–26 fp32 VGPRs (~16–22 fp16 VGPRs on architectures
|
`sdRoundedBox` temporaries, Ring_Arc carries wedge normals plus dot-product temporaries. Both land
|
||||||
with native mediump) via manual live-range analysis. The dominant peak is the Ring_Arc kind path
|
comfortably under Mali Valhall's 32-register occupancy cliff (G57/G77/G78 and later) and well under
|
||||||
(wedge normals + inner/outer radii + dot-product temporaries live simultaneously with carried state
|
desktop limits. On older Bifrost Mali (G71/G72/G76, 16-register cliff) either shape kind may incur
|
||||||
like `f_color`, `f_uv_rect`/`f_effects`, and `half_size_ppx`). RRect is 1–2 regs lower
|
partial occupancy reduction. These estimates are hand-counted; exact numbers require `malioc` or
|
||||||
(`corner_radii_ppx` vec4 replaces the separate inner/outer + normal pairs). NGon and Ellipse are lighter still. Real compilers
|
Radeon GPU Analyzer against the compiled SPIR-V.
|
||||||
apply live-range coalescing, mediump-to-fp16 promotion, and rematerialization that typically shave
|
|
||||||
2–4 regs from hand-counted estimates — the conservative 26-reg upper bound is expected to compile
|
|
||||||
down to within the 24-register budget, but this must be verified with `malioc` (see "Verifying
|
|
||||||
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
|
All public drawing procs use prefixed names for clarity: `sdf_*` for SDF-path shapes, `tes_*` for
|
||||||
MSAA's per-tile bandwidth multiplier is expensive).
|
tessellated-path shapes. Proc groups provide a single entry point per shape concept (e.g.,
|
||||||
|
`sdf_rectangle` dispatches to `sdf_rectangle_solid` or `sdf_rectangle_gradient` based on argument
|
||||||
|
count).
|
||||||
|
|
||||||
## 2D rendering pipeline plan
|
## 2D rendering pipeline plan
|
||||||
|
|
||||||
@@ -85,23 +66,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 (~18–24 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 (~48–60 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 (~15–40 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
|
||||||
@@ -117,113 +97,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**.
|
| ~16 regs (main pipeline) | 4,096 | 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; ~2016–2018): 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 |
|
| ~16 regs (main pipeline) | 4,096 | 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 ±2–3 registers between versions on unchanged source.
|
| -------------------- | -------------------------- |
|
||||||
2. **Feature additions.** Each new effect, flag, or uniform adds 1–4 live registers. A new gradient
|
| 0–32 regs (main) | 100% (full thread count) |
|
||||||
mode or outline option lands in this range.
|
| 33–64 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 4–8 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 (89–100%); 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 (~5–10% 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
|
||||||
50–67% occupancy on effected shapes. This is acceptable for the small fraction of frame fragments
|
12–24 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
|
||||||
|
|
||||||
@@ -233,11 +156,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 (~15–40 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)
|
||||||
|
|
||||||
@@ -266,9 +188,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 1–50ms to compile on Metal/Vulkan/D3D12 at
|
**PSO compilation costs multiply.** Each pipeline takes 1–50ms 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
|
||||||
@@ -349,23 +271,18 @@ 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. **`flags` (flat varying from storage buffer): gradient/texture/stroke mode.** 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 flag bits. However, since the SDF path now evaluates only `sdRoundedBox` with no
|
||||||
kind value. The fragment shader's `if/else if` chain selects the appropriate SDF function (~15–30
|
kind dispatch, the only flag-dependent branches are gradient vs. texture vs. solid color selection
|
||||||
instructions per kind). Divergence occurs only at primitive boundaries where adjacent quads have
|
— all lightweight (3–8 instructions per path). Divergence at primitive boundaries between
|
||||||
different kinds.
|
different flag combinations has negligible cost.
|
||||||
|
|
||||||
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 (3–8 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:
|
||||||
|
|
||||||
@@ -382,12 +299,11 @@ For category 3, the divergence analysis depends on primitive size:
|
|||||||
frame-level divergence is typically **1–3%** of all warps.
|
frame-level divergence is typically **1–3%** of all warps.
|
||||||
|
|
||||||
At 1–3% divergence, the throughput impact is negligible. At 4K with 12.4M total fragments
|
At 1–3% 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. Without kind dispatch, the
|
||||||
is Ring_Arc (~30 instructions); when a divergent warp straddles two different kinds, it pays the cost
|
longest untaken branch is the gradient evaluation (~8 instructions), not a different SDF function.
|
||||||
of both (~45–60 instructions total). Each divergent warp's extra cost is modest — at ~12G
|
Each divergent warp pays at most ~8 extra instructions. At ~12G instructions/sec on a mid-range GPU,
|
||||||
instructions/sec on a mid-range GPU, even 3,000 divergent warps × 60 extra instructions totals
|
that totals ~1.3μs — under 0.02% of an 8.3ms (120 FPS) frame budget. This is
|
||||||
~15μs, under 0.2% of an 8.3ms (120 FPS) frame budget. This is confirmed by production renderers
|
confirmed by production renderers that use exactly this pattern:
|
||||||
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
|
||||||
@@ -411,10 +327,10 @@ 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 (~15–30 instructions
|
divergent warps pay double a large cost. Without kind dispatch, the SDF path always evaluates
|
||||||
each), and the gradient/texture/solid color selection branches are shorter still (3–8 instructions
|
`sdRoundedBox`; the only branches are gradient/texture/solid color selection at 3–8 instructions
|
||||||
each). Even fully divergent, the combined penalty is ~30–60 extra instructions — comparable to a
|
each. Even fully divergent, the penalty is ~8 extra instructions — less than a single texture
|
||||||
single texture sample's latency.
|
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
|
||||||
@@ -422,10 +338,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, the SDF
|
||||||
SDF kind branches and flag-based color selection cluster at ~22–26 registers (see register
|
path has a single evaluation (sdRoundedBox) with flag-based color selection, clustering at ~15–18
|
||||||
analysis in Current state), within the ≤24-register budget that guarantees full occupancy on
|
registers, so there is negligible occupancy loss.
|
||||||
Valhall and all desktop architectures. See Known limitations for V3D / Bifrost.
|
|
||||||
|
|
||||||
**References:**
|
**References:**
|
||||||
|
|
||||||
@@ -445,40 +360,28 @@ our design:
|
|||||||
|
|
||||||
### Main pipeline: SDF + tessellated (unified)
|
### Main pipeline: SDF + tessellated (unified)
|
||||||
|
|
||||||
The main pipeline serves three submission modes through a single `TRIANGLELIST` pipeline and a
|
The main pipeline serves two submission modes through a single `TRIANGLELIST` pipeline and a single
|
||||||
single vertex input layout, distinguished by a `mode` field in the `Vertex_Uniforms_2D` push
|
vertex input layout, distinguished by a mode marker in the `Primitive.flags` field (low byte:
|
||||||
constant (`Core_2D_Mode.Tessellated = 0`, `Core_2D_Mode.SDF = 1`, `Core_2D_Mode.Text = 2`), pushed
|
0 = tessellated, 1 = SDF). The tessellated path sets this to 0 via zero-initialization in the vertex
|
||||||
per draw call via `push_globals`. The vertex shader branches on this uniform to select the
|
shader; the SDF path sets it to 1 via `pack_flags`.
|
||||||
appropriate code path.
|
|
||||||
|
|
||||||
- **Tessellated mode** (`mode = 0`): direct vertex buffer with explicit geometry in _logical_
|
- **Tessellated mode** (`mode = 0`): direct vertex buffer with explicit geometry. Used for text
|
||||||
pixels. Vertex shader scales positions by `dpi_scale`. Used for triangles, triangle fans/strips,
|
(SDL_ttf atlas sampling), triangle fans/strips, ellipses, regular polygons, circle sectors, and
|
||||||
single-pixel points, and any user-provided raw vertex geometry.
|
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. `Core_2D_Primitive.bounds` is in logical pixels; the vertex shader scales by
|
|
||||||
`dpi_scale`.
|
|
||||||
- **Text mode** (`mode = 2`): direct vertex buffer with explicit geometry in _physical_ pixels.
|
|
||||||
Vertex shader does NOT scale. Used for SDL_ttf atlas sampling. The CPU-side anchor snap to
|
|
||||||
integer physical pixels (`prepare_text`/`prepare_text_transformed`) is what produces crisp glyphs
|
|
||||||
— sub-pixel anchors blur via the bilinear sampler. Mode 2 shares the fragment-shader path with
|
|
||||||
Tessellated (kind 0), so the only divergence between text and shape rasterization is the vertex
|
|
||||||
shader's `* dpi_scale` step.
|
|
||||||
|
|
||||||
All three modes use the same fragment shader. Modes 0 (Tessellated) and 2 (Text) take the same
|
Both modes use the same fragment shader. The fragment shader checks the mode marker: mode 0 computes
|
||||||
fragment-shader path (kind 0), which premultiplies the texture sample and computes `out = color * t`;
|
`out = color * texture(tex, uv)`; mode 1 always evaluates `sdRoundedBox` and applies
|
||||||
they differ only in the vertex shader (whether positions are pre-scaled to physical pixels). Mode 1
|
gradient/texture/solid color based on flag bits.
|
||||||
(SDF) checks `Shape_Kind` (low byte of `Core_2D_Primitive.flags`): kinds 1–4 dispatch to one of four
|
|
||||||
SDF functions (RRect, NGon, Ellipse, Ring_Arc) and apply gradient/texture/outline/solid color based
|
|
||||||
on `Shape_Flags` bits.
|
|
||||||
|
|
||||||
#### Why SDF for shapes
|
#### 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
|
||||||
@@ -509,55 +412,60 @@ 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 (80 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 4–6× data duplication across quad corners. A rounded rectangle costs
|
buffer instancing eliminates the 4–6× data duplication across quad corners. A rounded rectangle costs
|
||||||
96 bytes instead of 4 vertices × 60+ bytes = 240+ bytes.
|
80 bytes instead of 4 vertices × 40+ bytes = 160+ bytes.
|
||||||
|
|
||||||
The tessellated and text paths retain the existing direct vertex buffer layout (20 bytes/vertex, no
|
The tessellated path retains the existing direct vertex buffer layout (20 bytes/vertex, no storage
|
||||||
storage buffer access). The vertex shader branch on `mode` (push constant) is warp-uniform — every
|
buffer access). The vertex shader branch on `mode` (push constant) is warp-uniform — every invocation
|
||||||
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 folding
|
||||||
|
|
||||||
The fragment shader dispatches on `Shape_Kind` (low byte of `Core_2D_Primitive.flags`) to evaluate
|
The SDF path evaluates a single function — `sdRoundedBox` — for all primitives. There is no
|
||||||
one of four signed distance functions. The `Shape_Kind` enum, per-kind `*_Params` structs, and
|
`Shape_Kind` enum or per-primitive kind dispatch in the fragment shader. Shapes that are algebraically
|
||||||
CPU-side drawing procs all live in `core_2d.odin`. The drawing procs build the appropriate
|
special cases of a rounded rectangle are emitted as RRect primitives by the CPU-side drawing procs:
|
||||||
`Core_2D_Primitive` and set the kind automatically:
|
|
||||||
|
|
||||||
Each user-facing shape proc accepts a `Brush` union (color, linear gradient, radial gradient,
|
| User-facing shape | RRect mapping | Notes |
|
||||||
or textured fill) as its fill source, plus optional outline parameters. The procs map to SDF
|
| ---------------------------- | -------------------------------------------- | ---------------------------------------- |
|
||||||
kinds as follows:
|
| Rectangle (sharp or rounded) | Direct | Per-corner radii from `radii` param |
|
||||||
|
| Circle | `half_size = (r, r)`, `radii = (r, r, r, r)` | Uniform radii = half-size |
|
||||||
|
| Line segment / capsule | Rotated RRect, `radii = half_thickness` | Stadium shape (fully-rounded minor axis) |
|
||||||
|
| Full ring / annulus | Stroked circle at mid-radius | `stroke_px = outer - inner` |
|
||||||
|
|
||||||
| User-facing proc | Shape_Kind | SDF function | Notes |
|
Shapes without a closed-form RRect reduction are drawn via the tessellated path:
|
||||||
| -------------------- | ---------- | ------------------ | ---------------------------------------------------------- |
|
|
||||||
| `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,
|
| Shape | Tessellated proc | Method |
|
||||||
arc geometry). See the `Shape_Flag` enum in `core_2d.odin` for the authoritative flag
|
| ------------------------- | ---------------------------------- | -------------------------- |
|
||||||
definitions and bit assignments.
|
| Ellipse | `tes_ellipse`, `tes_ellipse_lines` | Triangle fan approximation |
|
||||||
|
| Regular polygon (N-gon) | `tes_polygon`, `tes_polygon_lines` | Triangle fan from center |
|
||||||
|
| Circle sector (pie slice) | `tes_sector` | Triangle fan arc |
|
||||||
|
|
||||||
|
The `Shape_Flags` bit set controls rendering mode per primitive:
|
||||||
|
|
||||||
|
| Flag | Bit | Effect |
|
||||||
|
| ----------------- | --- | -------------------------------------------------------------------- |
|
||||||
|
| `Stroke` | 0 | Outline instead of fill (`d = abs(d) - stroke_width/2`) |
|
||||||
|
| `Textured` | 1 | Sample texture using `uv.uv_rect` (mutually exclusive with Gradient) |
|
||||||
|
| `Gradient` | 2 | Bilinear 4-corner interpolation from `uv.corner_colors` |
|
||||||
|
| `Gradient_Radial` | 3 | Radial 2-color falloff (inner/outer) from `uv.corner_colors[0..1]` |
|
||||||
|
|
||||||
**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)
|
- Ellipses (`tes_ellipse`, `tes_ellipse_lines`)
|
||||||
- `tess.triangle`, `tess.triangle_aa`, `tess.triangle_lines` (single triangles)
|
- Regular polygons (`tes_polygon`, `tes_polygon_lines`)
|
||||||
- `tess.triangle_fan`, `tess.triangle_strip` (arbitrary user-provided geometry)
|
- Circle sectors / pie slices (`tes_sector`)
|
||||||
|
- `tes_triangle`, `tes_triangle_fan`, `tes_triangle_strip` (arbitrary user-provided geometry)
|
||||||
- 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 design rule: if the shape reduces to `sdRoundedBox`, it goes SDF. If it requires a different SDF
|
||||||
`Shape_Kind`. If it is described by a vertex list or has no practical SDF, it stays tessellated.
|
function or is described by a vertex list, it stays tessellated.
|
||||||
|
|
||||||
### Effects pipeline
|
### Effects pipeline
|
||||||
|
|
||||||
@@ -618,153 +526,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 (~15–40 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_ppx: 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
|
||||||
@@ -776,30 +575,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
|
flags: u32, // 20: low byte = Shape_Kind, bits 8+ = Shape_Flags
|
||||||
rotation_sc: u32, // 24: packed f16 pair (sin, cos). Requires .Rotated flag.
|
rotation_sc: u32, // 24: packed f16 pair (sin, cos). Requires .Rotated flag.
|
||||||
_pad: f32, // 28: reserved for future use
|
_pad: f32, // 28: reserved for future use
|
||||||
params: Shape_Params, // 32: per-kind params union (half_feather_ppx, radii_ppx, etc.) (32 bytes)
|
params: Shape_Params, // 32: per-kind params union (half_feather, radii, etc.) (32 bytes)
|
||||||
uv_rect: [4]f32, // 64: texture UV coordinates. Read when .Textured.
|
uv: Uv_Or_Effects, // 64: texture UV rect or gradient/outline parameters (16 bytes)
|
||||||
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
|
`RRect_Params` holds the rounded-rectangle parameters directly — there is no `Shape_Params` union.
|
||||||
`Ring_Arc_Params` (plus a `raw: [8]f32` view), defined in `core_2d.odin`. Each SDF kind
|
`Uv_Or_Gradient` is a `#raw_union` that aliases `[4]f32` (texture UV rect) with `[4]Color` (gradient
|
||||||
writes its own params variant; the fragment shader reads the appropriate fields based on `Shape_Kind`.
|
corner colors, clockwise from top-left: TL, TR, BR, BL). The `flags` field encodes both the
|
||||||
`Gradient_Outline` is a 16-byte struct containing `gradient_color: Color`, `outline_color: Color`,
|
tessellated/SDF mode marker (low byte) and shape flags (bits 8+) via `pack_flags`.
|
||||||
`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
|
||||||
|
|
||||||
@@ -823,16 +617,17 @@ pair into bitmap atlases and emits indexed triangle data via `GetGPUTextDrawData
|
|||||||
**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 `mode = 0`, 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 MSDF glyph mode in the fragment shader, which would require reintroducing a mode/kind
|
||||||
already exists for the four current SDF kinds).
|
distinction (the current shader evaluates only `sdRoundedBox` with no kind dispatch).
|
||||||
- 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:**
|
||||||
|
|
||||||
@@ -846,8 +641,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
|
||||||
|
|
||||||
@@ -897,30 +692,31 @@ 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 `sdf_rectangle_texture` and
|
||||||
union as its fill source; passing a `Texture_Fill` value (carrying `Texture_Id`, `tint`,
|
`sdf_rectangle_texture_corners`, mirroring `sdf_rectangle` and `sdf_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 proc for "simple" fullscreen
|
||||||
the tessellated path's lower register count would improve occupancy at large fragment counts.
|
quads, on the theory that the tessellated path's lower register count (~16 regs vs ~18 for the SDF
|
||||||
Both paths are well within the ≤24-register main pipeline budget — both run at full
|
textured branch) would improve occupancy at large fragment counts. Applying the register-pressure
|
||||||
occupancy on every target architecture (Valhall and above). The remaining ALU difference
|
analysis from the pipeline-strategy section above shows this is wrong: both 16 and 18 registers are
|
||||||
(~15 extra instructions for the SDF evaluation) amounts to ~20μs at 4K — below noise.
|
well below the register cliff (~43 regs on consumer Ampere/Ada, ~32 on Volta/A100), so both run at
|
||||||
Meanwhile, splitting into a separate pipeline would add ~1–5μs per pipeline bind on the CPU
|
100% occupancy. The remaining ALU difference (~15 extra instructions for the SDF evaluation) amounts
|
||||||
side per scissor, matching or exceeding the GPU-side savings. Within the main pipeline,
|
to ~20μs at 4K — below noise. Meanwhile, splitting into a separate pipeline would add ~1–5μs per
|
||||||
unified remains strictly better.
|
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 uses `sdf_` and `tes_` prefixes to indicate the rendering path, with suffixes
|
||||||
`ellipse`, `polygon`, `ring`, `line`, `line_strip`). Gradients, textures, and outlines are
|
for modifiers: `sdf_rectangle_texture` and `sdf_rectangle_texture_corners` sit alongside
|
||||||
selected via the `Brush` union and optional outline parameters rather than separate overloads.
|
`sdf_rectangle` (solid or gradient overload). Proc groups like `sdf_rectangle` dispatch to
|
||||||
|
`sdf_rectangle_solid` or `sdf_rectangle_gradient` based on argument count. Future per-shape texture
|
||||||
|
variants (`sdf_circle_texture`) 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
|
||||||
@@ -929,8 +725,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.
|
||||||
@@ -954,13 +750,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 `sdf_rectangle_texture` (SDF, sharp
|
||||||
called with a `Texture_Fill` brush and the appropriate radii (zero for sharp corners, per-corner
|
corners), nonzero dispatches to `sdf_rectangle_texture_corners` (SDF, per-corner radii). A
|
||||||
values from Clay's `cornerRadius` otherwise).
|
`fit_params` call computes UVs from the fit 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.
|
||||||
@@ -968,6 +764,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**: `sdf_circle_texture`, `tes_ellipse_texture`, `tes_polygon_texture` — potential future additions, reserved by naming convention.
|
||||||
|
|
||||||
**References:**
|
**References:**
|
||||||
|
|
||||||
|
|||||||
-1185
File diff suppressed because it is too large
Load Diff
-794
@@ -1,794 +0,0 @@
|
|||||||
// Clay UI integration for the `draw` package.
|
|
||||||
//
|
|
||||||
// All code in this file is dedicated to bridging Clay's render command stream into `draw`'s
|
|
||||||
// primitive/sub-batch pipeline. Nothing outside this file should reference the `clay` package
|
|
||||||
// directly; everything Clay-related (types, lifecycle helpers, render-command dispatch, the
|
|
||||||
// border-merge stack, the Clay backdrop bracket walker, the text measure/error callbacks,
|
|
||||||
// and the `Clay_Image_Data` user-facing helper) lives here. `draw.odin`'s lifecycle procs
|
|
||||||
// call `init_clay`, `destroy_clay`, and `clear_clay_per_frame` to drive the bits of state
|
|
||||||
// that necessarily live on the shared `Global` struct.
|
|
||||||
package draw
|
|
||||||
|
|
||||||
import "base:runtime"
|
|
||||||
import "core:c"
|
|
||||||
import "core:log"
|
|
||||||
import "core:strings"
|
|
||||||
import sdl "vendor:sdl3"
|
|
||||||
import sdl_ttf "vendor:sdl3/ttf"
|
|
||||||
|
|
||||||
import clay "../vendor/clay"
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
|
||||||
// ----- Lifecycle ------------
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// Allocate the Clay arena, build the merge-candidate stack, hand the arena to Clay, and
|
|
||||||
// register the text-measurement and error callbacks. Called by `init` once `GLOB` has been
|
|
||||||
// populated with the device/window state Clay's callbacks read from.
|
|
||||||
//INTERNAL
|
|
||||||
init_clay :: proc(window: ^sdl.Window, allocator: runtime.Allocator) {
|
|
||||||
min_memory_size: c.size_t = cast(c.size_t)clay.MinMemorySize()
|
|
||||||
GLOB.clay_merge_open_stack = make([dynamic]Clay_Merge_Candidate, 0, 16, allocator = allocator)
|
|
||||||
GLOB.clay_memory = make([^]u8, min_memory_size, allocator = allocator)
|
|
||||||
arena := clay.CreateArenaWithCapacityAndMemory(min_memory_size, GLOB.clay_memory)
|
|
||||||
window_width, window_height: c.int
|
|
||||||
sdl.GetWindowSize(window, &window_width, &window_height)
|
|
||||||
clay.Initialize(arena, {f32(window_width), f32(window_height)}, {handler = clay_error_handler})
|
|
||||||
clay.SetMeasureTextFunction(measure_text_clay, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Free the Clay arena memory allocated in `init_clay`. Called by `destroy`. The merge stack
|
|
||||||
// is left to the package allocator's normal teardown to preserve historical behavior.
|
|
||||||
//INTERNAL
|
|
||||||
destroy_clay :: proc(allocator: runtime.Allocator) {
|
|
||||||
free(GLOB.clay_memory, allocator)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset Clay per-frame state: the z-index high-water mark and the border-merge stack.
|
|
||||||
// Called by `clear_global` at the start of every frame.
|
|
||||||
//INTERNAL
|
|
||||||
clear_clay_per_frame :: proc() {
|
|
||||||
GLOB.clay_z_index = 0
|
|
||||||
clear(&GLOB.clay_merge_open_stack)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
|
||||||
// ----- Image data (Clay RenderCommandType.Image payload) ------------
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
Clay_Image_Data :: struct {
|
|
||||||
texture_id: Texture_Id,
|
|
||||||
fit: Fit_Mode,
|
|
||||||
tint: Color,
|
|
||||||
}
|
|
||||||
|
|
||||||
clay_image_data :: proc(id: Texture_Id, fit: Fit_Mode = .Stretch, tint: Color = WHITE) -> Clay_Image_Data {
|
|
||||||
return {texture_id = id, fit = fit, tint = tint}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
|
||||||
// ----- Callbacks (clay -> draw) ------------
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
@(private = "file")
|
|
||||||
clay_error_handler :: proc "c" (errorData: clay.ErrorData) {
|
|
||||||
context = GLOB.odin_context
|
|
||||||
log.error("Clay error:", errorData.errorType, errorData.errorText)
|
|
||||||
}
|
|
||||||
|
|
||||||
@(private = "file")
|
|
||||||
measure_text_clay :: proc "c" (
|
|
||||||
text: clay.StringSlice,
|
|
||||||
config: ^clay.TextElementConfig,
|
|
||||||
user_data: rawptr,
|
|
||||||
) -> clay.Dimensions {
|
|
||||||
context = GLOB.odin_context
|
|
||||||
text := string(text.chars[:text.length])
|
|
||||||
c_text := strings.clone_to_cstring(text, context.temp_allocator)
|
|
||||||
defer delete(c_text, context.temp_allocator)
|
|
||||||
width, height: c.int
|
|
||||||
if !sdl_ttf.GetStringSize(get_font(config.fontId, config.fontSize), c_text, 0, &width, &height) {
|
|
||||||
log.panicf("Failed to measure text: %s", sdl.GetError())
|
|
||||||
}
|
|
||||||
|
|
||||||
return clay.Dimensions{width = f32(width) / GLOB.dpi_scaling, height = f32(height) / GLOB.dpi_scaling}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
|
||||||
// ----- Custom draw + customData envelope ------------
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// Called for each Clay `RenderCommandType.Custom` render command that
|
|
||||||
// `prepare_clay_batch` encounters and which is NOT a levlib-managed variant
|
|
||||||
// (e.g. `Backdrop_Marker`).
|
|
||||||
//
|
|
||||||
// - `layer` is the layer the command belongs to (post-z-index promotion).
|
|
||||||
// - `bounds` is already translated into the active layer's coordinate system
|
|
||||||
// and pre-DPI, matching what the built-in shape procs expect.
|
|
||||||
// - `render_data` is Clay's `CustomRenderData` for the element, exposing
|
|
||||||
// `backgroundColor` and `cornerRadius`. Its `customData` field has been
|
|
||||||
// unwrapped from the `Clay_Custom` envelope: it points at the user's own
|
|
||||||
// data (the value the user wrote into the `rawptr` variant), not at the
|
|
||||||
// `Clay_Custom` itself. If the union was zero-init (no variant set) or
|
|
||||||
// `customData` was originally nil, the callback receives nil.
|
|
||||||
//
|
|
||||||
// The callback must not call `new_layer` or `prepare_clay_batch`.
|
|
||||||
Custom_Draw :: #type proc(layer: ^Layer, bounds: Rectangle, render_data: clay.CustomRenderData)
|
|
||||||
|
|
||||||
ClayBatch :: struct {
|
|
||||||
bounds: Rectangle,
|
|
||||||
cmds: clay.ClayArray(clay.RenderCommand),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Discriminated sum of everything `clay.CustomElementConfig.customData` is allowed to point
|
|
||||||
// at. levlib-defined variants (currently just `Backdrop_Marker`) are recognized by
|
|
||||||
// `prepare_clay_batch` and routed to the appropriate internal path; the `rawptr` variant is
|
|
||||||
// the escape hatch for user-defined custom drawing — `prepare_clay_batch` unwraps it before
|
|
||||||
// invoking `custom_draw` so the callback sees the user's pointer in `render_data.customData`
|
|
||||||
// exactly as if no wrapper were involved.
|
|
||||||
//
|
|
||||||
// Contract: `customData`, when non-nil, MUST point at storage holding a `Clay_Custom`
|
|
||||||
// value. The user owns that storage; its lifetime must span the Clay layout call and the
|
|
||||||
// matching `prepare_clay_batch` call. Pointing `customData` at a bare user struct violates
|
|
||||||
// the contract — the dispatcher will read its first bytes as a union tag and either route
|
|
||||||
// the draw incorrectly or panic on type assertion. There is no recovery path; this is a
|
|
||||||
// strict-discipline API by design.
|
|
||||||
//
|
|
||||||
// Construction notes (Odin implicit-conversion rules):
|
|
||||||
// - Backdrop variant: `bd: Clay_Custom = Backdrop_Marker{...}` works directly.
|
|
||||||
// Variant-to-union conversion is implicit.
|
|
||||||
// - User pointer: `up: Clay_Custom = rawptr(&my_struct)` — the explicit `rawptr(...)` is
|
|
||||||
// required because Odin does not chain `^T -> rawptr -> Clay_Custom` implicitly. A bare
|
|
||||||
// `up: Clay_Custom = &my_struct` is a compile error.
|
|
||||||
Clay_Custom :: union {
|
|
||||||
Backdrop_Marker,
|
|
||||||
rawptr,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Per-primitive parameters for a backdrop blur dispatched through the Clay integration.
|
|
||||||
// Embedded as a `Clay_Custom` variant; `prepare_clay_batch` walks the command stream,
|
|
||||||
// opens/closes a backdrop scope around contiguous backdrop runs, and feeds these to
|
|
||||||
// `backdrop_blur` via `dispatch_clay_backdrop`. The discriminant is the union tag — no
|
|
||||||
// in-band magic field needed (compiler-enforced).
|
|
||||||
Backdrop_Marker :: struct {
|
|
||||||
sigma: f32,
|
|
||||||
tint: Color,
|
|
||||||
radii: Rectangle_Radii,
|
|
||||||
feather_ppx: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
|
||||||
// ----- Border-merge stack ------------
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// One entry on the Clay merge stack. Pushed by `dispatch_clay_command` when emitting a
|
|
||||||
// Rectangle or an Image primitive, then popped by a matching Border to retroactively add
|
|
||||||
// the outline. See `try_dispatch_clay_border_merge` for the matching semantics.
|
|
||||||
//INTERNAL
|
|
||||||
Clay_Merge_Candidate :: struct {
|
|
||||||
primitive_index: u32, // Index into `GLOB.tmp_primitives` of the candidate primitive.
|
|
||||||
outer_bounds: Rectangle, // Clay's bounding box — keyed on for the bounds match check.
|
|
||||||
corner_radii: clay.CornerRadius, // Clay's corner radii — also keyed on for the match check.
|
|
||||||
image_data: Clay_Image_Data, // Only read when kind == .Fill_Texture (needed to refit UVs to inner_bounds).
|
|
||||||
kind: Clay_Merge_Candidate_Kind,
|
|
||||||
}
|
|
||||||
|
|
||||||
//INTERNAL
|
|
||||||
Clay_Merge_Candidate_Kind :: enum u8 {
|
|
||||||
// Solid Color brush. Used for Rectangle commands and for the bg primitive of an Image
|
|
||||||
// command that has `backgroundColor.a > 0`. Merge mutation: shrink shape + add outline.
|
|
||||||
Fill_Color,
|
|
||||||
// Texture_Fill brush. Used for the image primitive of an Image command with no bg, where
|
|
||||||
// `fit_params` returned `fit_rect == outer_bounds` (the image fully covers Clay's bounds).
|
|
||||||
// Merge mutation: shrink shape + add outline + refit UV against inner_bounds.
|
|
||||||
Fill_Texture,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns true if this Clay render command represents a backdrop primitive — i.e. its
|
|
||||||
// `customData` points at a `Clay_Custom` whose active variant is `Backdrop_Marker`.
|
|
||||||
is_clay_backdrop :: proc(cmd: ^clay.RenderCommand) -> bool {
|
|
||||||
if cmd.commandType != .Custom do return false
|
|
||||||
p := cmd.renderData.custom.customData
|
|
||||||
if p == nil do return false
|
|
||||||
_, ok := (^Clay_Custom)(p).(Backdrop_Marker)
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
|
||||||
// ----- Border emission ------------
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// Emit a Clay border drawn INSIDE `bounds` — the outer edge of each side aligns with
|
|
||||||
// `bounds`, the inner edge is `border_width.*` pixels inset. Matches Clay's layout model
|
|
||||||
// (CSS border-box) so the visible element occupies exactly Clay's allocated space.
|
|
||||||
//
|
|
||||||
// The fast path (uniform widths) uses `rectangle()` with the built-in SDF outline, which
|
|
||||||
// always extends outward from the shape it's given — we pre-shrink the shape by
|
|
||||||
// `border_width` so the outline lands precisely at Clay's bounds. The slow path (non-uniform
|
|
||||||
// widths) emits per-side rectangles and per-corner arcs directly, all positioned inside
|
|
||||||
// `bounds`. All-zero widths is a no-op.
|
|
||||||
//
|
|
||||||
// A corner is rounded iff its radius is positive AND both adjacent sides have positive
|
|
||||||
// width. Top corners take their thickness from `border_width.top`, bottom corners from
|
|
||||||
// `border_width.bottom`. When the two widths meeting at a corner differ there is a step at
|
|
||||||
// the side/corner junction (acceptable for the rare mixed-width case).
|
|
||||||
//
|
|
||||||
// When `border_width > corner_radius`, the inner corner clamps to zero (sharp inside, still
|
|
||||||
// rounded outside) — matches CSS-standard behavior.
|
|
||||||
//INTERNAL
|
|
||||||
clay_emit_partial_border :: proc(
|
|
||||||
layer: ^Layer,
|
|
||||||
bounds: Rectangle,
|
|
||||||
border_color: Color,
|
|
||||||
border_width: clay.BorderWidth,
|
|
||||||
corner_radii: clay.CornerRadius,
|
|
||||||
) {
|
|
||||||
// All-zero: nothing to draw.
|
|
||||||
if border_width.top == 0 && border_width.right == 0 && border_width.bottom == 0 && border_width.left == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert side widths once (u16 -> f32) and cache for reuse.
|
|
||||||
width_top := f32(border_width.top)
|
|
||||||
width_right := f32(border_width.right)
|
|
||||||
width_bottom := f32(border_width.bottom)
|
|
||||||
width_left := f32(border_width.left)
|
|
||||||
|
|
||||||
// Fast path: all four sides have the same nonzero width. Pre-shrink the shape by the
|
|
||||||
// uniform width so the SDF outline (which always extends outward from the shape) lands
|
|
||||||
// exactly at Clay's `bounds` — the visible border ends up INSIDE Clay's allocation while
|
|
||||||
// the SDF mechanism keeps doing outward outlining. Single SDF primitive, exact curves,
|
|
||||||
// analytical AA.
|
|
||||||
if border_width.left == border_width.top &&
|
|
||||||
border_width.top == border_width.right &&
|
|
||||||
border_width.right == border_width.bottom {
|
|
||||||
uniform_width := width_top
|
|
||||||
inner_bounds := Rectangle {
|
|
||||||
x = bounds.x + uniform_width,
|
|
||||||
y = bounds.y + uniform_width,
|
|
||||||
width = bounds.width - 2 * uniform_width,
|
|
||||||
height = bounds.height - 2 * uniform_width,
|
|
||||||
}
|
|
||||||
inner_radii := Rectangle_Radii {
|
|
||||||
top_left = max(0, corner_radii.topLeft - uniform_width),
|
|
||||||
top_right = max(0, corner_radii.topRight - uniform_width),
|
|
||||||
bottom_right = max(0, corner_radii.bottomRight - uniform_width),
|
|
||||||
bottom_left = max(0, corner_radii.bottomLeft - uniform_width),
|
|
||||||
}
|
|
||||||
rectangle(
|
|
||||||
layer,
|
|
||||||
inner_bounds,
|
|
||||||
BLANK,
|
|
||||||
outline_color = border_color,
|
|
||||||
outline_width = uniform_width,
|
|
||||||
radii = inner_radii,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// A corner is drawn rounded only if its radius is positive AND both adjacent sides are present.
|
|
||||||
top_left_rounded := corner_radii.topLeft > 0 && border_width.top > 0 && border_width.left > 0
|
|
||||||
top_right_rounded := corner_radii.topRight > 0 && border_width.top > 0 && border_width.right > 0
|
|
||||||
bottom_left_rounded := corner_radii.bottomLeft > 0 && border_width.bottom > 0 && border_width.left > 0
|
|
||||||
bottom_right_rounded := corner_radii.bottomRight > 0 && border_width.bottom > 0 && border_width.right > 0
|
|
||||||
|
|
||||||
// Horizontal x-coordinates where the top/bottom side rectangles start/end. When the
|
|
||||||
// adjacent corner is rounded, the side stops at `bounds.x + radius` (where the corner
|
|
||||||
// arc takes over). When not rounded, the side runs to the bounds edge; the perpendicular
|
|
||||||
// side handles the inset to avoid overlap.
|
|
||||||
top_left_x: f32 = top_left_rounded ? bounds.x + corner_radii.topLeft : bounds.x
|
|
||||||
top_right_x: f32 =
|
|
||||||
top_right_rounded ? bounds.x + bounds.width - corner_radii.topRight : bounds.x + bounds.width
|
|
||||||
bottom_left_x: f32 = bottom_left_rounded ? bounds.x + corner_radii.bottomLeft : bounds.x
|
|
||||||
bottom_right_x: f32 =
|
|
||||||
bottom_right_rounded ? bounds.x + bounds.width - corner_radii.bottomRight : bounds.x + bounds.width
|
|
||||||
|
|
||||||
// Vertical y-coordinates where the left/right side rectangles start/end. When the
|
|
||||||
// adjacent corner is rounded, inset by the corner radius. When not rounded, inset by the
|
|
||||||
// adjacent horizontal width — the horizontal side owns the corner area (extending through
|
|
||||||
// it to the bounds edge), so the vertical side starts below it to avoid overdraw of
|
|
||||||
// translucent colors.
|
|
||||||
top_left_y: f32 = top_left_rounded ? bounds.y + corner_radii.topLeft : bounds.y + width_top
|
|
||||||
top_right_y: f32 = top_right_rounded ? bounds.y + corner_radii.topRight : bounds.y + width_top
|
|
||||||
bottom_left_y: f32 =
|
|
||||||
bottom_left_rounded ? bounds.y + bounds.height - corner_radii.bottomLeft : bounds.y + bounds.height - width_bottom
|
|
||||||
bottom_right_y: f32 =
|
|
||||||
bottom_right_rounded ? bounds.y + bounds.height - corner_radii.bottomRight : bounds.y + bounds.height - width_bottom
|
|
||||||
|
|
||||||
// Side rectangles drawn INSIDE `bounds`. Sharp corners, solid fill, no outline. Each
|
|
||||||
// gated on its own width — skipping zero-width sides saves the primitive upload.
|
|
||||||
if border_width.top > 0 {
|
|
||||||
top_side := Rectangle {
|
|
||||||
x = top_left_x,
|
|
||||||
y = bounds.y,
|
|
||||||
width = top_right_x - top_left_x,
|
|
||||||
height = width_top,
|
|
||||||
}
|
|
||||||
rectangle(layer, top_side, border_color)
|
|
||||||
}
|
|
||||||
if border_width.bottom > 0 {
|
|
||||||
bottom_side := Rectangle {
|
|
||||||
x = bottom_left_x,
|
|
||||||
y = bounds.y + bounds.height - width_bottom,
|
|
||||||
width = bottom_right_x - bottom_left_x,
|
|
||||||
height = width_bottom,
|
|
||||||
}
|
|
||||||
rectangle(layer, bottom_side, border_color)
|
|
||||||
}
|
|
||||||
if border_width.left > 0 {
|
|
||||||
left_side := Rectangle {
|
|
||||||
x = bounds.x,
|
|
||||||
y = top_left_y,
|
|
||||||
width = width_left,
|
|
||||||
height = bottom_left_y - top_left_y,
|
|
||||||
}
|
|
||||||
rectangle(layer, left_side, border_color)
|
|
||||||
}
|
|
||||||
if border_width.right > 0 {
|
|
||||||
right_side := Rectangle {
|
|
||||||
x = bounds.x + bounds.width - width_right,
|
|
||||||
y = top_right_y,
|
|
||||||
width = width_right,
|
|
||||||
height = bottom_right_y - top_right_y,
|
|
||||||
}
|
|
||||||
rectangle(layer, right_side, border_color)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Corner arcs (90° quadrants) drawn INSIDE bounds: outer radius matches Clay's
|
|
||||||
// `corner_radii`, inner radius is the outer radius minus the relevant border thickness
|
|
||||||
// (clamped to 0 for thick borders — produces a filled pie slice when border > radius,
|
|
||||||
// matching CSS). Angle convention matches ring(): 0° = +x (right), 90° = +y (down),
|
|
||||||
// 180° = -x (left), 270° = -y (up).
|
|
||||||
if top_left_rounded {
|
|
||||||
radius := corner_radii.topLeft
|
|
||||||
inner_radius := max(0, radius - width_top)
|
|
||||||
center := Vec2{bounds.x + radius, bounds.y + radius}
|
|
||||||
ring(layer, center, inner_radius, radius, border_color, start_angle = 180, end_angle = 270)
|
|
||||||
}
|
|
||||||
if top_right_rounded {
|
|
||||||
radius := corner_radii.topRight
|
|
||||||
inner_radius := max(0, radius - width_top)
|
|
||||||
center := Vec2{bounds.x + bounds.width - radius, bounds.y + radius}
|
|
||||||
ring(layer, center, inner_radius, radius, border_color, start_angle = 270, end_angle = 360)
|
|
||||||
}
|
|
||||||
if bottom_right_rounded {
|
|
||||||
radius := corner_radii.bottomRight
|
|
||||||
inner_radius := max(0, radius - width_bottom)
|
|
||||||
center := Vec2{bounds.x + bounds.width - radius, bounds.y + bounds.height - radius}
|
|
||||||
ring(layer, center, inner_radius, radius, border_color, start_angle = 0, end_angle = 90)
|
|
||||||
}
|
|
||||||
if bottom_left_rounded {
|
|
||||||
radius := corner_radii.bottomLeft
|
|
||||||
inner_radius := max(0, radius - width_bottom)
|
|
||||||
center := Vec2{bounds.x + radius, bounds.y + bounds.height - radius}
|
|
||||||
ring(layer, center, inner_radius, radius, border_color, start_angle = 90, end_angle = 180)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to retroactively merge this Border into a pending Rectangle/Image candidate on the
|
|
||||||
// merge stack. Returns true on success so the caller can skip the standalone Border emission.
|
|
||||||
//
|
|
||||||
// Clay emits a parent element's bg and border bracketing all the children's commands, so a
|
|
||||||
// simple "is the next command a Border?" check (the previous approach) only catches leaf
|
|
||||||
// elements. The stack approach lets us pair them across arbitrary nesting: every Rectangle/
|
|
||||||
// Image push registers itself; every Border pops down until it finds a geometric match.
|
|
||||||
//
|
|
||||||
// Pop semantics: non-matching candidates above the match are discarded — their elements had
|
|
||||||
// no border anyway, so their primitives stay in `tmp_primitives` as plain Rectangles. A
|
|
||||||
// Border that finds no match at all falls back to standalone `clay_emit_partial_border`.
|
|
||||||
//
|
|
||||||
// Predicates that decline a candidate:
|
|
||||||
// - non-uniform or zero border widths (can't be a single uniform outline)
|
|
||||||
// - translucent border (the unmerged path's bg-under-border blending differs)
|
|
||||||
// - mismatched bounds or cornerRadius (the candidate isn't from the same element)
|
|
||||||
//
|
|
||||||
// False-match risk: two unrelated elements with bit-identical bounds and corner radii.
|
|
||||||
// Requires geometric coincidence (rare in practice), and even when it fires, the misattributed
|
|
||||||
// outline still lands at the correct screen position with the correct color — the pixels
|
|
||||||
// match the unmerged ground truth for opaque borders (the only kind we merge).
|
|
||||||
//INTERNAL
|
|
||||||
try_dispatch_clay_border_merge :: proc(bounds: Rectangle, border_data: clay.BorderRenderData) -> bool {
|
|
||||||
border_width := border_data.width
|
|
||||||
uniform_nonzero :=
|
|
||||||
border_width.left == border_width.top &&
|
|
||||||
border_width.top == border_width.right &&
|
|
||||||
border_width.right == border_width.bottom &&
|
|
||||||
border_width.top > 0
|
|
||||||
if !uniform_nonzero do return false
|
|
||||||
if border_data.color[3] < 255 do return false
|
|
||||||
|
|
||||||
for len(GLOB.clay_merge_open_stack) > 0 {
|
|
||||||
candidate := pop(&GLOB.clay_merge_open_stack)
|
|
||||||
if candidate.outer_bounds != bounds do continue
|
|
||||||
if candidate.corner_radii != border_data.cornerRadius do continue
|
|
||||||
apply_clay_border_merge_to_primitive(candidate, border_data)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mutates `tmp_primitives[candidate.primitive_index]` in place: shrinks the SDF shape by
|
|
||||||
// the uniform border width so the (outward) outline lands at the outer bounds, sets the
|
|
||||||
// outline flag and params, and — for `Fill_Texture` candidates — refits the texture's UV
|
|
||||||
// against `inner_bounds` so the image doesn't overflow into the border strip.
|
|
||||||
//
|
|
||||||
// The primitive's `bounds` field stays at the outer bounds: the rasterized quad already
|
|
||||||
// covers the area the outline now occupies. Skipping the bounds expansion that
|
|
||||||
// `apply_brush_and_outline` would normally do is intentional — expanding here would push the
|
|
||||||
// rasterized quad past Clay's outer edge.
|
|
||||||
//INTERNAL
|
|
||||||
apply_clay_border_merge_to_primitive :: proc(
|
|
||||||
candidate: Clay_Merge_Candidate,
|
|
||||||
border_data: clay.BorderRenderData,
|
|
||||||
) {
|
|
||||||
prim := &GLOB.tmp_primitives[candidate.primitive_index]
|
|
||||||
uniform_width := f32(border_data.width.top)
|
|
||||||
dpi_scale := GLOB.dpi_scaling
|
|
||||||
|
|
||||||
inner_half_width := candidate.outer_bounds.width * 0.5 - uniform_width
|
|
||||||
inner_half_height := candidate.outer_bounds.height * 0.5 - uniform_width
|
|
||||||
prim.params.rrect.half_size_ppx = {inner_half_width * dpi_scale, inner_half_height * dpi_scale}
|
|
||||||
prim.params.rrect.radii_ppx = {
|
|
||||||
max(0, candidate.corner_radii.topLeft - uniform_width) * dpi_scale,
|
|
||||||
max(0, candidate.corner_radii.topRight - uniform_width) * dpi_scale,
|
|
||||||
max(0, candidate.corner_radii.bottomRight - uniform_width) * dpi_scale,
|
|
||||||
max(0, candidate.corner_radii.bottomLeft - uniform_width) * dpi_scale,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the outline bit in the packed flags field (low byte = Shape_Kind, bits 8+ = Shape_Flags).
|
|
||||||
prim.flags |= u32(transmute(u8)Shape_Flags{.Outline}) << 8
|
|
||||||
prim.effects.outline_color = Color(border_data.color)
|
|
||||||
prim.effects.outline_packed = pack_f16_pair(f16(uniform_width * dpi_scale), 0)
|
|
||||||
|
|
||||||
if candidate.kind == .Fill_Texture {
|
|
||||||
// The candidate was only pushed if its `fit_rect == outer_bounds` at emission time, so the
|
|
||||||
// image fills the rasterized quad. Refit UVs against `inner_bounds` so the image is scoped
|
|
||||||
// to the area inside the new outline rather than overflowing into the border strip.
|
|
||||||
inner_bounds := Rectangle {
|
|
||||||
x = candidate.outer_bounds.x + uniform_width,
|
|
||||||
y = candidate.outer_bounds.y + uniform_width,
|
|
||||||
width = candidate.outer_bounds.width - 2 * uniform_width,
|
|
||||||
height = candidate.outer_bounds.height - 2 * uniform_width,
|
|
||||||
}
|
|
||||||
uv_rect, _, _ := fit_params(candidate.image_data.fit, inner_bounds, candidate.image_data.texture_id)
|
|
||||||
prim.uv_rect = {uv_rect.x, uv_rect.y, uv_rect.width, uv_rect.height}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
|
||||||
// ----- Command dispatch ------------
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// Dispatch a single non-backdrop Clay render command to the appropriate `draw` primitive.
|
|
||||||
// Extracted from the main `prepare_clay_batch` walk so that the deferred-buffer flush path
|
|
||||||
// can replay commands accumulated during an open backdrop scope without duplicating the
|
|
||||||
// per-command lowering code.
|
|
||||||
//INTERNAL
|
|
||||||
dispatch_clay_command :: proc(
|
|
||||||
layer: ^Layer,
|
|
||||||
render_command: ^clay.RenderCommand,
|
|
||||||
custom_draw: Custom_Draw,
|
|
||||||
temp_allocator: runtime.Allocator,
|
|
||||||
) {
|
|
||||||
// Translate bounding box of the primitive by the layer position
|
|
||||||
bounds := Rectangle {
|
|
||||||
x = render_command.boundingBox.x + layer.bounds.x,
|
|
||||||
y = render_command.boundingBox.y + layer.bounds.y,
|
|
||||||
width = render_command.boundingBox.width,
|
|
||||||
height = render_command.boundingBox.height,
|
|
||||||
}
|
|
||||||
|
|
||||||
switch render_command.commandType {
|
|
||||||
case clay.RenderCommandType.None:
|
|
||||||
log.errorf(
|
|
||||||
"Received render command with type None. This generally means we're in some kind of fucked up state.",
|
|
||||||
)
|
|
||||||
case clay.RenderCommandType.Text:
|
|
||||||
render_data := render_command.renderData.text
|
|
||||||
txt := string(render_data.stringContents.chars[:render_data.stringContents.length])
|
|
||||||
c_text := strings.clone_to_cstring(txt, temp_allocator)
|
|
||||||
defer delete(c_text, temp_allocator)
|
|
||||||
// Clay render-command IDs are derived via Clay's internal HashNumber (Jenkins-family)
|
|
||||||
// and namespaced with .Clay so they can never collide with user-provided custom text IDs.
|
|
||||||
sdl_text := cache_get_or_update(
|
|
||||||
Cache_Key{render_command.id, .Clay},
|
|
||||||
c_text,
|
|
||||||
get_font(render_data.fontId, render_data.fontSize),
|
|
||||||
)
|
|
||||||
prepare_text(layer, Text{sdl_text, {bounds.x, bounds.y}, Color(render_data.textColor)})
|
|
||||||
case clay.RenderCommandType.Image:
|
|
||||||
// Any texture
|
|
||||||
render_data := render_command.renderData.image
|
|
||||||
if render_data.imageData == nil do return
|
|
||||||
img_data := (^Clay_Image_Data)(render_data.imageData)^
|
|
||||||
corner_radii_clay := render_data.cornerRadius
|
|
||||||
radii := Rectangle_Radii {
|
|
||||||
top_left = corner_radii_clay.topLeft,
|
|
||||||
top_right = corner_radii_clay.topRight,
|
|
||||||
bottom_right = corner_radii_clay.bottomRight,
|
|
||||||
bottom_left = corner_radii_clay.bottomLeft,
|
|
||||||
}
|
|
||||||
|
|
||||||
background_color := Color(render_data.backgroundColor)
|
|
||||||
uv_rect, sampler, fit_rect := fit_params(img_data.fit, bounds, img_data.texture_id)
|
|
||||||
|
|
||||||
if background_color.a > 0 {
|
|
||||||
// Bg behind image. Push the bg primitive as the merge candidate so a matching Border
|
|
||||||
// turns into a bg+border-merged primitive plus a separate image draw on top.
|
|
||||||
rectangle(layer, bounds, background_color, radii = radii)
|
|
||||||
bg_primitive_index := u32(len(GLOB.tmp_primitives) - 1)
|
|
||||||
rectangle(
|
|
||||||
layer,
|
|
||||||
fit_rect,
|
|
||||||
Texture_Fill{id = img_data.texture_id, tint = img_data.tint, uv_rect = uv_rect, sampler = sampler},
|
|
||||||
radii = radii,
|
|
||||||
)
|
|
||||||
append(
|
|
||||||
&GLOB.clay_merge_open_stack,
|
|
||||||
Clay_Merge_Candidate {
|
|
||||||
primitive_index = bg_primitive_index,
|
|
||||||
outer_bounds = bounds,
|
|
||||||
corner_radii = corner_radii_clay,
|
|
||||||
kind = .Fill_Color,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// No bg: the image itself can host the outline if its fit fully covers Clay's bounds.
|
|
||||||
// `Fit_Mode.Fit` with aspect mismatch returns a sub-rect, which can't host an outline
|
|
||||||
// (the rasterized quad wouldn't reach Clay's outer edge), so we skip pushing.
|
|
||||||
rectangle(
|
|
||||||
layer,
|
|
||||||
fit_rect,
|
|
||||||
Texture_Fill{id = img_data.texture_id, tint = img_data.tint, uv_rect = uv_rect, sampler = sampler},
|
|
||||||
radii = radii,
|
|
||||||
)
|
|
||||||
if fit_rect == bounds {
|
|
||||||
img_primitive_index := u32(len(GLOB.tmp_primitives) - 1)
|
|
||||||
append(
|
|
||||||
&GLOB.clay_merge_open_stack,
|
|
||||||
Clay_Merge_Candidate {
|
|
||||||
primitive_index = img_primitive_index,
|
|
||||||
outer_bounds = bounds,
|
|
||||||
corner_radii = corner_radii_clay,
|
|
||||||
image_data = img_data,
|
|
||||||
kind = .Fill_Texture,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case clay.RenderCommandType.ScissorStart:
|
|
||||||
if bounds.width == 0 || bounds.height == 0 do return
|
|
||||||
|
|
||||||
curr_scissor := &GLOB.scissors[layer.scissor_start + layer.scissor_len - 1]
|
|
||||||
|
|
||||||
if curr_scissor.sub_batch_len != 0 {
|
|
||||||
// Scissor has some content, need to make a new scissor
|
|
||||||
new := Scissor {
|
|
||||||
sub_batch_start = curr_scissor.sub_batch_start + curr_scissor.sub_batch_len,
|
|
||||||
bounds = sdl.Rect {
|
|
||||||
c.int(bounds.x * GLOB.dpi_scaling),
|
|
||||||
c.int(bounds.y * GLOB.dpi_scaling),
|
|
||||||
c.int(bounds.width * GLOB.dpi_scaling),
|
|
||||||
c.int(bounds.height * GLOB.dpi_scaling),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
append(&GLOB.scissors, new)
|
|
||||||
layer.scissor_len += 1
|
|
||||||
} else {
|
|
||||||
curr_scissor.bounds = sdl.Rect {
|
|
||||||
c.int(bounds.x * GLOB.dpi_scaling),
|
|
||||||
c.int(bounds.y * GLOB.dpi_scaling),
|
|
||||||
c.int(bounds.width * GLOB.dpi_scaling),
|
|
||||||
c.int(bounds.height * GLOB.dpi_scaling),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case clay.RenderCommandType.ScissorEnd:
|
|
||||||
case clay.RenderCommandType.OverlayColorStart, clay.RenderCommandType.OverlayColorEnd:
|
|
||||||
unimplemented("Clay overlays not supported yet...")
|
|
||||||
case clay.RenderCommandType.Rectangle:
|
|
||||||
render_data := render_command.renderData.rectangle
|
|
||||||
corner_radii_clay := render_data.cornerRadius
|
|
||||||
background_color := Color(render_data.backgroundColor)
|
|
||||||
radii := Rectangle_Radii {
|
|
||||||
top_left = corner_radii_clay.topLeft,
|
|
||||||
top_right = corner_radii_clay.topRight,
|
|
||||||
bottom_right = corner_radii_clay.bottomRight,
|
|
||||||
bottom_left = corner_radii_clay.bottomLeft,
|
|
||||||
}
|
|
||||||
rectangle(layer, bounds, background_color, radii = radii)
|
|
||||||
// Register this primitive as a merge candidate. If the element has a matching Border
|
|
||||||
// later in the stream (after its children's commands), `try_dispatch_clay_border_merge`
|
|
||||||
// will pop this candidate and mutate the primitive in-place to add the outline.
|
|
||||||
primitive_index := u32(len(GLOB.tmp_primitives) - 1)
|
|
||||||
append(
|
|
||||||
&GLOB.clay_merge_open_stack,
|
|
||||||
Clay_Merge_Candidate {
|
|
||||||
primitive_index = primitive_index,
|
|
||||||
outer_bounds = bounds,
|
|
||||||
corner_radii = corner_radii_clay,
|
|
||||||
kind = .Fill_Color,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
case clay.RenderCommandType.Border:
|
|
||||||
render_data := render_command.renderData.border
|
|
||||||
if try_dispatch_clay_border_merge(bounds, render_data) do return
|
|
||||||
clay_emit_partial_border(
|
|
||||||
layer,
|
|
||||||
bounds,
|
|
||||||
Color(render_data.color),
|
|
||||||
render_data.width,
|
|
||||||
render_data.cornerRadius,
|
|
||||||
)
|
|
||||||
case clay.RenderCommandType.Custom:
|
|
||||||
// Copy the CustomRenderData by value so we can patch its `customData` field for the
|
|
||||||
// user callback without mutating Clay-owned memory. After unwrapping, the callback
|
|
||||||
// sees its own pointer in `render_data.customData`, identical to what it would see
|
|
||||||
// if `Clay_Custom` did not exist as an intermediary.
|
|
||||||
patched := render_command.renderData.custom
|
|
||||||
// Default to nil so a zero-init `Clay_Custom` (no variant set) and an originally-nil
|
|
||||||
// `customData` both surface to the callback as `customData = nil`.
|
|
||||||
patched.customData = nil
|
|
||||||
if custom_data_pointer := render_command.renderData.custom.customData; custom_data_pointer != nil {
|
|
||||||
switch custom_value in (^Clay_Custom)(custom_data_pointer)^ {
|
|
||||||
case Backdrop_Marker: // The walker pre-filters backdrops into `dispatch_clay_backdrop` and never feeds
|
|
||||||
// them here; reaching this branch means either the walker logic is broken or the
|
|
||||||
// `Clay_Custom` variant tag mutated between the walker's `is_clay_backdrop` check
|
|
||||||
// and this re-check (heap corruption / lifetime bug in user-managed customData
|
|
||||||
// memory). Both are renderer-level bugs that warrant a hard failure rather than a
|
|
||||||
// silently-dropped panel.
|
|
||||||
log.panicf(
|
|
||||||
"backdrop marker reached dispatch_clay_command; either the prepare_clay_batch walker is misrouting commands or the customData pointee at %p was mutated mid-frame",
|
|
||||||
render_command.renderData.custom.customData,
|
|
||||||
)
|
|
||||||
case rawptr: patched.customData = custom_value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if custom_draw != nil {
|
|
||||||
custom_draw(layer, bounds, patched)
|
|
||||||
} else if patched.customData != nil {
|
|
||||||
log.panicf(
|
|
||||||
"Received clay render command of type custom with non-nil user data but no custom_draw proc provided.",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dispatch a single backdrop Clay render command to `backdrop_blur` on the active layer.
|
|
||||||
// Caller guarantees:
|
|
||||||
// - a backdrop scope is open on `layer` so the underlying `append_or_extend_sub_batch`
|
|
||||||
// contract assertion is satisfied;
|
|
||||||
// - the command's `customData` points at a `Clay_Custom` whose active variant is
|
|
||||||
// `Backdrop_Marker` (the walker has already verified this via `is_clay_backdrop`).
|
|
||||||
//INTERNAL
|
|
||||||
dispatch_clay_backdrop :: proc(layer: ^Layer, cmd: ^clay.RenderCommand) {
|
|
||||||
bounds := Rectangle {
|
|
||||||
x = cmd.boundingBox.x + layer.bounds.x,
|
|
||||||
y = cmd.boundingBox.y + layer.bounds.y,
|
|
||||||
width = cmd.boundingBox.width,
|
|
||||||
height = cmd.boundingBox.height,
|
|
||||||
}
|
|
||||||
// Type-asserting form (no `, ok`): panics loudly if the variant tag changed since
|
|
||||||
// `is_clay_backdrop`, which is the desired tripwire for a heap-corruption bug in
|
|
||||||
// user-managed customData.
|
|
||||||
marker := (^Clay_Custom)(cmd.renderData.custom.customData).(Backdrop_Marker)
|
|
||||||
backdrop_blur(
|
|
||||||
layer,
|
|
||||||
bounds,
|
|
||||||
gaussian_sigma = marker.sigma,
|
|
||||||
tint = marker.tint,
|
|
||||||
radii = marker.radii,
|
|
||||||
feather_ppx = marker.feather_ppx,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close the in-flight backdrop scope (if open) and replay every command accumulated in the
|
|
||||||
// deferred index buffer. Ordering: end_backdrop first so deferred non-backdrop draws land
|
|
||||||
// at submission position relative to the bracket they followed (the bracket is now closed,
|
|
||||||
// so these draws render after it). Used at every zIndex transition and at end of stream.
|
|
||||||
//INTERNAL
|
|
||||||
flush_deferred_and_close_backdrop_scope :: proc(
|
|
||||||
layer: ^Layer,
|
|
||||||
batch: ^ClayBatch,
|
|
||||||
deferred_indices: ^[dynamic]i32,
|
|
||||||
backdrop_scope_open: ^bool,
|
|
||||||
custom_draw: Custom_Draw,
|
|
||||||
temp_allocator: runtime.Allocator,
|
|
||||||
) {
|
|
||||||
if backdrop_scope_open^ {
|
|
||||||
end_backdrop(layer)
|
|
||||||
backdrop_scope_open^ = false
|
|
||||||
}
|
|
||||||
// Clear the merge stack at scope/stratum boundaries: any pending candidates from the
|
|
||||||
// pre-scope (or pre-transition) commands stay as plain primitives — they can't merge
|
|
||||||
// with Borders on the far side of the boundary because that would change draw order.
|
|
||||||
clear(&GLOB.clay_merge_open_stack)
|
|
||||||
for index in deferred_indices^ {
|
|
||||||
cmd := clay.RenderCommandArray_Get(&batch.cmds, index)
|
|
||||||
dispatch_clay_command(layer, cmd, custom_draw, temp_allocator)
|
|
||||||
}
|
|
||||||
clear(deferred_indices)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
|
||||||
// ----- Main entry point ------------
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// Process Clay render commands into shape, text, and backdrop primitives.
|
|
||||||
//
|
|
||||||
// Single-walk dispatcher with a deferred buffer. The walk does three things per command:
|
|
||||||
// 1. zIndex transitions: close the in-flight scope, flush any deferred non-backdrop
|
|
||||||
// commands into the current layer, then open a new layer seeded with `base_layer.bounds`
|
|
||||||
// (NOT the bumping element's bounds — Clay's floating elements with `clipTo = .None`
|
|
||||||
// should not be over-clipped, and `clipTo = .AttachedParent` floating elements get a
|
|
||||||
// Clay-emitted ScissorStart immediately afterward that narrows correctly).
|
|
||||||
// 2. Backdrop commands: open a scope on first encounter (extending it on subsequent ones),
|
|
||||||
// then dispatch the backdrop_blur call.
|
|
||||||
// 3. Non-backdrop commands during an open scope: append to the deferred buffer for replay
|
|
||||||
// after the scope closes. The buffer holds command indices, not pointers, so it stays
|
|
||||||
// valid even if the underlying ClayArray reallocates.
|
|
||||||
// At end of stream, flush whatever remains.
|
|
||||||
prepare_clay_batch :: proc(
|
|
||||||
base_layer: ^Layer,
|
|
||||||
batch: ^ClayBatch,
|
|
||||||
custom_draw: Custom_Draw = nil,
|
|
||||||
temp_allocator := context.temp_allocator,
|
|
||||||
) {
|
|
||||||
layer := base_layer
|
|
||||||
command_count := int(batch.cmds.length)
|
|
||||||
deferred_indices := make([dynamic]i32, 0, 16, temp_allocator)
|
|
||||||
backdrop_scope_open := false
|
|
||||||
// Seed from GLOB.clay_z_index so multi-batch frames preserve the original semantics: a
|
|
||||||
// later call to `prepare_clay_batch` doesn't re-trigger layer splits for zIndex values
|
|
||||||
// the previous batch already saw.
|
|
||||||
previous_z_index := GLOB.clay_z_index
|
|
||||||
|
|
||||||
// Start with a clean merge stack. The stack is also cleared by
|
|
||||||
// `flush_deferred_and_close_backdrop_scope` at every stratum boundary; both clears together
|
|
||||||
// ensure merge candidates never pair across a boundary that would shift draw order.
|
|
||||||
clear(&GLOB.clay_merge_open_stack)
|
|
||||||
for i in 0 ..< command_count {
|
|
||||||
cmd := clay.RenderCommandArray_Get(&batch.cmds, i32(i))
|
|
||||||
|
|
||||||
// zIndex transition: close out current stratum, create new layer, continue.
|
|
||||||
if cmd.zIndex > previous_z_index {
|
|
||||||
log.debug("Higher zIndex found, creating new layer & setting z_index to", cmd.zIndex)
|
|
||||||
flush_deferred_and_close_backdrop_scope(
|
|
||||||
layer,
|
|
||||||
batch,
|
|
||||||
&deferred_indices,
|
|
||||||
&backdrop_scope_open,
|
|
||||||
custom_draw,
|
|
||||||
temp_allocator,
|
|
||||||
)
|
|
||||||
layer = new_layer(layer, base_layer.bounds)
|
|
||||||
previous_z_index = cmd.zIndex
|
|
||||||
// Keep GLOB.clay_z_index in sync for any external readers (debug tooling, etc.).
|
|
||||||
GLOB.clay_z_index = cmd.zIndex
|
|
||||||
}
|
|
||||||
|
|
||||||
if is_clay_backdrop(cmd) {
|
|
||||||
if !backdrop_scope_open {
|
|
||||||
begin_backdrop(layer)
|
|
||||||
backdrop_scope_open = true
|
|
||||||
}
|
|
||||||
dispatch_clay_backdrop(layer, cmd)
|
|
||||||
} else if backdrop_scope_open {
|
|
||||||
append(&deferred_indices, i32(i))
|
|
||||||
} else {
|
|
||||||
// Rectangle/Image dispatches push merge candidates; Border dispatches pop the stack
|
|
||||||
// to retroactively add an outline to a matching candidate. See
|
|
||||||
// `try_dispatch_clay_border_merge` for the matching semantics.
|
|
||||||
dispatch_clay_command(layer, cmd, custom_draw, temp_allocator)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// End-of-stream: flush whatever remains.
|
|
||||||
flush_deferred_and_close_backdrop_scope(
|
|
||||||
layer,
|
|
||||||
batch,
|
|
||||||
&deferred_indices,
|
|
||||||
&backdrop_scope_open,
|
|
||||||
custom_draw,
|
|
||||||
temp_allocator,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-1613
File diff suppressed because it is too large
Load Diff
@@ -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. 0–4px is the
|
|
||||||
// preferred range; larger radii exist only for chips/pills.
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
RADIUS_NONE :: 0 // sharp corners — preferred default for chrome
|
|
||||||
RADIUS_SM :: 4 // micro-rounding for inline code, small badges
|
|
||||||
RADIUS_MD :: 6 // default for cards, buttons, inputs
|
|
||||||
RADIUS_LG :: 10 // rare — used only for prominent containers
|
|
||||||
RADIUS_PILL :: 999 // fully-rounded chips, status pills, tags
|
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// BORDER WIDTH
|
|
||||||
// 1px solid is the standard. Heavier weights are only used for
|
|
||||||
// the Art Deco hairline accent on pre/code blocks.
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
// Standard border weight everywhere — always crisp, always visible.
|
|
||||||
BORDER_WIDTH_DEFAULT :: 1
|
|
||||||
|
|
||||||
// Accent edge on <pre> blocks (left side, gold) and similar
|
|
||||||
// emphasized rule treatments.
|
|
||||||
BORDER_WIDTH_ACCENT :: 2
|
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// MOTION — TRANSITION DURATIONS
|
|
||||||
// Fast and purposeful. No bounce, no spring, no elastic. UI
|
|
||||||
// state changes in well under a quarter-second. Animations
|
|
||||||
// must explain causality; nothing is decorative.
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
// Entering active/pressed state. Snap-down feel — must feel
|
|
||||||
// instant under the finger.
|
|
||||||
TRANSITION_PRESS :: 55 * time.Millisecond
|
|
||||||
|
|
||||||
// Releasing from a pressed state, and slower hover-out cases.
|
|
||||||
TRANSITION_UI :: 180 * time.Millisecond
|
|
||||||
|
|
||||||
// Hover enter / exit color shift on buttons, cards, links.
|
|
||||||
TRANSITION_HOVER :: 150 * time.Millisecond
|
|
||||||
|
|
||||||
// Overlay / modal / popover fade-in. Slightly longer to
|
|
||||||
// signal "a layer changed", not "a control changed".
|
|
||||||
TRANSITION_MODAL :: 200 * time.Millisecond
|
|
||||||
|
|
||||||
// Cursor / immediate-feedback transitions (caret moves,
|
|
||||||
// terminal output ticks).
|
|
||||||
TRANSITION_CURSOR :: 80 * time.Millisecond
|
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// MOTION — COMPONENT-LEVEL TIMINGS
|
|
||||||
// Specific named durations for known interactions. Prefer these
|
|
||||||
// over picking a raw transition for a given component.
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
// Button press fade — primary/secondary/danger/success share this.
|
|
||||||
BUTTON_PRESS_FADE_DUR :: 55 * time.Millisecond
|
|
||||||
|
|
||||||
// Button release / hover-out fade.
|
|
||||||
BUTTON_RELEASE_FADE_DUR :: 180 * time.Millisecond
|
|
||||||
|
|
||||||
// Card hover (border + bg crossfade).
|
|
||||||
CARD_HOVER_FADE_DUR :: 150 * time.Millisecond
|
|
||||||
|
|
||||||
// Card press (border + bg snap to active).
|
|
||||||
CARD_PRESS_FADE_DUR :: 55 * time.Millisecond
|
|
||||||
|
|
||||||
// Modal / overlay enter.
|
|
||||||
MODAL_ENTER_DUR :: 200 * time.Millisecond
|
|
||||||
|
|
||||||
// Modal / overlay exit (mirror of enter for symmetry).
|
|
||||||
MODAL_EXIT_DUR :: 200 * time.Millisecond
|
|
||||||
|
|
||||||
// Link color crossfade on hover.
|
|
||||||
LINK_HOVER_FADE_DUR :: 180 * time.Millisecond
|
|
||||||
|
|
||||||
// Terminal scanline flicker tick — single frame of the loop.
|
|
||||||
SCANLINE_FLICKER_TICK :: 80 * time.Millisecond
|
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// TYPOGRAPHY — FONT FAMILY NAMES
|
|
||||||
// Sans: IBM Plex Sans
|
|
||||||
// Mono: Lilex — IBM Plex Mono with programming ligatures.
|
|
||||||
// Drop-in Plex Mono replacement; same skeleton, same
|
|
||||||
// proportions, plus =>, !=, >=, <=, etc. ligatures.
|
|
||||||
// Plex Sans covers display, body, and condensed roles by
|
|
||||||
// default. Lilex is for code, terminal output, data values,
|
|
||||||
// and full mono-mode surfaces.
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
// Plain family names
|
|
||||||
FONT_FAMILY_SANS :: "IBM Plex Sans"
|
|
||||||
FONT_FAMILY_MONO :: "Lilex"
|
|
||||||
|
|
||||||
// IBM Plex Sans raw font data
|
|
||||||
SANS_THIN_RAW :: #load("fonts/IBMPlexSans-Thin.ttf") // IBM Plex Sans
|
|
||||||
SANS_THIN_ITALIC_RAW :: #load("fonts/IBMPlexSans-ThinItalic.ttf") // IBM Plex Sans
|
|
||||||
SANS_EXTRALIGHT_RAW :: #load("fonts/IBMPlexSans-ExtraLight.ttf") // IBM Plex Sans
|
|
||||||
SANS_EXTRALIGHT_ITALIC_RAW :: #load("fonts/IBMPlexSans-ExtraLightItalic.ttf") // IBM Plex Sans
|
|
||||||
SANS_LIGHT_RAW :: #load("fonts/IBMPlexSans-Light.ttf") // IBM Plex Sans
|
|
||||||
SANS_LIGHT_ITALIC_RAW :: #load("fonts/IBMPlexSans-LightItalic.ttf") // IBM Plex Sans
|
|
||||||
SANS_REGULAR_RAW :: #load("fonts/IBMPlexSans-Regular.ttf") // IBM Plex Sans
|
|
||||||
SANS_ITALIC_RAW :: #load("fonts/IBMPlexSans-Italic.ttf") // IBM Plex Sans
|
|
||||||
SANS_MEDIUM_RAW :: #load("fonts/IBMPlexSans-Medium.ttf") // IBM Plex Sans
|
|
||||||
SANS_MEDIUM_ITALIC_RAW :: #load("fonts/IBMPlexSans-MediumItalic.ttf") // IBM Plex Sans
|
|
||||||
SANS_SEMIBOLD_RAW :: #load("fonts/IBMPlexSans-SemiBold.ttf") // IBM Plex Sans
|
|
||||||
SANS_SEMIBOLD_ITALIC_RAW :: #load("fonts/IBMPlexSans-SemiBoldItalic.ttf") // IBM Plex Sans
|
|
||||||
SANS_BOLD_RAW :: #load("fonts/IBMPlexSans-Bold.ttf") // IBM Plex Sans
|
|
||||||
SANS_BOLD_ITALIC_RAW :: #load("fonts/IBMPlexSans-BoldItalic.ttf") // IBM Plex Sans
|
|
||||||
|
|
||||||
// Lilex raw font data
|
|
||||||
MONO_THIN_RAW :: #load("fonts/Lilex-Thin.ttf") // Lilex
|
|
||||||
MONO_THIN_ITALIC_RAW :: #load("fonts/Lilex-ThinItalic.ttf") // Lilex
|
|
||||||
MONO_EXTRALIGHT_RAW :: #load("fonts/Lilex-ExtraLight.ttf") // Lilex
|
|
||||||
MONO_EXTRALIGHT_ITALIC_RAW :: #load("fonts/Lilex-ExtraLightItalic.ttf") // Lilex
|
|
||||||
MONO_LIGHT_RAW :: #load("fonts/Lilex-Light.ttf") // Lilex
|
|
||||||
MONO_LIGHT_ITALIC_RAW :: #load("fonts/Lilex-LightItalic.ttf") // Lilex
|
|
||||||
MONO_REGULAR_RAW :: #load("fonts/Lilex-Regular.ttf") // Lilex
|
|
||||||
MONO_ITALIC_RAW :: #load("fonts/Lilex-Italic.ttf") // Lilex
|
|
||||||
MONO_MEDIUM_RAW :: #load("fonts/Lilex-Medium.ttf") // Lilex
|
|
||||||
MONO_MEDIUM_ITALIC_RAW :: #load("fonts/Lilex-MediumItalic.ttf") // Lilex
|
|
||||||
MONO_SEMIBOLD_RAW :: #load("fonts/Lilex-SemiBold.ttf") // Lilex
|
|
||||||
MONO_SEMIBOLD_ITALIC_RAW :: #load("fonts/Lilex-SemiBoldItalic.ttf") // Lilex
|
|
||||||
MONO_BOLD_RAW :: #load("fonts/Lilex-Bold.ttf") // Lilex
|
|
||||||
MONO_BOLD_ITALIC_RAW :: #load("fonts/Lilex-BoldItalic.ttf") // Lilex
|
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// TYPOGRAPHY — TYPE SCALE (1.25 modular ratio, base 16px)
|
|
||||||
// Minimum body size on web is 14px; print is 12pt.
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
TEXT_XS :: 11 // status badges, fine print
|
|
||||||
TEXT_SM :: 13 // secondary labels, captions
|
|
||||||
TEXT_BASE :: 15 // default body text
|
|
||||||
TEXT_MD :: 16 // slightly prominent body
|
|
||||||
TEXT_LG :: 18 // subheadings, emphasized labels
|
|
||||||
TEXT_XL :: 22 // H3 level
|
|
||||||
TEXT_2XL :: 28 // H2 level
|
|
||||||
TEXT_3XL :: 36 // H1 level
|
|
||||||
TEXT_4XL :: 48 // display / hero
|
|
||||||
TEXT_5XL :: 64 // hero display
|
|
||||||
TEXT_6XL :: 96 // max scale; masthead only
|
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// TYPOGRAPHY — FONT WEIGHTS
|
|
||||||
// Constrained to the STATIC weights that BOTH faces actually
|
|
||||||
// ship from Google Fonts — IBM Plex Sans and Lilex share the
|
|
||||||
// same seven static instances:
|
|
||||||
// 100 Thin · 200 ExtraLight · 300 Light · 400 Regular ·
|
|
||||||
// 500 Medium · 600 SemiBold · 700 Bold
|
|
||||||
// There is no 800 ExtraBold and no 900 Black for either face.
|
|
||||||
// Do not request a weight outside this set — Google's API
|
|
||||||
// will fail or substitute, and the design will drift.
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
WEIGHT_THIN :: 100
|
|
||||||
WEIGHT_EXTRALIGHT :: 200
|
|
||||||
WEIGHT_LIGHT :: 300
|
|
||||||
WEIGHT_REGULAR :: 400
|
|
||||||
WEIGHT_MEDIUM :: 500
|
|
||||||
WEIGHT_SEMIBOLD :: 600
|
|
||||||
WEIGHT_BOLD :: 700
|
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// TYPOGRAPHY — LINE HEIGHTS (unitless multipliers)
|
|
||||||
// Multiply by font size to derive a leading in pixels.
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
LEADING_TIGHT :: 1.15 // display headings
|
|
||||||
LEADING_SNUG :: 1.30 // subheadings
|
|
||||||
LEADING_NORMAL :: 1.50 // default body prose
|
|
||||||
LEADING_LOOSE :: 1.70 // long-form reading, sparse density
|
|
||||||
LEADING_MONO :: 1.40 // code / terminal output
|
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// TYPOGRAPHY — LETTER SPACING (in EM units)
|
|
||||||
// Multiply by the resolved font size to get pixel spacing.
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
TRACKING_TIGHT :: -0.02 // large headings, tightened display
|
|
||||||
TRACKING_NORMAL :: 0.00 // body default
|
|
||||||
TRACKING_WIDE :: 0.05 // H1/H2 ALL CAPS, button labels
|
|
||||||
TRACKING_WIDER :: 0.10 // H5 caps, section headers
|
|
||||||
TRACKING_WIDEST :: 0.20 // .label / .label-mono — ALL CAPS chip text
|
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// HEADING ROLES — paired size + tracking + casing intent
|
|
||||||
// Casing is documentation only; these are the numbers a
|
|
||||||
// renderer actually consumes.
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
// H1 — page title, masthead. Title Case, ALL CAPS at display.
|
|
||||||
H1_SIZE :: TEXT_3XL
|
|
||||||
H1_WEIGHT :: WEIGHT_BOLD
|
|
||||||
H1_TRACKING :: TRACKING_WIDE
|
|
||||||
H1_LEADING :: LEADING_TIGHT
|
|
||||||
|
|
||||||
// H2 — major section. ALL CAPS.
|
|
||||||
H2_SIZE :: TEXT_2XL
|
|
||||||
H2_WEIGHT :: WEIGHT_BOLD
|
|
||||||
H2_TRACKING :: TRACKING_WIDE
|
|
||||||
H2_LEADING :: LEADING_TIGHT
|
|
||||||
|
|
||||||
// H3 — subsection. Sentence case, condensed semibold.
|
|
||||||
H3_SIZE :: TEXT_XL
|
|
||||||
H3_WEIGHT :: WEIGHT_SEMIBOLD
|
|
||||||
H3_TRACKING :: TRACKING_NORMAL
|
|
||||||
H3_LEADING :: LEADING_TIGHT
|
|
||||||
|
|
||||||
// H4 — minor subsection.
|
|
||||||
H4_SIZE :: TEXT_LG
|
|
||||||
H4_WEIGHT :: WEIGHT_SEMIBOLD
|
|
||||||
H4_TRACKING :: TRACKING_NORMAL
|
|
||||||
H4_LEADING :: LEADING_SNUG
|
|
||||||
|
|
||||||
// H5 — small caps section header (uses FG_SECONDARY).
|
|
||||||
H5_SIZE :: TEXT_BASE
|
|
||||||
H5_WEIGHT :: WEIGHT_SEMIBOLD
|
|
||||||
H5_TRACKING :: TRACKING_WIDER
|
|
||||||
H5_LEADING :: LEADING_SNUG
|
|
||||||
|
|
||||||
// H6 — mono caps eyebrow / overline (uses FG_CAPTION).
|
|
||||||
H6_SIZE :: TEXT_SM
|
|
||||||
H6_WEIGHT :: WEIGHT_REGULAR
|
|
||||||
H6_TRACKING :: TRACKING_WIDEST
|
|
||||||
H6_LEADING :: LEADING_SNUG
|
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// LABEL ROLES — small caps annotation chips
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
// .label — sans condensed, ALL CAPS, FG_CAPTION.
|
|
||||||
LABEL_SIZE :: TEXT_XS
|
|
||||||
LABEL_WEIGHT :: WEIGHT_SEMIBOLD
|
|
||||||
LABEL_TRACKING :: TRACKING_WIDEST
|
|
||||||
|
|
||||||
// .label-mono — mono ALL CAPS, FG_MUTED.
|
|
||||||
LABEL_MONO_SIZE :: TEXT_XS
|
|
||||||
LABEL_MONO_WEIGHT :: WEIGHT_REGULAR
|
|
||||||
LABEL_MONO_TRACKING :: TRACKING_WIDEST
|
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// FOCUS RING
|
|
||||||
// 1px solid gold outline at 2px offset. Crisp, never blurry.
|
|
||||||
// No glow, no box-shadow halo.
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
FOCUS_RING_WIDTH :: 1
|
|
||||||
FOCUS_RING_OFFSET :: 2
|
|
||||||
FOCUS_RING_COLOR :: BORDER_FOCUS // GOLD_BRIGHT
|
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// COMPONENT — BUTTONS
|
|
||||||
// Cybersteel buttons are uppercase, semibold→bold, with wide
|
|
||||||
// tracking. Default size is "md"; sm/lg shift padding + size.
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
// Default (md) padding: vertical / horizontal
|
|
||||||
BUTTON_PAD_Y :: 8
|
|
||||||
BUTTON_PAD_X :: 18
|
|
||||||
BUTTON_FONT_SIZE :: 12
|
|
||||||
BUTTON_FONT_WEIGHT :: WEIGHT_BOLD
|
|
||||||
BUTTON_TRACKING :: 0.07 // EM — ALL CAPS button label
|
|
||||||
BUTTON_RADIUS :: RADIUS_MD
|
|
||||||
BUTTON_BORDER :: BORDER_WIDTH_DEFAULT
|
|
||||||
|
|
||||||
// Small button
|
|
||||||
BUTTON_SM_PAD_Y :: 5
|
|
||||||
BUTTON_SM_PAD_X :: 12
|
|
||||||
BUTTON_SM_FONT_SIZE :: 10
|
|
||||||
|
|
||||||
// Large button
|
|
||||||
BUTTON_LG_PAD_Y :: 11
|
|
||||||
BUTTON_LG_PAD_X :: 24
|
|
||||||
BUTTON_LG_FONT_SIZE :: 14
|
|
||||||
|
|
||||||
// Primary — solid gold fill, dark text. Hover brightens, press
|
|
||||||
// flips to fg-heading (cream) fill.
|
|
||||||
BUTTON_PRIMARY_BG :: GOLD_DIM
|
|
||||||
BUTTON_PRIMARY_FG :: BG_SHELL
|
|
||||||
BUTTON_PRIMARY_BORDER :: GOLD_DIM
|
|
||||||
BUTTON_PRIMARY_BG_HOVER :: GOLD_BRIGHT
|
|
||||||
BUTTON_PRIMARY_BORDER_HOVER :: GOLD_BRIGHT
|
|
||||||
BUTTON_PRIMARY_BG_PRESS :: FG_HEADING
|
|
||||||
BUTTON_PRIMARY_FG_PRESS :: BG_SHELL
|
|
||||||
BUTTON_PRIMARY_BORDER_PRESS :: FG_HEADING
|
|
||||||
|
|
||||||
// Secondary — transparent bg, structural border, hover gains
|
|
||||||
// gold tint + gold-dim border, press fills with gold-bright.
|
|
||||||
BUTTON_SECONDARY_BG :: [4]u8{0, 0, 0, 0} // transparent
|
|
||||||
BUTTON_SECONDARY_FG :: FG_SECONDARY
|
|
||||||
BUTTON_SECONDARY_BORDER :: BG_BORDER
|
|
||||||
BUTTON_SECONDARY_BG_HOVER :: TINT_GOLD_HOVER
|
|
||||||
BUTTON_SECONDARY_BORDER_HOVER :: GOLD_DIM
|
|
||||||
BUTTON_SECONDARY_FG_HOVER :: FG_BODY
|
|
||||||
BUTTON_SECONDARY_BG_PRESS :: GOLD_BRIGHT
|
|
||||||
BUTTON_SECONDARY_FG_PRESS :: [4]u8{0xff, 0xff, 0xff, 0xff}
|
|
||||||
BUTTON_SECONDARY_BORDER_PRESS :: GOLD_BRIGHT
|
|
||||||
|
|
||||||
// Ghost — fully transparent, no border. Hover lifts to BG_ACTIVE.
|
|
||||||
BUTTON_GHOST_BG :: [4]u8{0, 0, 0, 0}
|
|
||||||
BUTTON_GHOST_FG :: FG_CAPTION
|
|
||||||
BUTTON_GHOST_BORDER :: [4]u8{0, 0, 0, 0}
|
|
||||||
BUTTON_GHOST_BG_HOVER :: BG_ACTIVE
|
|
||||||
BUTTON_GHOST_FG_HOVER :: FG_BODY
|
|
||||||
BUTTON_GHOST_BG_PRESS :: GOLD_DIM
|
|
||||||
BUTTON_GHOST_FG_PRESS :: [4]u8{0xff, 0xff, 0xff, 0xff}
|
|
||||||
|
|
||||||
// Danger — destructive ghost button.
|
|
||||||
BUTTON_DANGER_BG :: [4]u8{0, 0, 0, 0}
|
|
||||||
BUTTON_DANGER_FG :: RED_BRIGHT
|
|
||||||
BUTTON_DANGER_BORDER :: RED_DIM
|
|
||||||
BUTTON_DANGER_BG_HOVER :: TINT_DANGER_HOVER
|
|
||||||
BUTTON_DANGER_BORDER_HOVER :: RED_BRIGHT
|
|
||||||
BUTTON_DANGER_FG_HOVER :: FG_BODY
|
|
||||||
BUTTON_DANGER_BG_PRESS :: RED_BRIGHT
|
|
||||||
BUTTON_DANGER_FG_PRESS :: [4]u8{0xff, 0xff, 0xff, 0xff}
|
|
||||||
BUTTON_DANGER_BORDER_PRESS :: RED_BRIGHT
|
|
||||||
|
|
||||||
// Success — confirming ghost button.
|
|
||||||
BUTTON_SUCCESS_BG :: [4]u8{0, 0, 0, 0}
|
|
||||||
BUTTON_SUCCESS_FG :: GREEN_BRIGHT
|
|
||||||
BUTTON_SUCCESS_BORDER :: GREEN_DIM
|
|
||||||
BUTTON_SUCCESS_BG_HOVER :: TINT_SUCCESS_HOVER
|
|
||||||
BUTTON_SUCCESS_BORDER_HOVER :: GREEN_BRIGHT
|
|
||||||
BUTTON_SUCCESS_FG_HOVER :: FG_BODY
|
|
||||||
BUTTON_SUCCESS_BG_PRESS :: GREEN_BRIGHT
|
|
||||||
BUTTON_SUCCESS_FG_PRESS :: [4]u8{0xff, 0xff, 0xff, 0xff}
|
|
||||||
BUTTON_SUCCESS_BORDER_PRESS :: GREEN_BRIGHT
|
|
||||||
|
|
||||||
// Disabled — flat low-contrast surface, opacity-dimmed.
|
|
||||||
BUTTON_DISABLED_BG :: BG_ACTIVE
|
|
||||||
BUTTON_DISABLED_FG :: FG_MUTED
|
|
||||||
BUTTON_DISABLED_BORDER :: BG_BORDER
|
|
||||||
BUTTON_DISABLED_OPACITY :: 0.5
|
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// COMPONENT — CARDS
|
|
||||||
// Flat, structural, mechanical. Background sits one step above
|
|
||||||
// page; border is structural by default and shifts to gold-dim
|
|
||||||
// on hover/press. Corner radius is the default 6px (RADIUS_MD).
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
CARD_BG :: BG_SURFACE
|
|
||||||
CARD_BORDER :: BG_BORDER
|
|
||||||
CARD_BORDER_HOVER :: GOLD_DIM
|
|
||||||
CARD_BG_PRESS :: BG_ACTIVE
|
|
||||||
CARD_BORDER_PRESS :: GOLD_DIM
|
|
||||||
CARD_RADIUS :: RADIUS_MD
|
|
||||||
CARD_BORDER_WIDTH :: BORDER_WIDTH_DEFAULT
|
|
||||||
CARD_PADDING :: SPACE_GROUP // 16px default inset
|
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// COMPONENT — INPUTS
|
|
||||||
// Inputs sit on BG_SURFACE with structural borders. Focus
|
|
||||||
// promotes the border to gold-bright; the focus ring follows.
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
INPUT_BG :: BG_SURFACE
|
|
||||||
INPUT_FG :: FG_BODY
|
|
||||||
INPUT_PLACEHOLDER :: FG_CAPTION
|
|
||||||
INPUT_BORDER :: BG_BORDER
|
|
||||||
INPUT_BORDER_HOVER :: GOLD_DIM
|
|
||||||
INPUT_BORDER_FOCUS :: GOLD_BRIGHT
|
|
||||||
INPUT_BORDER_DANGER :: RED_DIM
|
|
||||||
INPUT_RADIUS :: RADIUS_MD
|
|
||||||
INPUT_PAD_Y :: SPACE_COMPONENT // 12
|
|
||||||
INPUT_PAD_X :: SPACE_GROUP // 16
|
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// COMPONENT — BADGES / STATUS PILLS
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
BADGE_FONT_SIZE :: TEXT_XS
|
|
||||||
BADGE_WEIGHT :: WEIGHT_SEMIBOLD
|
|
||||||
BADGE_TRACKING :: TRACKING_WIDEST
|
|
||||||
BADGE_PAD_Y :: SPACE_CHIP // 4
|
|
||||||
BADGE_PAD_X :: SPACE_ELEMENT // 8
|
|
||||||
BADGE_RADIUS :: RADIUS_SM
|
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// COMPONENT — DECO RULE
|
|
||||||
// Hairline Art Deco horizontal rule: 1px gold-dim top + 1px
|
|
||||||
// structural drop, with panel-sized vertical margins.
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
DECO_RULE_TOP_WIDTH :: 1
|
|
||||||
DECO_RULE_TOP_COLOR :: GOLD_DIM
|
|
||||||
DECO_RULE_DROP_WIDTH :: 1
|
|
||||||
DECO_RULE_DROP_COLOR :: BG_BORDER
|
|
||||||
DECO_RULE_MARGIN_Y :: SPACE_PANEL // 24
|
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// LAYOUT — FIXED CHROME WIDTHS
|
|
||||||
// Sidebar widths are fixed; content lives in 8 or 12 column
|
|
||||||
// grids. No responsive collapsing for chrome — Cybersteel UIs
|
|
||||||
// run on real workstations.
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
SIDEBAR_WIDTH_NARROW :: 240
|
|
||||||
SIDEBAR_WIDTH_WIDE :: 280
|
|
||||||
|
|
||||||
GRID_COLUMNS_NARROW :: 8
|
|
||||||
GRID_COLUMNS_WIDE :: 12
|
|
||||||
|
|
||||||
// Toolbar height matches SPACE_BLOCK so vertical rhythm aligns.
|
|
||||||
TOOLBAR_HEIGHT :: SPACE_BLOCK // 32
|
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// CODE BLOCKS — <pre>
|
|
||||||
// Mono, BG_SHELL surface with a 1px structural border and a
|
|
||||||
// 2px gold-dim accent on the left edge.
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
CODE_INLINE_BG :: BG_SURFACE
|
|
||||||
CODE_INLINE_FG :: GOLD_BRIGHT
|
|
||||||
CODE_INLINE_BORDER :: BG_BORDER
|
|
||||||
CODE_INLINE_PAD_Y :: 2
|
|
||||||
CODE_INLINE_PAD_X :: 6
|
|
||||||
CODE_INLINE_RADIUS :: RADIUS_SM
|
|
||||||
|
|
||||||
PRE_BG :: BG_SHELL
|
|
||||||
PRE_FG :: FG_BODY
|
|
||||||
PRE_BORDER :: BG_BORDER
|
|
||||||
PRE_BORDER_LEFT_COLOR :: GOLD_DIM
|
|
||||||
PRE_BORDER_LEFT_WIDTH :: BORDER_WIDTH_ACCENT // 2
|
|
||||||
PRE_PAD_Y :: SPACE_GROUP // 16
|
|
||||||
PRE_PAD_X :: SPACE_PANEL // 24
|
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// SCANLINE OVERLAY (opt-in, terminal surfaces only)
|
|
||||||
// Repeating-stripe pattern at very low opacity. Stripe is 2 logical
|
|
||||||
// pixels transparent + 2 logical pixels black-at-3% (TINT_SCANLINE).
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
SCANLINE_STRIPE_LPX :: 2
|
|
||||||
SCANLINE_GAP_LPX :: 2
|
|
||||||
SCANLINE_COLOR :: TINT_SCANLINE
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+629
-517
File diff suppressed because it is too large
Load Diff
+27
-39
@@ -1,71 +1,54 @@
|
|||||||
package draw_qr
|
package draw_qr
|
||||||
|
|
||||||
import "core:mem"
|
|
||||||
import "core:slice"
|
|
||||||
|
|
||||||
import draw ".."
|
import draw ".."
|
||||||
import "../../qrcode"
|
import "../../qrcode"
|
||||||
|
|
||||||
DFT_QR_DARK :: draw.BLACK // Default QR code dark module color.
|
DFT_QR_DARK :: draw.BLACK // Default QR code dark module color.
|
||||||
DFT_QR_LIGHT :: draw.WHITE // Default QR code light module color.
|
DFT_QR_LIGHT :: draw.WHITE // Default QR code light module color.
|
||||||
DFT_QR_BOOST_ECL :: true // Default QR error correction level boost.
|
DFT_QR_BOOST_ECL :: true // Default QR error correction level boost.
|
||||||
DFT_QR_QUIET_ZONE :: 4 // Default light-pixel border on each side; 4 is the QR spec value.
|
|
||||||
|
|
||||||
// Returns the number of bytes to_texture will write. Equals dim*dim*4 where
|
// Returns the number of bytes to_texture will write for the given encoded
|
||||||
// dim = qrcode.get_size(qrcode_buf) + 2*quiet_zone.
|
// QR buffer. Equivalent to size*size*4 where size = qrcode.get_size(qrcode_buf).
|
||||||
texture_size :: #force_inline proc(qrcode_buf: []u8, quiet_zone: int = DFT_QR_QUIET_ZONE) -> int {
|
texture_size :: #force_inline proc(qrcode_buf: []u8) -> int {
|
||||||
size := qrcode.get_size(qrcode_buf)
|
size := qrcode.get_size(qrcode_buf)
|
||||||
if size == 0 || quiet_zone < 0 do return 0
|
return size * size * 4
|
||||||
padded_size := size + 2 * quiet_zone
|
|
||||||
return padded_size * padded_size * 4
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decodes an encoded QR buffer into tightly-packed RGBA pixel data written to
|
// Decodes an encoded QR buffer into tightly-packed RGBA pixel data written to
|
||||||
// texture_buf. No allocations, no GPU calls. Returns the Texture_Desc the
|
// texture_buf. No allocations, no GPU calls. Returns the Texture_Desc the
|
||||||
// caller should pass to draw.register_texture alongside texture_buf.
|
// caller should pass to draw.register_texture alongside texture_buf.
|
||||||
//
|
//
|
||||||
// quiet_zone adds that many `light` pixels on each side; the spec value is 4.
|
|
||||||
// Final dimension is qrcode.get_size + 2*quiet_zone on each axis.
|
|
||||||
//
|
|
||||||
// Returns ok=false when:
|
// Returns ok=false when:
|
||||||
// - qrcode_buf is invalid (qrcode.get_size returns 0).
|
// - qrcode_buf is invalid (qrcode.get_size returns 0).
|
||||||
// - quiet_zone is negative.
|
// - texture_buf is smaller than to_texture_size(qrcode_buf).
|
||||||
// - texture_buf is smaller than texture_size(qrcode_buf, quiet_zone).
|
|
||||||
@(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 = DFT_QR_DARK,
|
||||||
light: draw.Color = DFT_QR_LIGHT,
|
light: draw.Color = DFT_QR_LIGHT,
|
||||||
quiet_zone: int = DFT_QR_QUIET_ZONE,
|
|
||||||
) -> (
|
) -> (
|
||||||
desc: draw.Texture_Desc,
|
desc: draw.Texture_Desc,
|
||||||
ok: bool,
|
ok: bool,
|
||||||
) {
|
) {
|
||||||
size := qrcode.get_size(qrcode_buf)
|
size := qrcode.get_size(qrcode_buf)
|
||||||
if size == 0 || quiet_zone < 0 do return
|
if size == 0 do return {}, false
|
||||||
padded_size := size + 2 * quiet_zone
|
if len(texture_buf) < size * size * 4 do return {}, false
|
||||||
if len(texture_buf) < padded_size * padded_size * 4 do return
|
|
||||||
|
|
||||||
// Type-pun to []Color so each store is a single 32-bit write.
|
|
||||||
pixels := mem.slice_data_cast([]draw.Color, texture_buf[:padded_size * padded_size * 4])
|
|
||||||
|
|
||||||
// Bulk-fill with light: handles the border and every light QR module at once.
|
|
||||||
slice.fill(pixels, light)
|
|
||||||
|
|
||||||
// Overwrite only the dark modules, offset by the quiet-zone border.
|
|
||||||
for y in 0 ..< size {
|
for y in 0 ..< size {
|
||||||
row := (y + quiet_zone) * padded_size + quiet_zone
|
|
||||||
for x in 0 ..< size {
|
for x in 0 ..< size {
|
||||||
if qrcode.get_module(qrcode_buf, x, y) {
|
i := (y * size + x) * 4
|
||||||
pixels[row + x] = dark
|
c := dark if qrcode.get_module(qrcode_buf, x, y) else light
|
||||||
}
|
texture_buf[i + 0] = c[0]
|
||||||
|
texture_buf[i + 1] = c[1]
|
||||||
|
texture_buf[i + 2] = c[2]
|
||||||
|
texture_buf[i + 3] = c[3]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return draw.Texture_Desc {
|
return draw.Texture_Desc {
|
||||||
width = u32(padded_size),
|
width = u32(size),
|
||||||
height = u32(padded_size),
|
height = u32(size),
|
||||||
depth_or_layers = 1,
|
depth_or_layers = 1,
|
||||||
type = .D2,
|
type = .D2,
|
||||||
format = .R8G8B8A8_UNORM,
|
format = .R8G8B8A8_UNORM,
|
||||||
@@ -88,20 +71,19 @@ register_texture_from_raw :: proc(
|
|||||||
qrcode_buf: []u8,
|
qrcode_buf: []u8,
|
||||||
dark: draw.Color = DFT_QR_DARK,
|
dark: draw.Color = DFT_QR_DARK,
|
||||||
light: draw.Color = DFT_QR_LIGHT,
|
light: draw.Color = DFT_QR_LIGHT,
|
||||||
quiet_zone: int = DFT_QR_QUIET_ZONE,
|
|
||||||
temp_allocator := context.temp_allocator,
|
temp_allocator := context.temp_allocator,
|
||||||
) -> (
|
) -> (
|
||||||
texture: draw.Texture_Id,
|
texture: draw.Texture_Id,
|
||||||
ok: bool,
|
ok: bool,
|
||||||
) {
|
) {
|
||||||
tex_size := texture_size(qrcode_buf, quiet_zone)
|
tex_size := texture_size(qrcode_buf)
|
||||||
if tex_size == 0 do return draw.INVALID_TEXTURE, false
|
if tex_size == 0 do return draw.INVALID_TEXTURE, false
|
||||||
|
|
||||||
pixels, alloc_err := make([]u8, tex_size, temp_allocator)
|
pixels, alloc_err := make([]u8, tex_size, temp_allocator)
|
||||||
if alloc_err != nil do return draw.INVALID_TEXTURE, false
|
if alloc_err != nil do return draw.INVALID_TEXTURE, false
|
||||||
defer delete(pixels, temp_allocator)
|
defer delete(pixels, temp_allocator)
|
||||||
|
|
||||||
desc := to_texture(qrcode_buf, pixels, dark, light, quiet_zone) or_return
|
desc := to_texture(qrcode_buf, pixels, dark, light) or_return
|
||||||
return draw.register_texture(desc, pixels)
|
return draw.register_texture(desc, pixels)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,7 +103,6 @@ register_texture_from_text :: proc(
|
|||||||
boost_ecl: bool = DFT_QR_BOOST_ECL,
|
boost_ecl: bool = DFT_QR_BOOST_ECL,
|
||||||
dark: draw.Color = DFT_QR_DARK,
|
dark: draw.Color = DFT_QR_DARK,
|
||||||
light: draw.Color = DFT_QR_LIGHT,
|
light: draw.Color = DFT_QR_LIGHT,
|
||||||
quiet_zone: int = DFT_QR_QUIET_ZONE,
|
|
||||||
temp_allocator := context.temp_allocator,
|
temp_allocator := context.temp_allocator,
|
||||||
) -> (
|
) -> (
|
||||||
texture: draw.Texture_Id,
|
texture: draw.Texture_Id,
|
||||||
@@ -142,7 +123,7 @@ register_texture_from_text :: proc(
|
|||||||
temp_allocator,
|
temp_allocator,
|
||||||
) or_return
|
) or_return
|
||||||
|
|
||||||
return register_texture_from_raw(qrcode_buf, dark, light, quiet_zone, temp_allocator)
|
return register_texture_from_raw(qrcode_buf, dark, light, temp_allocator)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encodes arbitrary binary data as a QR Code and registers the result as an RGBA texture.
|
// Encodes arbitrary binary data as a QR Code and registers the result as an RGBA texture.
|
||||||
@@ -161,7 +142,6 @@ register_texture_from_binary :: proc(
|
|||||||
boost_ecl: bool = DFT_QR_BOOST_ECL,
|
boost_ecl: bool = DFT_QR_BOOST_ECL,
|
||||||
dark: draw.Color = DFT_QR_DARK,
|
dark: draw.Color = DFT_QR_DARK,
|
||||||
light: draw.Color = DFT_QR_LIGHT,
|
light: draw.Color = DFT_QR_LIGHT,
|
||||||
quiet_zone: int = DFT_QR_QUIET_ZONE,
|
|
||||||
temp_allocator := context.temp_allocator,
|
temp_allocator := context.temp_allocator,
|
||||||
) -> (
|
) -> (
|
||||||
texture: draw.Texture_Id,
|
texture: draw.Texture_Id,
|
||||||
@@ -182,10 +162,18 @@ register_texture_from_binary :: proc(
|
|||||||
temp_allocator,
|
temp_allocator,
|
||||||
) or_return
|
) or_return
|
||||||
|
|
||||||
return register_texture_from_raw(qrcode_buf, dark, light, quiet_zone, temp_allocator)
|
return register_texture_from_raw(qrcode_buf, dark, light, temp_allocator)
|
||||||
}
|
}
|
||||||
|
|
||||||
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.
|
||||||
|
clay_image :: #force_inline proc(
|
||||||
|
texture: draw.Texture_Id,
|
||||||
|
tint: draw.Color = draw.DFT_TINT,
|
||||||
|
) -> draw.Clay_Image_Data {
|
||||||
|
return draw.clay_image_data(texture, fit = .Fit, tint = tint)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,363 +0,0 @@
|
|||||||
package examples
|
|
||||||
|
|
||||||
import "core:os"
|
|
||||||
import sdl "vendor:sdl3"
|
|
||||||
|
|
||||||
import "../../draw"
|
|
||||||
import "../../vendor/clay"
|
|
||||||
import cyber "../cybersteel"
|
|
||||||
|
|
||||||
// Clay border debug example.
|
|
||||||
//
|
|
||||||
// Lays out a grid of bordered Clay elements that exercise every code path in
|
|
||||||
// `clay_emit_partial_border` and `try_dispatch_clay_rect_border_pair`:
|
|
||||||
//
|
|
||||||
// 1. Uniform borders (fast path) — sharp, rounded, and the border-thicker-than-radius
|
|
||||||
// edge case (inner corner clamps to 0).
|
|
||||||
// 2. Background + border combinations — opaque bg + opaque uniform border MERGES into one
|
|
||||||
// SDF primitive; translucent border DECLINES the merge to preserve blend fidelity;
|
|
||||||
// non-uniform border declines and falls through to the slow path; translucent bg with
|
|
||||||
// opaque border still merges (bg alpha doesn't affect merge correctness).
|
|
||||||
// 3. Single-side borders — top / right / bottom / left individually.
|
|
||||||
// 4. Two-side borders — parallel pairs (no corners drawn) and adjacent pairs (one corner
|
|
||||||
// rounds, others stay square).
|
|
||||||
// 5. Three-side borders + asymmetric widths.
|
|
||||||
// 6. Layout correctness — a vertical list with bottom-border separators (each border
|
|
||||||
// lives inside its own item, no bleed between siblings) and a row of adjacent fully
|
|
||||||
// bordered siblings (no border overlap, each in its own bounds).
|
|
||||||
clay_borders :: proc() {
|
|
||||||
if !sdl.Init({.VIDEO}) do os.exit(1)
|
|
||||||
window := sdl.CreateWindow("Clay Borders Debug", 1200, 900, {.HIGH_PIXEL_DENSITY})
|
|
||||||
gpu := sdl.CreateGPUDevice(draw.PLATFORM_SHADER_FORMAT, true, nil)
|
|
||||||
if !sdl.ClaimWindowForGPUDevice(gpu, window) do os.exit(1)
|
|
||||||
if !draw.init(gpu, window) do os.exit(1)
|
|
||||||
PLEX_SANS_REGULAR = draw.register_font(cyber.SANS_REGULAR_RAW)
|
|
||||||
|
|
||||||
// Distinct colors so the fill, border, and translucent variants are visually unambiguous.
|
|
||||||
BG_PAGE :: draw.Color{25, 25, 30, 255}
|
|
||||||
FILL_OPAQUE :: draw.Color{80, 120, 200, 255}
|
|
||||||
FILL_TRANSLUCENT :: draw.Color{80, 120, 200, 128}
|
|
||||||
BORDER_OPAQUE :: draw.Color{255, 200, 100, 255}
|
|
||||||
BORDER_TRANSLUCENT :: draw.Color{255, 200, 100, 128}
|
|
||||||
|
|
||||||
label_config := clay.TextElementConfig {
|
|
||||||
fontId = PLEX_SANS_REGULAR,
|
|
||||||
fontSize = 12,
|
|
||||||
textColor = {220, 220, 220, 255},
|
|
||||||
}
|
|
||||||
header_config := clay.TextElementConfig {
|
|
||||||
fontId = PLEX_SANS_REGULAR,
|
|
||||||
fontSize = 16,
|
|
||||||
textColor = {255, 255, 255, 255},
|
|
||||||
}
|
|
||||||
title_config := clay.TextElementConfig {
|
|
||||||
fontId = PLEX_SANS_REGULAR,
|
|
||||||
fontSize = 22,
|
|
||||||
textColor = {255, 255, 255, 255},
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
defer free_all(context.temp_allocator)
|
|
||||||
ev: sdl.Event
|
|
||||||
for sdl.PollEvent(&ev) {
|
|
||||||
if ev.type == .QUIT do return
|
|
||||||
}
|
|
||||||
|
|
||||||
base_layer := draw.begin({width = 1200, height = 900})
|
|
||||||
clay.SetLayoutDimensions({width = base_layer.bounds.width, height = base_layer.bounds.height})
|
|
||||||
clay.BeginLayout()
|
|
||||||
|
|
||||||
if clay.UI(clay.ID("borders_page"))(
|
|
||||||
{
|
|
||||||
layout = {
|
|
||||||
sizing = {clay.SizingGrow({}), clay.SizingGrow({})},
|
|
||||||
padding = clay.PaddingAll(20),
|
|
||||||
childGap = 14,
|
|
||||||
layoutDirection = .TopToBottom,
|
|
||||||
},
|
|
||||||
backgroundColor = clay_color(BG_PAGE),
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
clay.Text("Clay Borders Debug", title_config)
|
|
||||||
|
|
||||||
//----- Section 1: Uniform borders (fast path) -----------------------------------
|
|
||||||
clay.Text("Uniform borders (fast path)", header_config)
|
|
||||||
if clay.UI(clay.ID("row_uniform"))(border_row_layout()) {
|
|
||||||
border_test_card(
|
|
||||||
"1px sharp",
|
|
||||||
label_config,
|
|
||||||
FILL_OPAQUE,
|
|
||||||
BORDER_OPAQUE,
|
|
||||||
{left = 1, right = 1, top = 1, bottom = 1},
|
|
||||||
{},
|
|
||||||
)
|
|
||||||
border_test_card(
|
|
||||||
"2px, radius 8",
|
|
||||||
label_config,
|
|
||||||
FILL_OPAQUE,
|
|
||||||
BORDER_OPAQUE,
|
|
||||||
{left = 2, right = 2, top = 2, bottom = 2},
|
|
||||||
{topLeft = 8, topRight = 8, bottomRight = 8, bottomLeft = 8},
|
|
||||||
)
|
|
||||||
border_test_card(
|
|
||||||
"8px, radius 20",
|
|
||||||
label_config,
|
|
||||||
FILL_OPAQUE,
|
|
||||||
BORDER_OPAQUE,
|
|
||||||
{left = 8, right = 8, top = 8, bottom = 8},
|
|
||||||
{topLeft = 20, topRight = 20, bottomRight = 20, bottomLeft = 20},
|
|
||||||
)
|
|
||||||
border_test_card(
|
|
||||||
"10px > radius 5 (inner clamps)",
|
|
||||||
label_config,
|
|
||||||
FILL_OPAQUE,
|
|
||||||
BORDER_OPAQUE,
|
|
||||||
{left = 10, right = 10, top = 10, bottom = 10},
|
|
||||||
{topLeft = 5, topRight = 5, bottomRight = 5, bottomLeft = 5},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
//----- Section 2: Background + border (merge optimization) ----------------------
|
|
||||||
clay.Text("Background + border (merge optimization)", header_config)
|
|
||||||
if clay.UI(clay.ID("row_bg_border"))(border_row_layout()) {
|
|
||||||
border_test_card(
|
|
||||||
"opaque bg + opaque (MERGES: 1 prim)",
|
|
||||||
label_config,
|
|
||||||
FILL_OPAQUE,
|
|
||||||
BORDER_OPAQUE,
|
|
||||||
{left = 2, right = 2, top = 2, bottom = 2},
|
|
||||||
{topLeft = 6, topRight = 6, bottomRight = 6, bottomLeft = 6},
|
|
||||||
)
|
|
||||||
border_test_card(
|
|
||||||
"translucent bg + opaque (MERGES)",
|
|
||||||
label_config,
|
|
||||||
FILL_TRANSLUCENT,
|
|
||||||
BORDER_OPAQUE,
|
|
||||||
{left = 3, right = 3, top = 3, bottom = 3},
|
|
||||||
{topLeft = 6, topRight = 6, bottomRight = 6, bottomLeft = 6},
|
|
||||||
)
|
|
||||||
border_test_card(
|
|
||||||
"opaque bg + translucent (NO merge)",
|
|
||||||
label_config,
|
|
||||||
FILL_OPAQUE,
|
|
||||||
BORDER_TRANSLUCENT,
|
|
||||||
{left = 4, right = 4, top = 4, bottom = 4},
|
|
||||||
{topLeft = 8, topRight = 8, bottomRight = 8, bottomLeft = 8},
|
|
||||||
)
|
|
||||||
border_test_card(
|
|
||||||
"opaque bg + non-uniform (NO merge)",
|
|
||||||
label_config,
|
|
||||||
FILL_OPAQUE,
|
|
||||||
BORDER_OPAQUE,
|
|
||||||
{left = 1, right = 4, top = 2, bottom = 3},
|
|
||||||
{topLeft = 6, topRight = 6, bottomRight = 6, bottomLeft = 6},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
//----- Section 3: Single side borders -------------------------------------------
|
|
||||||
clay.Text("Single side", header_config)
|
|
||||||
if clay.UI(clay.ID("row_single_side"))(border_row_layout()) {
|
|
||||||
border_test_card("top only (4px)", label_config, FILL_OPAQUE, BORDER_OPAQUE, {top = 4}, {})
|
|
||||||
border_test_card("right only (4px)", label_config, FILL_OPAQUE, BORDER_OPAQUE, {right = 4}, {})
|
|
||||||
border_test_card(
|
|
||||||
"bottom only (4px, divider)",
|
|
||||||
label_config,
|
|
||||||
FILL_OPAQUE,
|
|
||||||
BORDER_OPAQUE,
|
|
||||||
{bottom = 4},
|
|
||||||
{},
|
|
||||||
)
|
|
||||||
border_test_card("left only (4px)", label_config, FILL_OPAQUE, BORDER_OPAQUE, {left = 4}, {})
|
|
||||||
}
|
|
||||||
|
|
||||||
//----- Section 4: Two side borders ----------------------------------------------
|
|
||||||
clay.Text("Two sides", header_config)
|
|
||||||
if clay.UI(clay.ID("row_two_sides"))(border_row_layout()) {
|
|
||||||
border_test_card(
|
|
||||||
"T+B parallel (no corners)",
|
|
||||||
label_config,
|
|
||||||
FILL_OPAQUE,
|
|
||||||
BORDER_OPAQUE,
|
|
||||||
{top = 3, bottom = 3},
|
|
||||||
{topLeft = 8, topRight = 8, bottomRight = 8, bottomLeft = 8},
|
|
||||||
)
|
|
||||||
border_test_card(
|
|
||||||
"L+R parallel (no corners)",
|
|
||||||
label_config,
|
|
||||||
FILL_OPAQUE,
|
|
||||||
BORDER_OPAQUE,
|
|
||||||
{left = 3, right = 3},
|
|
||||||
{topLeft = 8, topRight = 8, bottomRight = 8, bottomLeft = 8},
|
|
||||||
)
|
|
||||||
border_test_card(
|
|
||||||
"T+L adjacent (TL rounds)",
|
|
||||||
label_config,
|
|
||||||
FILL_OPAQUE,
|
|
||||||
BORDER_OPAQUE,
|
|
||||||
{top = 3, left = 3},
|
|
||||||
{topLeft = 12, topRight = 12, bottomRight = 12, bottomLeft = 12},
|
|
||||||
)
|
|
||||||
border_test_card(
|
|
||||||
"B+R adjacent (BR rounds)",
|
|
||||||
label_config,
|
|
||||||
FILL_OPAQUE,
|
|
||||||
BORDER_OPAQUE,
|
|
||||||
{bottom = 3, right = 3},
|
|
||||||
{topLeft = 12, topRight = 12, bottomRight = 12, bottomLeft = 12},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
//----- Section 5: Three sides + asymmetric widths -------------------------------
|
|
||||||
clay.Text("Three sides + asymmetric widths", header_config)
|
|
||||||
if clay.UI(clay.ID("row_advanced"))(border_row_layout()) {
|
|
||||||
border_test_card(
|
|
||||||
"T+R+B (no L), rounded",
|
|
||||||
label_config,
|
|
||||||
FILL_OPAQUE,
|
|
||||||
BORDER_OPAQUE,
|
|
||||||
{top = 3, right = 3, bottom = 3},
|
|
||||||
{topLeft = 8, topRight = 8, bottomRight = 8, bottomLeft = 8},
|
|
||||||
)
|
|
||||||
border_test_card(
|
|
||||||
"T+L+R (no B), rounded",
|
|
||||||
label_config,
|
|
||||||
FILL_OPAQUE,
|
|
||||||
BORDER_OPAQUE,
|
|
||||||
{top = 3, left = 3, right = 3},
|
|
||||||
{topLeft = 8, topRight = 8, bottomRight = 8, bottomLeft = 8},
|
|
||||||
)
|
|
||||||
border_test_card(
|
|
||||||
"asym 1/2/3/4 T/R/B/L",
|
|
||||||
label_config,
|
|
||||||
FILL_OPAQUE,
|
|
||||||
BORDER_OPAQUE,
|
|
||||||
{top = 1, right = 2, bottom = 3, left = 4},
|
|
||||||
{},
|
|
||||||
)
|
|
||||||
border_test_card(
|
|
||||||
"asym + rounded",
|
|
||||||
label_config,
|
|
||||||
FILL_OPAQUE,
|
|
||||||
BORDER_OPAQUE,
|
|
||||||
{top = 2, right = 4, bottom = 2, left = 4},
|
|
||||||
{topLeft = 10, topRight = 10, bottomRight = 10, bottomLeft = 10},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
//----- Section 6: Layout correctness --------------------------------------------
|
|
||||||
clay.Text("Layout correctness", header_config)
|
|
||||||
if clay.UI(clay.ID("row_correctness"))(
|
|
||||||
{layout = {sizing = {clay.SizingGrow({}), clay.SizingFit({})}, childGap = 14}},
|
|
||||||
) {
|
|
||||||
// 6a: vertical list with per-item bottom-border separator. Each item's
|
|
||||||
// border draws INSIDE its own bounds, so adjacent items don't bleed.
|
|
||||||
if clay.UI(clay.ID("list_demo"))(
|
|
||||||
{
|
|
||||||
layout = {
|
|
||||||
sizing = {clay.SizingFixed(300), clay.SizingFit({})},
|
|
||||||
padding = clay.PaddingAll(6),
|
|
||||||
childGap = 6,
|
|
||||||
layoutDirection = .TopToBottom,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
clay.Text("List with bottom-border separators", label_config)
|
|
||||||
if clay.UI(clay.ID("list_outer"))(
|
|
||||||
{
|
|
||||||
layout = {sizing = {clay.SizingGrow({}), clay.SizingFit({})}, layoutDirection = .TopToBottom},
|
|
||||||
backgroundColor = clay_color(FILL_OPAQUE),
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
for index in 0 ..< 5 {
|
|
||||||
if clay.UI(clay.ID("list_item", u32(index)))(
|
|
||||||
{
|
|
||||||
layout = {sizing = {clay.SizingGrow({}), clay.SizingFixed(28)}, padding = clay.PaddingAll(6)},
|
|
||||||
border = {color = clay_color(BORDER_OPAQUE), width = {bottom = 1}},
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
clay.Text("Item", label_config)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6b: row of adjacent fully bordered siblings. With borders rendered
|
|
||||||
// INSIDE each element's bounds, the boundary between two siblings shows
|
|
||||||
// the natural 2*width sum (no overlap, no bleed).
|
|
||||||
if clay.UI(clay.ID("adj_demo"))(
|
|
||||||
{
|
|
||||||
layout = {
|
|
||||||
sizing = {clay.SizingFixed(380), clay.SizingFit({})},
|
|
||||||
padding = clay.PaddingAll(6),
|
|
||||||
childGap = 6,
|
|
||||||
layoutDirection = .TopToBottom,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
clay.Text("Adjacent bordered siblings (no gap)", label_config)
|
|
||||||
if clay.UI(clay.ID("adj_row"))({layout = {sizing = {clay.SizingGrow({}), clay.SizingFit({})}}}) {
|
|
||||||
for index in 0 ..< 4 {
|
|
||||||
if clay.UI(clay.ID("adj_item", u32(index)))(
|
|
||||||
{
|
|
||||||
layout = {sizing = {clay.SizingFixed(80), clay.SizingFixed(60)}},
|
|
||||||
backgroundColor = clay_color(FILL_OPAQUE),
|
|
||||||
border = {color = clay_color(BORDER_OPAQUE), width = {left = 2, right = 2, top = 2, bottom = 2}},
|
|
||||||
},
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clay_batch := draw.ClayBatch {
|
|
||||||
bounds = base_layer.bounds,
|
|
||||||
cmds = clay.EndLayout(0),
|
|
||||||
}
|
|
||||||
draw.prepare_clay_batch(base_layer, &clay_batch)
|
|
||||||
draw.end(gpu, window)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: convert a draw.Color (RGBA u8) to clay.Color (RGBA float in 0-255 range).
|
|
||||||
clay_color :: proc(c: draw.Color) -> clay.Color {
|
|
||||||
return clay.Color{f32(c[0]), f32(c[1]), f32(c[2]), f32(c[3])}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: shared row container declaration for the test sections.
|
|
||||||
border_row_layout :: proc() -> clay.ElementDeclaration {
|
|
||||||
return clay.ElementDeclaration{layout = {sizing = {clay.SizingGrow({}), clay.SizingFit({})}, childGap = 12}}
|
|
||||||
}
|
|
||||||
|
|
||||||
// One labeled test card: a fixed-width column with a caption above and a sample bordered
|
|
||||||
// rectangle below. Uses `clay.ID_LOCAL` for the inner element so each card gets a unique
|
|
||||||
// child ID without the caller passing one explicitly.
|
|
||||||
border_test_card :: proc(
|
|
||||||
label: string,
|
|
||||||
label_config: clay.TextElementConfig,
|
|
||||||
fill_color: draw.Color,
|
|
||||||
border_color: draw.Color,
|
|
||||||
border_width: clay.BorderWidth,
|
|
||||||
corner_radii: clay.CornerRadius,
|
|
||||||
) {
|
|
||||||
if clay.UI(clay.ID(label))(
|
|
||||||
{
|
|
||||||
layout = {
|
|
||||||
sizing = {clay.SizingFixed(275), clay.SizingFit({})},
|
|
||||||
padding = clay.PaddingAll(4),
|
|
||||||
childGap = 6,
|
|
||||||
layoutDirection = .TopToBottom,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
clay.Text(label, label_config)
|
|
||||||
if clay.UI(clay.ID_LOCAL("test_inner"))(
|
|
||||||
{
|
|
||||||
layout = {sizing = {clay.SizingGrow({}), clay.SizingFixed(64)}},
|
|
||||||
backgroundColor = clay_color(fill_color),
|
|
||||||
border = clay.BorderElementConfig{color = clay_color(border_color), width = border_width},
|
|
||||||
cornerRadius = corner_radii,
|
|
||||||
},
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+41
-68
@@ -5,92 +5,65 @@ import "core:log"
|
|||||||
import "core:mem"
|
import "core:mem"
|
||||||
import "core:os"
|
import "core:os"
|
||||||
|
|
||||||
EX_HELLOPE_SHAPES :: "hellope-shapes"
|
|
||||||
EX_HELLOPE_TEXT :: "hellope-text"
|
|
||||||
EX_HELLOPE_CLAY :: "hellope-clay"
|
|
||||||
EX_HELLOPE_CUSTOM :: "hellope-custom"
|
|
||||||
EX_CLAY_BORDERS :: "clay-borders"
|
|
||||||
EX_TEXTURES :: "textures"
|
|
||||||
EX_GAUSSIAN_BLUR :: "gaussian-blur"
|
|
||||||
EX_GAUSSIAN_BLUR_DEBUG :: "gaussian-blur-debug"
|
|
||||||
|
|
||||||
AVAILABLE_EXAMPLES_MSG ::
|
|
||||||
"Available examples: " +
|
|
||||||
EX_HELLOPE_SHAPES +
|
|
||||||
", " +
|
|
||||||
EX_HELLOPE_TEXT +
|
|
||||||
", " +
|
|
||||||
EX_HELLOPE_CLAY +
|
|
||||||
", " +
|
|
||||||
EX_HELLOPE_CUSTOM +
|
|
||||||
", " +
|
|
||||||
EX_CLAY_BORDERS +
|
|
||||||
", " +
|
|
||||||
EX_TEXTURES +
|
|
||||||
", " +
|
|
||||||
EX_GAUSSIAN_BLUR +
|
|
||||||
", " +
|
|
||||||
EX_GAUSSIAN_BLUR_DEBUG
|
|
||||||
|
|
||||||
main :: proc() {
|
main :: proc() {
|
||||||
//----- General setup ----------------------------------
|
//----- General setup ----------------------------------
|
||||||
// Temp
|
{
|
||||||
track_temp: mem.Tracking_Allocator
|
// Temp
|
||||||
mem.tracking_allocator_init(&track_temp, context.temp_allocator)
|
track_temp: mem.Tracking_Allocator
|
||||||
context.temp_allocator = mem.tracking_allocator(&track_temp)
|
mem.tracking_allocator_init(&track_temp, context.temp_allocator)
|
||||||
|
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.
|
// 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.
|
// This could be fine for some global state or it could be a memory leak.
|
||||||
defer {
|
defer {
|
||||||
// Temp allocator
|
// Temp allocator
|
||||||
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
|
||||||
}
|
if len(track.allocation_map) > 0 {
|
||||||
// Default allocator
|
fmt.eprintf("=== %v allocations not freed - main allocator: ===\n", len(track.allocation_map))
|
||||||
if len(track.allocation_map) > 0 {
|
for _, entry in track.allocation_map {
|
||||||
fmt.eprintf("=== %v allocations not freed - main allocator: ===\n", len(track.allocation_map))
|
fmt.eprintf("- %v bytes @ %v\n", entry.size, entry.location)
|
||||||
for _, entry in track.allocation_map {
|
}
|
||||||
fmt.eprintf("- %v bytes @ %v\n", entry.size, entry.location)
|
|
||||||
}
|
}
|
||||||
}
|
if len(track.bad_free_array) > 0 {
|
||||||
if len(track.bad_free_array) > 0 {
|
fmt.eprintf("=== %v incorrect frees - main allocator: ===\n", len(track.bad_free_array))
|
||||||
fmt.eprintf("=== %v incorrect frees - main allocator: ===\n", len(track.bad_free_array))
|
for entry in track.bad_free_array {
|
||||||
for entry in track.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)
|
||||||
}
|
}
|
||||||
mem.tracking_allocator_destroy(&track)
|
// Logger
|
||||||
|
context.logger = log.create_console_logger()
|
||||||
|
defer log.destroy_console_logger(context.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 {
|
||||||
fmt.eprintln("Usage: examples <example_name>")
|
fmt.eprintln("Usage: examples <example_name>")
|
||||||
fmt.eprintln(AVAILABLE_EXAMPLES_MSG)
|
fmt.eprintln("Available examples: hellope-shapes, hellope-text, hellope-clay, hellope-custom, textures")
|
||||||
os.exit(1)
|
os.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch args[1] {
|
switch args[1] {
|
||||||
case EX_HELLOPE_CLAY: hellope_clay()
|
case "hellope-clay": hellope_clay()
|
||||||
case EX_HELLOPE_CUSTOM: hellope_custom()
|
case "hellope-custom": hellope_custom()
|
||||||
case EX_HELLOPE_SHAPES: hellope_shapes()
|
case "hellope-shapes": hellope_shapes()
|
||||||
case EX_HELLOPE_TEXT: hellope_text()
|
case "hellope-text": hellope_text()
|
||||||
case EX_CLAY_BORDERS: clay_borders()
|
case "textures": textures()
|
||||||
case EX_TEXTURES: textures()
|
|
||||||
case EX_GAUSSIAN_BLUR: gaussian_blur()
|
|
||||||
case EX_GAUSSIAN_BLUR_DEBUG: gaussian_blur_debug()
|
|
||||||
case:
|
case:
|
||||||
fmt.eprintf("Unknown example: %v\n", args[1])
|
fmt.eprintf("Unknown example: %v\n", args[1])
|
||||||
fmt.eprintln(AVAILABLE_EXAMPLES_MSG)
|
fmt.eprintln("Available examples: hellope-shapes, hellope-text, hellope-clay, hellope-custom, textures")
|
||||||
os.exit(1)
|
os.exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
+67
-91
@@ -1,15 +1,14 @@
|
|||||||
package examples
|
package examples
|
||||||
|
|
||||||
import "core:math"
|
|
||||||
import "core:os"
|
|
||||||
import sdl "vendor:sdl3"
|
|
||||||
|
|
||||||
import "../../draw"
|
import "../../draw"
|
||||||
import "../../draw/tess"
|
import "../../draw/tess"
|
||||||
import "../../vendor/clay"
|
import "../../vendor/clay"
|
||||||
import cyber "../cybersteel"
|
import "core:math"
|
||||||
|
import "core:os"
|
||||||
|
import sdl "vendor:sdl3"
|
||||||
|
|
||||||
PLEX_SANS_REGULAR: draw.Font_Id = max(draw.Font_Id) // Max so we crash if registration is forgotten
|
JETBRAINS_MONO_REGULAR_RAW :: #load("fonts/JetBrainsMono-Regular.ttf")
|
||||||
|
JETBRAINS_MONO_REGULAR: draw.Font_Id = max(draw.Font_Id) // Max so we crash if registration is forgotten
|
||||||
|
|
||||||
hellope_shapes :: proc() {
|
hellope_shapes :: proc() {
|
||||||
if !sdl.Init({.VIDEO}) do os.exit(1)
|
if !sdl.Init({.VIDEO}) do os.exit(1)
|
||||||
@@ -48,7 +47,8 @@ hellope_shapes :: proc() {
|
|||||||
draw.rectangle(
|
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},
|
||||||
|
gradient = draw.Linear_Gradient{end_color = {0, 0, 255, 255}, angle = 0},
|
||||||
)
|
)
|
||||||
|
|
||||||
// ----- Rotation demos -----
|
// ----- Rotation demos -----
|
||||||
@@ -63,7 +63,7 @@ hellope_shapes :: proc() {
|
|||||||
outline_width = 2,
|
outline_width = 2,
|
||||||
origin = draw.center_of(rect),
|
origin = draw.center_of(rect),
|
||||||
rotation = spin_angle,
|
rotation = spin_angle,
|
||||||
feather_ppx = 1,
|
feather_px = 1,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Rounded rectangle rotating around its center
|
// Rounded rectangle rotating around its center
|
||||||
@@ -78,18 +78,18 @@ hellope_shapes :: proc() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// 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 := draw.Vec2{100, 450}
|
||||||
draw.circle(base_layer, planet_pos, 8, draw.Color{200, 200, 200, 255}) // planet (stationary)
|
draw.circle(base_layer, planet_pos, 8, {200, 200, 200, 255}) // planet (stationary)
|
||||||
draw.circle(
|
draw.circle(
|
||||||
base_layer,
|
base_layer,
|
||||||
planet_pos,
|
planet_pos,
|
||||||
5,
|
5,
|
||||||
draw.Color{100, 150, 255, 255},
|
{100, 150, 255, 255},
|
||||||
origin = draw.Vec2{0, 40},
|
origin = draw.Vec2{0, 40},
|
||||||
rotation = spin_angle,
|
rotation = spin_angle,
|
||||||
) // moon orbiting
|
) // moon orbiting
|
||||||
@@ -100,7 +100,7 @@ hellope_shapes :: proc() {
|
|||||||
draw.Vec2{250, 450},
|
draw.Vec2{250, 450},
|
||||||
0,
|
0,
|
||||||
30,
|
30,
|
||||||
draw.Color{100, 100, 220, 255},
|
{100, 100, 220, 255},
|
||||||
start_angle = 0,
|
start_angle = 0,
|
||||||
end_angle = 270,
|
end_angle = 270,
|
||||||
rotation = spin_angle,
|
rotation = spin_angle,
|
||||||
@@ -126,7 +126,7 @@ hellope_shapes :: proc() {
|
|||||||
{460, 450},
|
{460, 450},
|
||||||
6,
|
6,
|
||||||
30,
|
30,
|
||||||
draw.Color{180, 100, 220, 255},
|
{180, 100, 220, 255},
|
||||||
outline_color = draw.WHITE,
|
outline_color = draw.WHITE,
|
||||||
outline_width = 2,
|
outline_width = 2,
|
||||||
rotation = spin_angle,
|
rotation = spin_angle,
|
||||||
@@ -147,7 +147,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
|
||||||
@@ -168,10 +168,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 +180,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}, draw.Color{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,7 +217,7 @@ 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,
|
||||||
@@ -227,10 +234,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},
|
||||||
}
|
}
|
||||||
@@ -244,8 +251,9 @@ hellope_clay :: proc() {
|
|||||||
base_layer := draw.begin({width = 500, height = 500})
|
base_layer := draw.begin({width = 500, height = 500})
|
||||||
clay.SetLayoutDimensions({width = base_layer.bounds.width, height = base_layer.bounds.height})
|
clay.SetLayoutDimensions({width = base_layer.bounds.width, height = base_layer.bounds.height})
|
||||||
clay.BeginLayout()
|
clay.BeginLayout()
|
||||||
if clay.UI(clay.ID("outer"))(
|
if clay.UI()(
|
||||||
{
|
{
|
||||||
|
id = clay.ID("outer"),
|
||||||
layout = {
|
layout = {
|
||||||
sizing = {clay.SizingGrow({}), clay.SizingGrow({})},
|
sizing = {clay.SizingGrow({}), clay.SizingGrow({})},
|
||||||
childAlignment = {x = .Center, y = .Center},
|
childAlignment = {x = .Center, y = .Center},
|
||||||
@@ -253,13 +261,13 @@ hellope_clay :: proc() {
|
|||||||
backgroundColor = {127, 127, 127, 255},
|
backgroundColor = {127, 127, 127, 255},
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
clay.Text("Hellope!", text_config)
|
clay.Text("Hellope!", &text_config)
|
||||||
}
|
}
|
||||||
clay_batch := draw.ClayBatch {
|
clay_batch := draw.ClayBatch {
|
||||||
bounds = base_layer.bounds,
|
bounds = base_layer.bounds,
|
||||||
cmds = clay.EndLayout(0),
|
cmds = clay.EndLayout(),
|
||||||
}
|
}
|
||||||
draw.prepare_clay_batch(base_layer, &clay_batch)
|
draw.prepare_clay_batch(base_layer, &clay_batch, {0, 0})
|
||||||
draw.end(gpu, window)
|
draw.end(gpu, window)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -270,40 +278,22 @@ 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},
|
||||||
}
|
}
|
||||||
|
|
||||||
gauge := Gauge {
|
gauge := Gauge {
|
||||||
value = 0.73,
|
value = 0.73,
|
||||||
color = {50, 200, 100, 255},
|
color = {50, 200, 100, 255},
|
||||||
bg_color = {80, 80, 80, 255},
|
|
||||||
}
|
}
|
||||||
gauge2 := Gauge {
|
gauge2 := Gauge {
|
||||||
value = 0.45,
|
value = 0.45,
|
||||||
color = {200, 100, 50, 255},
|
color = {200, 100, 50, 255},
|
||||||
bg_color = {80, 80, 80, 255},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// `clay.CustomElementConfig.customData` is a rawptr; the Clay integration in `draw`
|
|
||||||
// requires it to point at a `Clay_Custom` value. The explicit `rawptr(...)` cast is
|
|
||||||
// necessary because Odin does not chain `^Gauge -> rawptr -> Clay_Custom` implicitly
|
|
||||||
// (variant-to-union and ^T-to-rawptr are each implicit on their own, but not stacked).
|
|
||||||
gauge_custom: draw.Clay_Custom = rawptr(&gauge)
|
|
||||||
gauge2_custom: draw.Clay_Custom = rawptr(&gauge2)
|
|
||||||
|
|
||||||
// Backdrop variant: variant-to-union conversion is implicit, so no cast needed.
|
|
||||||
// `tint = draw.WHITE` is the no-op tint per the backdrop module's convention
|
|
||||||
// (matches `examples/backdrop.odin`'s "pure blur, no color" usage).
|
|
||||||
backdrop_custom: draw.Clay_Custom = draw.Backdrop_Marker {
|
|
||||||
sigma = 8,
|
|
||||||
tint = draw.WHITE,
|
|
||||||
}
|
|
||||||
|
|
||||||
spin_angle: f32 = 0
|
spin_angle: f32 = 0
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@@ -321,8 +311,9 @@ hellope_custom :: proc() {
|
|||||||
clay.SetLayoutDimensions({width = base_layer.bounds.width, height = base_layer.bounds.height})
|
clay.SetLayoutDimensions({width = base_layer.bounds.width, height = base_layer.bounds.height})
|
||||||
clay.BeginLayout()
|
clay.BeginLayout()
|
||||||
|
|
||||||
if clay.UI(clay.ID("outer"))(
|
if clay.UI()(
|
||||||
{
|
{
|
||||||
|
id = clay.ID("outer"),
|
||||||
layout = {
|
layout = {
|
||||||
sizing = {clay.SizingGrow({}), clay.SizingGrow({})},
|
sizing = {clay.SizingGrow({}), clay.SizingGrow({})},
|
||||||
childAlignment = {x = .Center, y = .Center},
|
childAlignment = {x = .Center, y = .Center},
|
||||||
@@ -332,68 +323,53 @@ hellope_custom :: proc() {
|
|||||||
backgroundColor = {50, 50, 50, 255},
|
backgroundColor = {50, 50, 50, 255},
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
if clay.UI(clay.ID("title"))({layout = {sizing = {clay.SizingFit({}), clay.SizingFit({})}}}) {
|
if clay.UI()({id = clay.ID("title"), layout = {sizing = {clay.SizingFit({}), clay.SizingFit({})}}}) {
|
||||||
clay.Text("Custom Draw Demo", text_config)
|
clay.Text("Custom Draw Demo", &text_config)
|
||||||
}
|
}
|
||||||
|
|
||||||
// gauge1 is BEHIND the backdrop — the backdrop is declared as a floating CHILD
|
if clay.UI()(
|
||||||
// of gauge1, pinned to gauge1's LeftTop and sized 300x30 so it covers exactly
|
|
||||||
// gauge1's footprint. Clay emits a floating child's render command after the
|
|
||||||
// parent's, so the stream order is gauge1 → backdrop → gauge2: gauge1's pixels
|
|
||||||
// land in `source_texture` before the bracket samples (visible as a blurred
|
|
||||||
// reflection inside the strip), and gauge2 is deferred-replayed by
|
|
||||||
// `prepare_clay_batch` after the bracket closes (renders crisp on top of the
|
|
||||||
// bracket output — unrelated to the strip since they don't overlap).
|
|
||||||
// `backgroundColor` is omitted on the gauges; bg lives on `Gauge.bg_color`. See `draw_custom`.
|
|
||||||
if clay.UI(clay.ID("gauge"))(
|
|
||||||
{
|
{
|
||||||
|
id = clay.ID("gauge"),
|
||||||
layout = {sizing = {clay.SizingFixed(300), clay.SizingFixed(30)}},
|
layout = {sizing = {clay.SizingFixed(300), clay.SizingFixed(30)}},
|
||||||
custom = {customData = &gauge_custom},
|
custom = {customData = &gauge},
|
||||||
|
backgroundColor = {80, 80, 80, 255},
|
||||||
},
|
},
|
||||||
) {
|
) {}
|
||||||
if clay.UI(clay.ID("backdrop"))(
|
|
||||||
{
|
|
||||||
floating = {attachTo = .Parent, attachment = {parent = .LeftTop, element = .LeftTop}},
|
|
||||||
layout = {sizing = {clay.SizingFixed(300), clay.SizingFixed(30)}},
|
|
||||||
custom = {customData = &backdrop_custom},
|
|
||||||
},
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
if clay.UI(clay.ID("gauge2"))(
|
if clay.UI()(
|
||||||
{
|
{
|
||||||
|
id = clay.ID("gauge2"),
|
||||||
layout = {sizing = {clay.SizingFixed(300), clay.SizingFixed(30)}},
|
layout = {sizing = {clay.SizingFixed(300), clay.SizingFixed(30)}},
|
||||||
custom = {customData = &gauge2_custom},
|
custom = {customData = &gauge2},
|
||||||
|
backgroundColor = {80, 80, 80, 255},
|
||||||
},
|
},
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
clay_batch := draw.ClayBatch {
|
clay_batch := draw.ClayBatch {
|
||||||
bounds = base_layer.bounds,
|
bounds = base_layer.bounds,
|
||||||
cmds = clay.EndLayout(0),
|
cmds = clay.EndLayout(),
|
||||||
}
|
}
|
||||||
draw.prepare_clay_batch(base_layer, &clay_batch, custom_draw = draw_custom)
|
draw.prepare_clay_batch(base_layer, &clay_batch, {0, 0}, custom_draw = draw_custom)
|
||||||
draw.end(gpu, window)
|
draw.end(gpu, window)
|
||||||
}
|
}
|
||||||
|
|
||||||
Gauge :: struct {
|
Gauge :: struct {
|
||||||
value: f32,
|
value: f32,
|
||||||
color: draw.Color,
|
color: draw.Color,
|
||||||
bg_color: draw.Color,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
// `render_data.customData` has been unwrapped from the `Clay_Custom` envelope by
|
|
||||||
// `prepare_clay_batch` — it points at the Gauge directly, the same as it would have
|
|
||||||
// before the union refactor.
|
|
||||||
gauge := cast(^Gauge)render_data.customData
|
gauge := cast(^Gauge)render_data.customData
|
||||||
|
|
||||||
// `gauge.bg_color` instead of `render_data.backgroundColor`: under Clay master, an
|
|
||||||
// element with both `custom.customData` and `backgroundColor` emits a Custom AND a
|
|
||||||
// Rectangle for the same bounds, in that order — the Rectangle paints over the
|
|
||||||
// callback's output. Carrying bg on user data sidesteps it.
|
|
||||||
border_width: f32 = 2
|
border_width: f32 = 2
|
||||||
draw.rectangle(layer, bounds, gauge.bg_color, outline_color = draw.WHITE, outline_width = border_width)
|
draw.rectangle(
|
||||||
|
layer,
|
||||||
|
bounds,
|
||||||
|
draw.color_from_clay(render_data.backgroundColor),
|
||||||
|
outline_color = draw.WHITE,
|
||||||
|
outline_width = border_width,
|
||||||
|
)
|
||||||
|
|
||||||
fill := draw.Rectangle {
|
fill := draw.Rectangle {
|
||||||
x = bounds.x,
|
x = bounds.x,
|
||||||
|
|||||||
+45
-193
@@ -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}, draw.Color{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,
|
sampler = .Nearest_Clamp,
|
||||||
tint = draw.WHITE,
|
|
||||||
uv_rect = {0, 0, 1, 1},
|
|
||||||
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,
|
sampler = .Linear_Clamp,
|
||||||
tint = draw.WHITE,
|
|
||||||
uv_rect = {0, 0, 1, 1},
|
|
||||||
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,
|
sampler = .Nearest_Repeat,
|
||||||
tint = draw.WHITE,
|
uv_rect = {0, 0, 4, 4},
|
||||||
uv_rect = {0, 0, 4, 4},
|
|
||||||
sampler = .Nearest_Repeat,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
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,
|
||||||
)
|
)
|
||||||
@@ -166,60 +153,47 @@ textures :: proc() {
|
|||||||
|
|
||||||
ROW2_Y :: f32(190)
|
ROW2_Y :: f32(190)
|
||||||
|
|
||||||
// QR code (RGBA texture with baked colors, nearest sampling) + thin framing border.
|
// QR code (RGBA texture with baked colors, nearest sampling)
|
||||||
draw.rectangle(base_layer, {COL1, ROW2_Y, ITEM_SIZE, ITEM_SIZE}, draw.Color{255, 255, 255, 255}) // white bg
|
draw.rectangle(base_layer, {COL1, ROW2_Y, ITEM_SIZE, ITEM_SIZE}, draw.Color{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,
|
||||||
outline_color = draw.WHITE,
|
sampler = .Nearest_Clamp,
|
||||||
outline_width = 2,
|
|
||||||
)
|
)
|
||||||
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 + outline traces the rounded shape.
|
// 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,
|
sampler = .Nearest_Clamp,
|
||||||
tint = draw.WHITE,
|
|
||||||
uv_rect = {0, 0, 1, 1},
|
|
||||||
sampler = .Nearest_Clamp,
|
|
||||||
},
|
|
||||||
outline_color = draw.Color{255, 200, 100, 255},
|
|
||||||
outline_width = 3,
|
|
||||||
radii = draw.uniform_radii({COL2, ROW2_Y, ITEM_SIZE, ITEM_SIZE}, 0.3),
|
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 + outline rotates with the texture.
|
// 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,
|
sampler = .Nearest_Clamp,
|
||||||
tint = draw.WHITE,
|
|
||||||
uv_rect = {0, 0, 1, 1},
|
|
||||||
sampler = .Nearest_Clamp,
|
|
||||||
},
|
|
||||||
outline_color = draw.WHITE,
|
|
||||||
outline_width = 2,
|
|
||||||
origin = draw.center_of(rot_rect),
|
origin = draw.center_of(rot_rect),
|
||||||
rotation = spin_angle,
|
rotation = spin_angle,
|
||||||
)
|
)
|
||||||
@@ -227,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,
|
||||||
)
|
)
|
||||||
@@ -240,16 +214,12 @@ 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}, draw.Color{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,
|
||||||
)
|
)
|
||||||
@@ -257,16 +227,12 @@ textures :: proc() {
|
|||||||
// 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}, draw.Color{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,
|
||||||
)
|
)
|
||||||
@@ -274,143 +240,29 @@ textures :: proc() {
|
|||||||
// 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}, draw.Color{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 + outline traces the asymmetric corner shape.
|
// Per-corner radii
|
||||||
draw.rectangle(
|
draw.rectangle_texture(
|
||||||
base_layer,
|
base_layer,
|
||||||
{COL4, ROW3_Y, FIT_SIZE, FIT_SIZE},
|
{COL4, ROW3_Y, FIT_SIZE, FIT_SIZE},
|
||||||
draw.Texture_Fill {
|
checker_texture,
|
||||||
id = checker_texture,
|
sampler = .Nearest_Clamp,
|
||||||
tint = draw.WHITE,
|
|
||||||
uv_rect = {0, 0, 1, 1},
|
|
||||||
sampler = .Nearest_Clamp,
|
|
||||||
},
|
|
||||||
outline_color = draw.Color{255, 100, 100, 255},
|
|
||||||
outline_width = 3,
|
|
||||||
radii = {20, 0, 20, 0},
|
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 + outline (textured shape with built-in border).
|
|
||||||
draw.circle(
|
|
||||||
base_layer,
|
|
||||||
{SHAPE_COL1 + SHAPE_SIZE / 2, ROW4_Y + SHAPE_SIZE / 2},
|
|
||||||
SHAPE_SIZE / 2,
|
|
||||||
checker_fill,
|
|
||||||
outline_color = draw.WHITE,
|
|
||||||
outline_width = 2,
|
|
||||||
)
|
|
||||||
draw.text(
|
|
||||||
base_layer,
|
|
||||||
"Circle",
|
|
||||||
{SHAPE_COL1, ROW4_Y + SHAPE_SIZE + LABEL_OFFSET},
|
|
||||||
PLEX_SANS_REGULAR,
|
|
||||||
FONT_SIZE,
|
|
||||||
color = draw.WHITE,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Textured ellipse
|
|
||||||
draw.ellipse(
|
|
||||||
base_layer,
|
|
||||||
{SHAPE_COL2 + SHAPE_SIZE / 2, ROW4_Y + SHAPE_SIZE / 2},
|
|
||||||
SHAPE_SIZE / 2,
|
|
||||||
SHAPE_SIZE / 3,
|
|
||||||
checker_fill,
|
|
||||||
)
|
|
||||||
draw.text(
|
|
||||||
base_layer,
|
|
||||||
"Ellipse",
|
|
||||||
{SHAPE_COL2, ROW4_Y + SHAPE_SIZE + LABEL_OFFSET},
|
|
||||||
PLEX_SANS_REGULAR,
|
|
||||||
FONT_SIZE,
|
|
||||||
color = draw.WHITE,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Textured polygon (hexagon)
|
|
||||||
draw.polygon(
|
|
||||||
base_layer,
|
|
||||||
{SHAPE_COL3 + SHAPE_SIZE / 2, ROW4_Y + SHAPE_SIZE / 2},
|
|
||||||
6,
|
|
||||||
SHAPE_SIZE / 2,
|
|
||||||
checker_fill,
|
|
||||||
)
|
|
||||||
draw.text(
|
|
||||||
base_layer,
|
|
||||||
"Polygon",
|
|
||||||
{SHAPE_COL3, ROW4_Y + SHAPE_SIZE + LABEL_OFFSET},
|
|
||||||
PLEX_SANS_REGULAR,
|
|
||||||
FONT_SIZE,
|
|
||||||
color = draw.WHITE,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Textured ring
|
|
||||||
draw.ring(
|
|
||||||
base_layer,
|
|
||||||
{SHAPE_COL4 + SHAPE_SIZE / 2, ROW4_Y + SHAPE_SIZE / 2},
|
|
||||||
SHAPE_SIZE / 4,
|
|
||||||
SHAPE_SIZE / 2,
|
|
||||||
checker_fill,
|
|
||||||
)
|
|
||||||
draw.text(
|
|
||||||
base_layer,
|
|
||||||
"Ring",
|
|
||||||
{SHAPE_COL4, ROW4_Y + SHAPE_SIZE + LABEL_OFFSET},
|
|
||||||
PLEX_SANS_REGULAR,
|
|
||||||
FONT_SIZE,
|
|
||||||
color = draw.WHITE,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Textured line (capsule)
|
|
||||||
draw.line(
|
|
||||||
base_layer,
|
|
||||||
{SHAPE_COL5, ROW4_Y + SHAPE_SIZE / 2},
|
|
||||||
{SHAPE_COL5 + SHAPE_SIZE, ROW4_Y + SHAPE_SIZE / 2},
|
|
||||||
checker_fill,
|
|
||||||
thickness = 20,
|
|
||||||
)
|
|
||||||
draw.text(
|
|
||||||
base_layer,
|
|
||||||
"Line",
|
|
||||||
{SHAPE_COL5, ROW4_Y + SHAPE_SIZE + LABEL_OFFSET},
|
|
||||||
PLEX_SANS_REGULAR,
|
|
||||||
FONT_SIZE,
|
FONT_SIZE,
|
||||||
color = draw.WHITE,
|
color = draw.WHITE,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,722 @@
|
|||||||
|
package draw
|
||||||
|
|
||||||
|
import "core:c"
|
||||||
|
import "core:log"
|
||||||
|
import "core:mem"
|
||||||
|
import sdl "vendor:sdl3"
|
||||||
|
|
||||||
|
// Vertex layout for tessellated and text geometry.
|
||||||
|
// IMPORTANT: `color` must be premultiplied alpha (RGB channels pre-scaled by alpha).
|
||||||
|
// The tessellated fragment shader passes vertex color through directly — it does NOT
|
||||||
|
// premultiply. The blend state is ONE, ONE_MINUS_SRC_ALPHA (premultiplied-over).
|
||||||
|
// Use `premultiply_color` when constructing vertices manually for `prepare_shape`.
|
||||||
|
Vertex :: struct {
|
||||||
|
position: Vec2,
|
||||||
|
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 -----------
|
||||||
|
// ----------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// The SDF path evaluates one of four signed distance functions per primitive, dispatched
|
||||||
|
// by Shape_Kind encoded in the low byte of Primitive.flags:
|
||||||
|
//
|
||||||
|
// RRect — rounded rectangle with per-corner radii (sdRoundedBox). Also covers circles
|
||||||
|
// (uniform radii = half-size), capsule-style line segments (rotated, max rounding),
|
||||||
|
// and other RRect-reducible shapes.
|
||||||
|
// NGon — regular polygon with N sides and optional rounding.
|
||||||
|
// Ellipse — approximate ellipse (non-exact SDF, suitable for UI but not for shape merging).
|
||||||
|
// Ring_Arc — annular ring with optional angular clipping. Covers full rings, partial arcs,
|
||||||
|
// pie slices (inner_radius = 0), and loading spinners.
|
||||||
|
Shape_Kind :: enum u8 {
|
||||||
|
Solid = 0, // tessellated path (mode marker; not a real SDF kind)
|
||||||
|
RRect = 1,
|
||||||
|
NGon = 2,
|
||||||
|
Ellipse = 3,
|
||||||
|
Ring_Arc = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
Shape_Flag :: enum u8 {
|
||||||
|
Textured, // bit 0: sample texture using uv.uv_rect (mutually exclusive with Gradient)
|
||||||
|
Gradient, // bit 1: 2-color gradient using uv.effects.gradient_color as end/outer color
|
||||||
|
Gradient_Radial, // bit 2: if set with Gradient, radial from center; else linear at angle
|
||||||
|
Outline, // bit 3: outer outline band using uv.effects.outline_color; CPU expands bounds by outline_width
|
||||||
|
Rotated, // bit 4: shape has non-zero rotation; rotation_sc contains packed sin/cos
|
||||||
|
Arc_Narrow, // bit 5: ring arc span ≤ π — intersect half-planes. Neither Arc bit = full ring.
|
||||||
|
Arc_Wide, // bit 6: ring arc span > π — union half-planes. Neither Arc bit = full ring.
|
||||||
|
}
|
||||||
|
|
||||||
|
Shape_Flags :: bit_set[Shape_Flag;u8]
|
||||||
|
|
||||||
|
RRect_Params :: struct {
|
||||||
|
half_size: [2]f32,
|
||||||
|
radii: [4]f32,
|
||||||
|
half_feather: f32, // feather_px * 0.5; shader uses smoothstep(-h, h, d)
|
||||||
|
_: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
NGon_Params :: struct {
|
||||||
|
radius: f32,
|
||||||
|
sides: f32,
|
||||||
|
half_feather: f32, // feather_px * 0.5; shader uses smoothstep(-h, h, d)
|
||||||
|
_: [5]f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
Ellipse_Params :: struct {
|
||||||
|
radii: [2]f32,
|
||||||
|
half_feather: f32, // feather_px * 0.5; shader uses smoothstep(-h, h, d)
|
||||||
|
_: [5]f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
Ring_Arc_Params :: struct {
|
||||||
|
inner_radius: f32, // inner radius in physical pixels (0 for pie slice)
|
||||||
|
outer_radius: f32, // outer radius in physical pixels
|
||||||
|
normal_start: [2]f32, // pre-computed outward normal of start edge: (sin(start), -cos(start))
|
||||||
|
normal_end: [2]f32, // pre-computed outward normal of end edge: (-sin(end), cos(end))
|
||||||
|
half_feather: f32, // feather_px * 0.5; shader uses smoothstep(-h, h, d)
|
||||||
|
_: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
Shape_Params :: struct #raw_union {
|
||||||
|
rrect: RRect_Params,
|
||||||
|
ngon: NGon_Params,
|
||||||
|
ellipse: Ellipse_Params,
|
||||||
|
ring_arc: Ring_Arc_Params,
|
||||||
|
raw: [8]f32,
|
||||||
|
}
|
||||||
|
#assert(size_of(Shape_Params) == 32)
|
||||||
|
|
||||||
|
// GPU-side storage for 2-color gradient parameters and/or outline parameters.
|
||||||
|
// Packed into 16 bytes to alias with uv_rect in the Uv_Or_Effects raw union.
|
||||||
|
// The shader reads gradient_color and outline_color via unpackUnorm4x8.
|
||||||
|
// gradient_dir_sc stores the pre-computed gradient direction as (cos, sin) in f16 pair
|
||||||
|
// via unpackHalf2x16. outline_packed stores outline_width as f16 via unpackHalf2x16.
|
||||||
|
Gradient_Outline :: struct {
|
||||||
|
gradient_color: Color, // 0: end (linear) or outer (radial) gradient color
|
||||||
|
outline_color: Color, // 4: outline band color
|
||||||
|
gradient_dir_sc: u32, // 8: packed f16 pair: low = cos(angle), high = sin(angle) — pre-computed gradient direction
|
||||||
|
outline_packed: u32, // 12: packed f16 pair: low = outline_width (f16, physical pixels), high = reserved
|
||||||
|
}
|
||||||
|
|
||||||
|
#assert(size_of(Gradient_Outline) == 16)
|
||||||
|
|
||||||
|
// Uv_Or_Effects aliases the final 16 bytes of a Primitive. When .Textured is set,
|
||||||
|
// uv_rect holds texture-atlas coordinates. When .Gradient or .Outline is set,
|
||||||
|
// effects holds 2-color gradient parameters and/or outline parameters.
|
||||||
|
// Textured and Gradient are mutually exclusive; if both are set, Gradient takes precedence.
|
||||||
|
Uv_Or_Effects :: struct #raw_union {
|
||||||
|
uv_rect: [4]f32, // u_min, v_min, u_max, v_max (default {0,0,1,1})
|
||||||
|
effects: Gradient_Outline, // gradient + outline parameters
|
||||||
|
}
|
||||||
|
|
||||||
|
// GPU layout: 80 bytes, std430-compatible. The shader declares this as a storage buffer struct.
|
||||||
|
// The low byte of `flags` encodes the Shape_Kind (0 = tessellated, 1-4 = SDF kinds).
|
||||||
|
// Bits 8-15 encode Shape_Flags (Textured, Gradient, Gradient_Radial, Outline, Rotated, Arc_Narrow, Arc_Wide).
|
||||||
|
// rotation_sc stores pre-computed sin/cos of the rotation angle as a packed f16 pair,
|
||||||
|
// avoiding per-pixel trigonometry in the fragment shader. Only read when .Rotated is set.
|
||||||
|
Primitive :: struct {
|
||||||
|
bounds: [4]f32, // 0: min_x, min_y, max_x, max_y (world-space, pre-DPI)
|
||||||
|
color: Color, // 16: u8x4, fill color / gradient start color / texture tint
|
||||||
|
flags: u32, // 20: low byte = Shape_Kind, bits 8+ = Shape_Flags
|
||||||
|
rotation_sc: u32, // 24: packed f16 pair: low = sin(angle), high = cos(angle). Requires .Rotated flag.
|
||||||
|
_pad: f32, // 28: reserved for future use
|
||||||
|
params: Shape_Params, // 32: per-kind shape parameters (raw union, 32 bytes)
|
||||||
|
uv: Uv_Or_Effects, // 64: texture coords or gradient/outline parameters
|
||||||
|
}
|
||||||
|
|
||||||
|
#assert(size_of(Primitive) == 80)
|
||||||
|
|
||||||
|
// Pack shape kind and flags into the Primitive.flags field. The low byte encodes the Shape_Kind
|
||||||
|
// (which also serves as the SDF mode marker — kind > 0 means SDF path). The tessellated path
|
||||||
|
// leaves the field at 0 (Solid kind, set by vertex shader zero-initialization).
|
||||||
|
pack_kind_flags :: #force_inline proc(kind: Shape_Kind, flags: Shape_Flags) -> u32 {
|
||||||
|
return u32(kind) | (u32(transmute(u8)flags) << 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pack two f16 values into a single u32 for GPU consumption via unpackHalf2x16.
|
||||||
|
// Used to pack gradient_dir_sc (cos/sin) and outline_packed (width/reserved) in Gradient_Outline.
|
||||||
|
pack_f16_pair :: #force_inline proc(low, high: f16) -> u32 {
|
||||||
|
return u32(transmute(u16)low) | (u32(transmute(u16)high) << 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
// Premultiplied-alpha blending: src outputs RGB pre-multiplied by alpha,
|
||||||
|
// so src factor is ONE (not SRC_ALPHA). This eliminates the per-pixel
|
||||||
|
// divide in the outline path and is the standard blend mode used by
|
||||||
|
// Skia, Flutter, and GPUI.
|
||||||
|
blend_state = sdl.GPUColorTargetBlendState {
|
||||||
|
enable_blend = true,
|
||||||
|
enable_color_write_mask = true,
|
||||||
|
src_color_blendfactor = .ONE,
|
||||||
|
dst_color_blendfactor = .ONE_MINUS_SRC_ALPHA,
|
||||||
|
color_blend_op = .ADD,
|
||||||
|
src_alpha_blendfactor = .ONE,
|
||||||
|
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 := Color{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 .Tessellated:
|
||||||
|
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_ppx [[user(locn2), flat]];
|
|
||||||
float4 f_radii_ppx [[user(locn3), flat]];
|
|
||||||
float f_half_feather_ppx [[user(locn4), flat]];
|
|
||||||
};
|
|
||||||
|
|
||||||
static inline __attribute__((always_inline))
|
|
||||||
float3 blur_sample(thread const float2& uv, constant Uniforms& _108, texture2d<float> blur_input_tex, sampler blur_input_texSmplr)
|
|
||||||
{
|
|
||||||
float3 color = blur_input_tex.sample(blur_input_texSmplr, uv).xyz * _108.kernel0[0].x;
|
|
||||||
float2 axis_step = _108.direction * _108.inv_working_size;
|
|
||||||
for (uint i = 1u; i < _108.pair_count; i++)
|
|
||||||
{
|
|
||||||
float w = _108.kernel0[i].x;
|
|
||||||
float off = _108.kernel0[i].y;
|
|
||||||
float2 step_uv = axis_step * off;
|
|
||||||
color += (blur_input_tex.sample(blur_input_texSmplr, (uv - step_uv)).xyz * w);
|
|
||||||
color += (blur_input_tex.sample(blur_input_texSmplr, (uv + step_uv)).xyz * w);
|
|
||||||
}
|
|
||||||
return color;
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline __attribute__((always_inline))
|
|
||||||
float sdRoundedBox(thread const float2& p, thread const float2& b, thread const float4& r)
|
|
||||||
{
|
|
||||||
float2 _36;
|
|
||||||
if (p.x > 0.0)
|
|
||||||
{
|
|
||||||
_36 = r.xy;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_36 = r.zw;
|
|
||||||
}
|
|
||||||
float2 rxy = _36;
|
|
||||||
float _50;
|
|
||||||
if (p.y > 0.0)
|
|
||||||
{
|
|
||||||
_50 = rxy.x;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_50 = rxy.y;
|
|
||||||
}
|
|
||||||
float rr = _50;
|
|
||||||
float2 q = abs(p) - b;
|
|
||||||
if (rr == 0.0)
|
|
||||||
{
|
|
||||||
return fast::max(q.x, q.y);
|
|
||||||
}
|
|
||||||
q += float2(rr);
|
|
||||||
return (fast::min(fast::max(q.x, q.y), 0.0) + length(fast::max(q, float2(0.0)))) - rr;
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline __attribute__((always_inline))
|
|
||||||
float sdf_alpha(thread const float& d, thread const float& h)
|
|
||||||
{
|
|
||||||
return 1.0 - smoothstep(-h, h, d);
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment main0_out main0(main0_in in [[stage_in]], constant Uniforms& _108 [[buffer(0)]], texture2d<float> blur_input_tex [[texture(0)]], sampler blur_input_texSmplr [[sampler(0)]], float4 gl_FragCoord [[position]])
|
|
||||||
{
|
|
||||||
main0_out out = {};
|
|
||||||
if (_108.mode == 0u)
|
|
||||||
{
|
|
||||||
float2 uv = gl_FragCoord.xy * _108.inv_working_size;
|
|
||||||
float2 param = uv;
|
|
||||||
float3 color = blur_sample(param, _108, blur_input_tex, blur_input_texSmplr);
|
|
||||||
out.out_color = float4(color, 1.0);
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
float2 param_1 = in.p_local;
|
|
||||||
float2 param_2 = in.f_half_size_ppx;
|
|
||||||
float4 param_3 = in.f_radii_ppx;
|
|
||||||
float d = sdRoundedBox(param_1, param_2, param_3);
|
|
||||||
if (d > in.f_half_feather_ppx)
|
|
||||||
{
|
|
||||||
discard_fragment();
|
|
||||||
}
|
|
||||||
float grad_magnitude = fast::max(fwidth(d), 9.9999999747524270787835121154785e-07);
|
|
||||||
float d_n = d / grad_magnitude;
|
|
||||||
float h_n = in.f_half_feather_ppx / grad_magnitude;
|
|
||||||
float2 uv_1 = (gl_FragCoord.xy * _108.inv_downsample_factor) * _108.inv_working_size;
|
|
||||||
float3 color_1 = blur_input_tex.sample(blur_input_texSmplr, uv_1).xyz;
|
|
||||||
float3 tinted = mix(color_1, color_1 * in.f_color.xyz, float3(in.f_color.w));
|
|
||||||
float param_4 = d_n;
|
|
||||||
float param_5 = h_n;
|
|
||||||
float coverage = sdf_alpha(param_4, param_5);
|
|
||||||
out.out_color = float4(tinted * coverage, coverage);
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
Binary file not shown.
@@ -1,123 +0,0 @@
|
|||||||
#pragma clang diagnostic ignored "-Wmissing-prototypes"
|
|
||||||
#pragma clang diagnostic ignored "-Wmissing-braces"
|
|
||||||
|
|
||||||
#include <metal_stdlib>
|
|
||||||
#include <simd/simd.h>
|
|
||||||
|
|
||||||
using namespace metal;
|
|
||||||
|
|
||||||
template<typename T, size_t Num>
|
|
||||||
struct spvUnsafeArray
|
|
||||||
{
|
|
||||||
T elements[Num ? Num : 1];
|
|
||||||
|
|
||||||
thread T& operator [] (size_t pos) thread
|
|
||||||
{
|
|
||||||
return elements[pos];
|
|
||||||
}
|
|
||||||
constexpr const thread T& operator [] (size_t pos) const thread
|
|
||||||
{
|
|
||||||
return elements[pos];
|
|
||||||
}
|
|
||||||
|
|
||||||
device T& operator [] (size_t pos) device
|
|
||||||
{
|
|
||||||
return elements[pos];
|
|
||||||
}
|
|
||||||
constexpr const device T& operator [] (size_t pos) const device
|
|
||||||
{
|
|
||||||
return elements[pos];
|
|
||||||
}
|
|
||||||
|
|
||||||
constexpr const constant T& operator [] (size_t pos) const constant
|
|
||||||
{
|
|
||||||
return elements[pos];
|
|
||||||
}
|
|
||||||
|
|
||||||
threadgroup T& operator [] (size_t pos) threadgroup
|
|
||||||
{
|
|
||||||
return elements[pos];
|
|
||||||
}
|
|
||||||
constexpr const threadgroup T& operator [] (size_t pos) const threadgroup
|
|
||||||
{
|
|
||||||
return elements[pos];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Uniforms
|
|
||||||
{
|
|
||||||
float4x4 projection;
|
|
||||||
float dpi_scale;
|
|
||||||
uint mode;
|
|
||||||
float2 _pad0;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Gaussian_Blur_Primitive
|
|
||||||
{
|
|
||||||
float4 bounds;
|
|
||||||
float4 radii_ppx;
|
|
||||||
float2 half_size_ppx;
|
|
||||||
float half_feather_ppx;
|
|
||||||
uint color;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Gaussian_Blur_Primitive_1
|
|
||||||
{
|
|
||||||
float4 bounds;
|
|
||||||
float4 radii_ppx;
|
|
||||||
float2 half_size_ppx;
|
|
||||||
float half_feather_ppx;
|
|
||||||
uint color;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Gaussian_Blur_Primitives
|
|
||||||
{
|
|
||||||
Gaussian_Blur_Primitive_1 primitives[1];
|
|
||||||
};
|
|
||||||
|
|
||||||
constant spvUnsafeArray<float2, 6> _97 = spvUnsafeArray<float2, 6>({ float2(0.0), float2(1.0, 0.0), float2(0.0, 1.0), float2(0.0, 1.0), float2(1.0, 0.0), float2(1.0) });
|
|
||||||
|
|
||||||
struct main0_out
|
|
||||||
{
|
|
||||||
float2 p_local [[user(locn0)]];
|
|
||||||
float4 f_color [[user(locn1)]];
|
|
||||||
float2 f_half_size_ppx [[user(locn2)]];
|
|
||||||
float4 f_radii_ppx [[user(locn3)]];
|
|
||||||
float f_half_feather_ppx [[user(locn4)]];
|
|
||||||
float4 gl_Position [[position]];
|
|
||||||
};
|
|
||||||
|
|
||||||
vertex main0_out main0(constant Uniforms& _13 [[buffer(0)]], const device Gaussian_Blur_Primitives& _69 [[buffer(1)]], uint gl_VertexIndex [[vertex_id]], uint gl_InstanceIndex [[instance_id]])
|
|
||||||
{
|
|
||||||
main0_out out = {};
|
|
||||||
if (_13.mode == 0u)
|
|
||||||
{
|
|
||||||
float2 ndc = float2((int(gl_VertexIndex) == 1) ? 3.0 : (-1.0), (int(gl_VertexIndex) == 2) ? 3.0 : (-1.0));
|
|
||||||
out.gl_Position = float4(ndc, 0.0, 1.0);
|
|
||||||
out.p_local = float2(0.0);
|
|
||||||
out.f_color = float4(0.0);
|
|
||||||
out.f_half_size_ppx = float2(0.0);
|
|
||||||
out.f_radii_ppx = float4(0.0);
|
|
||||||
out.f_half_feather_ppx = 0.0;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Gaussian_Blur_Primitive p;
|
|
||||||
p.bounds = _69.primitives[int(gl_InstanceIndex)].bounds;
|
|
||||||
p.radii_ppx = _69.primitives[int(gl_InstanceIndex)].radii_ppx;
|
|
||||||
p.half_size_ppx = _69.primitives[int(gl_InstanceIndex)].half_size_ppx;
|
|
||||||
p.half_feather_ppx = _69.primitives[int(gl_InstanceIndex)].half_feather_ppx;
|
|
||||||
p.color = _69.primitives[int(gl_InstanceIndex)].color;
|
|
||||||
float2 corner = _97[int(gl_VertexIndex)];
|
|
||||||
float2 world_pos = mix(p.bounds.xy, p.bounds.zw, corner);
|
|
||||||
float2 center = (p.bounds.xy + p.bounds.zw) * 0.5;
|
|
||||||
out.p_local = (world_pos - center) * _13.dpi_scale;
|
|
||||||
out.f_color = unpack_unorm4x8_to_float(p.color);
|
|
||||||
out.f_half_size_ppx = p.half_size_ppx;
|
|
||||||
out.f_radii_ppx = p.radii_ppx;
|
|
||||||
out.f_half_feather_ppx = p.half_feather_ppx;
|
|
||||||
out.gl_Position = _13.projection * float4(world_pos * _13.dpi_scale, 0.0, 1.0);
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
Binary file not shown.
@@ -1,47 +0,0 @@
|
|||||||
#include <metal_stdlib>
|
|
||||||
#include <simd/simd.h>
|
|
||||||
|
|
||||||
using namespace metal;
|
|
||||||
|
|
||||||
struct Uniforms
|
|
||||||
{
|
|
||||||
float2 inv_source_size;
|
|
||||||
uint downsample_factor;
|
|
||||||
uint _pad0;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct main0_out
|
|
||||||
{
|
|
||||||
float4 out_color [[color(0)]];
|
|
||||||
};
|
|
||||||
|
|
||||||
fragment main0_out main0(constant Uniforms& _18 [[buffer(0)]], texture2d<float> source_tex [[texture(0)]], sampler source_texSmplr [[sampler(0)]], float4 gl_FragCoord [[position]])
|
|
||||||
{
|
|
||||||
main0_out out = {};
|
|
||||||
float2 src_block_center = gl_FragCoord.xy * float(_18.downsample_factor);
|
|
||||||
if (_18.downsample_factor == 1u)
|
|
||||||
{
|
|
||||||
float2 uv = src_block_center * _18.inv_source_size;
|
|
||||||
out.out_color = source_tex.sample(source_texSmplr, uv);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (_18.downsample_factor == 2u)
|
|
||||||
{
|
|
||||||
float2 uv_1 = src_block_center * _18.inv_source_size;
|
|
||||||
out.out_color = source_tex.sample(source_texSmplr, uv_1);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
float off = float(_18.downsample_factor) * 0.25;
|
|
||||||
float2 uv_tl = (src_block_center + float2(-off, -off)) * _18.inv_source_size;
|
|
||||||
float2 uv_tr = (src_block_center + float2(off, -off)) * _18.inv_source_size;
|
|
||||||
float2 uv_bl = (src_block_center + float2(-off, off)) * _18.inv_source_size;
|
|
||||||
float2 uv_br = (src_block_center + float2(off)) * _18.inv_source_size;
|
|
||||||
float4 c = ((source_tex.sample(source_texSmplr, uv_tl) + source_tex.sample(source_texSmplr, uv_tr)) + source_tex.sample(source_texSmplr, uv_bl)) + source_tex.sample(source_texSmplr, uv_br);
|
|
||||||
out.out_color = c * 0.25;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
Binary file not shown.
@@ -1,18 +0,0 @@
|
|||||||
#include <metal_stdlib>
|
|
||||||
#include <simd/simd.h>
|
|
||||||
|
|
||||||
using namespace metal;
|
|
||||||
|
|
||||||
struct main0_out
|
|
||||||
{
|
|
||||||
float4 gl_Position [[position]];
|
|
||||||
};
|
|
||||||
|
|
||||||
vertex main0_out main0(uint gl_VertexIndex [[vertex_id]])
|
|
||||||
{
|
|
||||||
main0_out out = {};
|
|
||||||
float2 ndc = float2((int(gl_VertexIndex) == 1) ? 3.0 : (-1.0), (int(gl_VertexIndex) == 2) ? 3.0 : (-1.0));
|
|
||||||
out.gl_Position = float4(ndc, 0.0, 1.0);
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
Binary file not shown.
@@ -24,8 +24,8 @@ struct main0_in
|
|||||||
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_flags [[user(locn4)]];
|
||||||
float4 f_uv_rect [[user(locn6), flat]];
|
uint f_rotation_sc [[user(locn5)]];
|
||||||
uint4 f_effects [[user(locn7)]];
|
uint4 f_uv_or_effects [[user(locn6)]];
|
||||||
};
|
};
|
||||||
|
|
||||||
static inline __attribute__((always_inline))
|
static inline __attribute__((always_inline))
|
||||||
@@ -107,70 +107,75 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
|
|||||||
}
|
}
|
||||||
float d = 1000000015047466219876688855040.0;
|
float d = 1000000015047466219876688855040.0;
|
||||||
float h = 0.5;
|
float h = 0.5;
|
||||||
float2 half_size_ppx = in.f_params.xy;
|
float2 half_size = in.f_params.xy;
|
||||||
float2 p_local_ppx = in.f_local_or_uv;
|
float2 p_local = in.f_local_or_uv;
|
||||||
|
if ((flags & 16u) != 0u)
|
||||||
|
{
|
||||||
|
float2 sc = float2(as_type<half2>(in.f_rotation_sc));
|
||||||
|
p_local = float2((sc.y * p_local.x) + (sc.x * p_local.y), ((-sc.x) * p_local.x) + (sc.y * p_local.y));
|
||||||
|
}
|
||||||
if (kind == 1u)
|
if (kind == 1u)
|
||||||
{
|
{
|
||||||
float4 corner_radii_ppx = float4(in.f_params.zw, in.f_params2.xy);
|
float4 corner_radii = float4(in.f_params.zw, in.f_params2.xy);
|
||||||
h = in.f_params2.z;
|
h = in.f_params2.z;
|
||||||
float2 param = p_local_ppx;
|
float2 param = p_local;
|
||||||
float2 param_1 = half_size_ppx;
|
float2 param_1 = half_size;
|
||||||
float4 param_2 = corner_radii_ppx;
|
float4 param_2 = corner_radii;
|
||||||
d = sdRoundedBox(param, param_1, param_2);
|
d = sdRoundedBox(param, param_1, param_2);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (kind == 2u)
|
if (kind == 2u)
|
||||||
{
|
{
|
||||||
float radius_ppx = in.f_params.x;
|
float radius = in.f_params.x;
|
||||||
float sides = in.f_params.y;
|
float sides = in.f_params.y;
|
||||||
h = in.f_params.z;
|
h = in.f_params.z;
|
||||||
float2 param_3 = p_local_ppx;
|
float2 param_3 = p_local;
|
||||||
float param_4 = radius_ppx;
|
float param_4 = radius;
|
||||||
float param_5 = sides;
|
float param_5 = sides;
|
||||||
d = sdRegularPolygon(param_3, param_4, param_5);
|
d = sdRegularPolygon(param_3, param_4, param_5);
|
||||||
half_size_ppx = float2(radius_ppx);
|
half_size = float2(radius);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (kind == 3u)
|
if (kind == 3u)
|
||||||
{
|
{
|
||||||
float2 radii_ppx = in.f_params.xy;
|
float2 ab = in.f_params.xy;
|
||||||
h = in.f_params.z;
|
h = in.f_params.z;
|
||||||
float2 param_6 = p_local_ppx;
|
float2 param_6 = p_local;
|
||||||
float2 param_7 = radii_ppx;
|
float2 param_7 = ab;
|
||||||
d = sdEllipseApprox(param_6, param_7);
|
d = sdEllipseApprox(param_6, param_7);
|
||||||
half_size_ppx = radii_ppx;
|
half_size = ab;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (kind == 4u)
|
if (kind == 4u)
|
||||||
{
|
{
|
||||||
float inner_radius_ppx = in.f_params.x;
|
float inner = in.f_params.x;
|
||||||
float outer_radius_ppx = in.f_params.y;
|
float outer = in.f_params.y;
|
||||||
float2 n_start = in.f_params.zw;
|
float2 n_start = in.f_params.zw;
|
||||||
float2 n_end = in.f_params2.xy;
|
float2 n_end = in.f_params2.xy;
|
||||||
uint arc_bits = (flags >> 5u) & 3u;
|
uint arc_bits = (flags >> 5u) & 3u;
|
||||||
h = in.f_params2.z;
|
h = in.f_params2.z;
|
||||||
float r = length(p_local_ppx);
|
float r = length(p_local);
|
||||||
d = fast::max(inner_radius_ppx - r, r - outer_radius_ppx);
|
d = fast::max(inner - r, r - outer);
|
||||||
if (arc_bits != 0u)
|
if (arc_bits != 0u)
|
||||||
{
|
{
|
||||||
float d_start = dot(p_local_ppx, n_start);
|
float d_start = dot(p_local, n_start);
|
||||||
float d_end = dot(p_local_ppx, n_end);
|
float d_end = dot(p_local, n_end);
|
||||||
float _338;
|
float _372;
|
||||||
if (arc_bits == 1u)
|
if (arc_bits == 1u)
|
||||||
{
|
{
|
||||||
_338 = fast::max(d_start, d_end);
|
_372 = fast::max(d_start, d_end);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_338 = fast::min(d_start, d_end);
|
_372 = fast::min(d_start, d_end);
|
||||||
}
|
}
|
||||||
float d_wedge = _338;
|
float d_wedge = _372;
|
||||||
d = fast::max(d, d_wedge);
|
d = fast::max(d, d_wedge);
|
||||||
}
|
}
|
||||||
half_size_ppx = float2(outer_radius_ppx);
|
half_size = float2(outer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,10 +187,10 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
|
|||||||
if ((flags & 2u) != 0u)
|
if ((flags & 2u) != 0u)
|
||||||
{
|
{
|
||||||
float4 gradient_start = in.f_color;
|
float4 gradient_start = in.f_color;
|
||||||
float4 gradient_end = unpack_unorm4x8_to_float(in.f_effects.x);
|
float4 gradient_end = unpack_unorm4x8_to_float(in.f_uv_or_effects.x);
|
||||||
if ((flags & 4u) != 0u)
|
if ((flags & 4u) != 0u)
|
||||||
{
|
{
|
||||||
float t_1 = length(p_local_ppx / half_size_ppx);
|
float t_1 = length(p_local / half_size);
|
||||||
float4 param_8 = gradient_start;
|
float4 param_8 = gradient_start;
|
||||||
float4 param_9 = gradient_end;
|
float4 param_9 = gradient_end;
|
||||||
float param_10 = t_1;
|
float param_10 = t_1;
|
||||||
@@ -193,8 +198,8 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
float2 direction = float2(as_type<half2>(in.f_effects.z));
|
float2 direction = float2(as_type<half2>(in.f_uv_or_effects.z));
|
||||||
float t_2 = (dot(p_local_ppx / half_size_ppx, direction) * 0.5) + 0.5;
|
float t_2 = (dot(p_local / half_size, direction) * 0.5) + 0.5;
|
||||||
float4 param_11 = gradient_start;
|
float4 param_11 = gradient_start;
|
||||||
float4 param_12 = gradient_end;
|
float4 param_12 = gradient_end;
|
||||||
float param_13 = t_2;
|
float param_13 = t_2;
|
||||||
@@ -205,8 +210,8 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
|
|||||||
{
|
{
|
||||||
if ((flags & 1u) != 0u)
|
if ((flags & 1u) != 0u)
|
||||||
{
|
{
|
||||||
float4 uv_rect = in.f_uv_rect;
|
float4 uv_rect = as_type<float4>(in.f_uv_or_effects);
|
||||||
float2 local_uv = ((p_local_ppx / half_size_ppx) * 0.5) + float2(0.5);
|
float2 local_uv = ((p_local / half_size) * 0.5) + float2(0.5);
|
||||||
float2 uv = mix(uv_rect.xy, uv_rect.zw, local_uv);
|
float2 uv = mix(uv_rect.xy, uv_rect.zw, local_uv);
|
||||||
shape_color = in.f_color * tex.sample(texSmplr, uv);
|
shape_color = in.f_color * tex.sample(texSmplr, uv);
|
||||||
}
|
}
|
||||||
@@ -217,8 +222,8 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
|
|||||||
}
|
}
|
||||||
if ((flags & 8u) != 0u)
|
if ((flags & 8u) != 0u)
|
||||||
{
|
{
|
||||||
float4 ol_color = unpack_unorm4x8_to_float(in.f_effects.y);
|
float4 ol_color = unpack_unorm4x8_to_float(in.f_uv_or_effects.y);
|
||||||
float ol_width = float2(as_type<half2>(in.f_effects.w)).x / grad_magnitude;
|
float ol_width = float2(as_type<half2>(in.f_uv_or_effects.w)).x / grad_magnitude;
|
||||||
float param_14 = d;
|
float param_14 = d;
|
||||||
float param_15 = h;
|
float param_15 = h;
|
||||||
float fill_cov = sdf_alpha(param_14, param_15);
|
float fill_cov = sdf_alpha(param_14, param_15);
|
||||||
|
|||||||
Binary file not shown.
@@ -10,7 +10,7 @@ struct Uniforms
|
|||||||
uint mode;
|
uint mode;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Core_2D_Primitive
|
struct Primitive
|
||||||
{
|
{
|
||||||
float4 bounds;
|
float4 bounds;
|
||||||
uint color;
|
uint color;
|
||||||
@@ -19,11 +19,10 @@ struct Core_2D_Primitive
|
|||||||
float _pad;
|
float _pad;
|
||||||
float4 params;
|
float4 params;
|
||||||
float4 params2;
|
float4 params2;
|
||||||
float4 uv_rect;
|
uint4 uv_or_effects;
|
||||||
uint4 effects;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Core_2D_Primitive_1
|
struct Primitive_1
|
||||||
{
|
{
|
||||||
float4 bounds;
|
float4 bounds;
|
||||||
uint color;
|
uint color;
|
||||||
@@ -32,13 +31,12 @@ struct Core_2D_Primitive_1
|
|||||||
float _pad;
|
float _pad;
|
||||||
float4 params;
|
float4 params;
|
||||||
float4 params2;
|
float4 params2;
|
||||||
float4 uv_rect;
|
uint4 uv_or_effects;
|
||||||
uint4 effects;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Core_2D_Primitives
|
struct Primitives
|
||||||
{
|
{
|
||||||
Core_2D_Primitive_1 primitives[1];
|
Primitive_1 primitives[1];
|
||||||
};
|
};
|
||||||
|
|
||||||
struct main0_out
|
struct main0_out
|
||||||
@@ -48,8 +46,8 @@ struct main0_out
|
|||||||
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_flags [[user(locn4)]];
|
||||||
float4 f_uv_rect [[user(locn6)]];
|
uint f_rotation_sc [[user(locn5)]];
|
||||||
uint4 f_effects [[user(locn7)]];
|
uint4 f_uv_or_effects [[user(locn6)]];
|
||||||
float4 gl_Position [[position]];
|
float4 gl_Position [[position]];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -60,60 +58,42 @@ 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& _31 [[buffer(1)]], uint gl_InstanceIndex [[instance_id]])
|
vertex main0_out main0(main0_in in [[stage_in]], constant Uniforms& _12 [[buffer(0)]], const device Primitives& _75 [[buffer(1)]], uint gl_InstanceIndex [[instance_id]])
|
||||||
{
|
{
|
||||||
main0_out out = {};
|
main0_out out = {};
|
||||||
if (_12.mode == 1u)
|
if (_12.mode == 0u)
|
||||||
{
|
|
||||||
Core_2D_Primitive p;
|
|
||||||
p.bounds = _31.primitives[int(gl_InstanceIndex)].bounds;
|
|
||||||
p.color = _31.primitives[int(gl_InstanceIndex)].color;
|
|
||||||
p.flags = _31.primitives[int(gl_InstanceIndex)].flags;
|
|
||||||
p.rotation_sc = _31.primitives[int(gl_InstanceIndex)].rotation_sc;
|
|
||||||
p._pad = _31.primitives[int(gl_InstanceIndex)]._pad;
|
|
||||||
p.params = _31.primitives[int(gl_InstanceIndex)].params;
|
|
||||||
p.params2 = _31.primitives[int(gl_InstanceIndex)].params2;
|
|
||||||
p.uv_rect = _31.primitives[int(gl_InstanceIndex)].uv_rect;
|
|
||||||
p.effects = _31.primitives[int(gl_InstanceIndex)].effects;
|
|
||||||
float2 corner = in.v_position;
|
|
||||||
float2 world_pos = mix(p.bounds.xy, p.bounds.zw, corner);
|
|
||||||
float2 center = (p.bounds.xy + p.bounds.zw) * 0.5;
|
|
||||||
float2 local = (world_pos - center) * _12.dpi_scale;
|
|
||||||
uint flags = (p.flags >> 8u) & 255u;
|
|
||||||
if ((flags & 16u) != 0u)
|
|
||||||
{
|
|
||||||
float2 sc = float2(as_type<half2>(p.rotation_sc));
|
|
||||||
local = float2((sc.y * local.x) + (sc.x * local.y), ((-sc.x) * local.x) + (sc.y * local.y));
|
|
||||||
}
|
|
||||||
out.f_color = unpack_unorm4x8_to_float(p.color);
|
|
||||||
out.f_local_or_uv = local;
|
|
||||||
out.f_params = p.params;
|
|
||||||
out.f_params2 = p.params2;
|
|
||||||
out.f_flags = p.flags;
|
|
||||||
out.f_uv_rect = p.uv_rect;
|
|
||||||
out.f_effects = p.effects;
|
|
||||||
out.gl_Position = _12.projection * float4(world_pos * _12.dpi_scale, 0.0, 1.0);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
out.f_color = in.v_color;
|
out.f_color = in.v_color;
|
||||||
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_flags = 0u;
|
||||||
out.f_uv_rect = float4(0.0);
|
out.f_rotation_sc = 0u;
|
||||||
out.f_effects = uint4(0u);
|
out.f_uv_or_effects = uint4(0u);
|
||||||
float2 _199;
|
out.gl_Position = _12.projection * float4(in.v_position * _12.dpi_scale, 0.0, 1.0);
|
||||||
if (_12.mode == 2u)
|
}
|
||||||
{
|
else
|
||||||
_199 = in.v_position;
|
{
|
||||||
}
|
Primitive p;
|
||||||
else
|
p.bounds = _75.primitives[int(gl_InstanceIndex)].bounds;
|
||||||
{
|
p.color = _75.primitives[int(gl_InstanceIndex)].color;
|
||||||
_199 = in.v_position * _12.dpi_scale;
|
p.flags = _75.primitives[int(gl_InstanceIndex)].flags;
|
||||||
}
|
p.rotation_sc = _75.primitives[int(gl_InstanceIndex)].rotation_sc;
|
||||||
float2 pos = _199;
|
p._pad = _75.primitives[int(gl_InstanceIndex)]._pad;
|
||||||
out.gl_Position = _12.projection * float4(pos, 0.0, 1.0);
|
p.params = _75.primitives[int(gl_InstanceIndex)].params;
|
||||||
|
p.params2 = _75.primitives[int(gl_InstanceIndex)].params2;
|
||||||
|
p.uv_or_effects = _75.primitives[int(gl_InstanceIndex)].uv_or_effects;
|
||||||
|
float2 corner = in.v_position;
|
||||||
|
float2 world_pos = mix(p.bounds.xy, p.bounds.zw, corner);
|
||||||
|
float2 center = (p.bounds.xy + p.bounds.zw) * 0.5;
|
||||||
|
out.f_color = unpack_unorm4x8_to_float(p.color);
|
||||||
|
out.f_local_or_uv = (world_pos - center) * _12.dpi_scale;
|
||||||
|
out.f_params = p.params;
|
||||||
|
out.f_params2 = p.params2;
|
||||||
|
out.f_flags = p.flags;
|
||||||
|
out.f_rotation_sc = p.rotation_sc;
|
||||||
|
out.f_uv_or_effects = p.uv_or_effects;
|
||||||
|
out.gl_Position = _12.projection * float4(world_pos * _12.dpi_scale, 0.0, 1.0);
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
@@ -1,155 +0,0 @@
|
|||||||
#version 450 core
|
|
||||||
|
|
||||||
// Unified backdrop blur fragment shader.
|
|
||||||
// Handles both the 1D separable blur passes (mode 0, used for BOTH the H-pass and V-pass;
|
|
||||||
// `direction` picks the axis) and the composite pass (mode 1, reads the fully-blurred
|
|
||||||
// working texture, masks via RRect SDF, applies tint, and writes to source_texture with
|
|
||||||
// premultiplied-over blending). Working textures are sized at the full swapchain resolution;
|
|
||||||
// downsampled content occupies only a sub-rect at downsample factor > 1 (set via viewport).
|
|
||||||
//
|
|
||||||
// The composite blends with source_texture via the standard premultiplied-over blend state
|
|
||||||
// (ONE, ONE_MINUS_SRC_ALPHA).
|
|
||||||
//
|
|
||||||
// Backdrop primitives are tint-only — there is no outline. A specialized edge effect
|
|
||||||
// (e.g. liquid-glass-style refraction outlines) would be implemented as a dedicated
|
|
||||||
// primitive type with its own pipeline.
|
|
||||||
//
|
|
||||||
// Two modes, structurally distinct:
|
|
||||||
//
|
|
||||||
// Mode 0: 1D separable blur. Used for BOTH the H-pass and V-pass; `direction` (set in the
|
|
||||||
// per-pass uniforms) picks (1,0) for H or (0,1) for V. Reads the previous working-
|
|
||||||
// res texture and writes the next working-res texture. Fullscreen-triangle vertex
|
|
||||||
// output; gl_FragCoord.xy is in working-res target pixel space; UV =
|
|
||||||
// gl_FragCoord.xy * inv_working_size.
|
|
||||||
//
|
|
||||||
// Mode 1: composite. Reads the fully-blurred working-res texture, applies the SDF mask and
|
|
||||||
// tint, writes to source_texture. Instanced unit-quad vertex output covering the
|
|
||||||
// per-primitive bounds; gl_FragCoord.xy is in the full-resolution render target;
|
|
||||||
// UV into the blurred working texture =
|
|
||||||
// (gl_FragCoord.xy * inv_downsample_factor) * inv_working_size.
|
|
||||||
// No kernel is applied here — the blur is already complete.
|
|
||||||
//
|
|
||||||
// V-blur is run as its own working→working pass rather than folded into the composite. The
|
|
||||||
// folded variant produced a horizontal-vs-vertical asymmetry artifact: when V-blur sampled
|
|
||||||
// the H-blur output through the bilinear-upsample/SDF-mask/tint pipeline in one shader
|
|
||||||
// invocation, horizontal source features ended up looking sharper than vertical ones.
|
|
||||||
// Matching V's structure exactly to H's restores symmetry.
|
|
||||||
|
|
||||||
const uint MAX_KERNEL_PAIRS = 32;
|
|
||||||
|
|
||||||
// --- Inputs from vertex shader ---
|
|
||||||
layout(location = 0) in vec2 p_local;
|
|
||||||
layout(location = 1) in mediump vec4 f_color;
|
|
||||||
layout(location = 2) flat in vec2 f_half_size_ppx;
|
|
||||||
layout(location = 3) flat in vec4 f_radii_ppx;
|
|
||||||
layout(location = 4) flat in float f_half_feather_ppx;
|
|
||||||
|
|
||||||
// --- Output ---
|
|
||||||
layout(location = 0) out vec4 out_color;
|
|
||||||
|
|
||||||
// --- Sampler ---
|
|
||||||
// Mode 0: bound to downsample_texture. Mode 1: bound to h_blur_texture.
|
|
||||||
layout(set = 2, binding = 0) uniform sampler2D blur_input_tex;
|
|
||||||
|
|
||||||
// --- Uniforms (set 3) ---
|
|
||||||
// Per-bracket-substage. `mode` matches the vertex shader's mode (0 = H, 1 = V).
|
|
||||||
// `direction` selects the kernel axis for blur offsets.
|
|
||||||
// `kernel` holds the per-sigma weight/offset pairs computed CPU-side using the
|
|
||||||
// linear-sampling pair adjustment (RAD/Rákos).
|
|
||||||
layout(set = 3, binding = 0) uniform Uniforms {
|
|
||||||
vec2 inv_working_size; // 1.0 / working-resolution texture dimensions
|
|
||||||
uint pair_count; // number of (weight, offset) pairs; pair[0] is the center
|
|
||||||
uint mode; // 0 = H-blur, 1 = V-composite
|
|
||||||
vec2 direction; // (1,0) for H, (0,1) for V — multiplied into the kernel offset
|
|
||||||
float inv_downsample_factor; // 1.0 / downsample_factor (mode 1 only; mode 0 ignores)
|
|
||||||
float _pad0;
|
|
||||||
vec4 kernel[MAX_KERNEL_PAIRS]; // .x = weight (paired-sum for idx>0), .y = offset (texels)
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
|
||||||
// ----- SDF helper --------------------
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
float sdRoundedBox(vec2 p, vec2 b, vec4 r) {
|
|
||||||
vec2 rxy = (p.x > 0.0) ? r.xy : r.zw;
|
|
||||||
float rr = (p.y > 0.0) ? rxy.x : rxy.y;
|
|
||||||
vec2 q = abs(p) - b;
|
|
||||||
if (rr == 0.0) {
|
|
||||||
return max(q.x, q.y);
|
|
||||||
}
|
|
||||||
q += rr;
|
|
||||||
return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - rr;
|
|
||||||
}
|
|
||||||
|
|
||||||
float sdf_alpha(float d, float h) {
|
|
||||||
return 1.0 - smoothstep(-h, h, d);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
|
||||||
// ----- Blur sample loop --------------
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
vec3 blur_sample(vec2 uv) {
|
|
||||||
vec3 color = kernel[0].x * texture(blur_input_tex, uv).rgb;
|
|
||||||
|
|
||||||
// Per-pair offset in texel space, projected onto the active axis.
|
|
||||||
vec2 axis_step = direction * inv_working_size;
|
|
||||||
|
|
||||||
for (uint i = 1u; i < pair_count; i += 1u) {
|
|
||||||
float w = kernel[i].x;
|
|
||||||
float off = kernel[i].y;
|
|
||||||
vec2 step_uv = off * axis_step;
|
|
||||||
color += w * texture(blur_input_tex, uv - step_uv).rgb;
|
|
||||||
color += w * texture(blur_input_tex, uv + step_uv).rgb;
|
|
||||||
}
|
|
||||||
|
|
||||||
return color;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
|
||||||
// ----- Main --------------------------
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
if (mode == 0u) {
|
|
||||||
// ---- Mode 0: 1D separable blur (used for both H-pass and V-pass).
|
|
||||||
// gl_FragCoord is in working-res target pixel space; sample the previous working-res
|
|
||||||
// texture along `direction` with the kernel.
|
|
||||||
vec2 uv = gl_FragCoord.xy * inv_working_size;
|
|
||||||
vec3 color = blur_sample(uv);
|
|
||||||
out_color = vec4(color, 1.0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Mode 1: composite per-primitive.
|
|
||||||
// RRect SDF — early discard for fragments well outside the masked region.
|
|
||||||
float d = sdRoundedBox(p_local, f_half_size_ppx, f_radii_ppx);
|
|
||||||
if (d > f_half_feather_ppx) {
|
|
||||||
discard;
|
|
||||||
}
|
|
||||||
|
|
||||||
// fwidth-based normalization for AA (matches main pipeline approach).
|
|
||||||
float grad_magnitude = max(fwidth(d), 1e-6);
|
|
||||||
float d_n = d / grad_magnitude;
|
|
||||||
float h_n = f_half_feather_ppx / grad_magnitude;
|
|
||||||
|
|
||||||
// Sample the fully-blurred working-res texture. gl_FragCoord is full-res; convert to
|
|
||||||
// working-res UV via inv_downsample_factor. No kernel is applied — the H+V blur passes
|
|
||||||
// already produced the final blurred image; this is just an upsample + tint.
|
|
||||||
vec2 uv = (gl_FragCoord.xy * inv_downsample_factor) * inv_working_size;
|
|
||||||
vec3 color = texture(blur_input_tex, uv).rgb;
|
|
||||||
|
|
||||||
// Tint composition: inside the masked region the panel is fully opaque — it completely
|
|
||||||
// hides the original framebuffer content, just like real frosted glass and like iOS
|
|
||||||
// UIBlurEffect / CSS backdrop-filter. f_color.rgb specifies the tint color; f_color.a
|
|
||||||
// specifies the tint *mix strength* (NOT panel opacity). At alpha=0 we see the pure
|
|
||||||
// blur; at alpha=255 we see the blur fully multiplied by the tint color.
|
|
||||||
//
|
|
||||||
// Output is premultiplied to match the ONE, ONE_MINUS_SRC_ALPHA blend state. Coverage
|
|
||||||
// (the SDF mask's edge AA) modulates only the alpha channel, never the panel-vs-source
|
|
||||||
// blend; that way edge pixels still feather correctly while mid-panel pixels stay fully
|
|
||||||
// opaque.
|
|
||||||
mediump vec3 tinted = mix(color, color * f_color.rgb, f_color.a);
|
|
||||||
mediump float coverage = sdf_alpha(d_n, h_n);
|
|
||||||
out_color = vec4(tinted * coverage, coverage);
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
#version 450 core
|
|
||||||
|
|
||||||
// Unified backdrop blur vertex shader.
|
|
||||||
// Handles both the 1D separable blur passes (fullscreen triangle, mode 0; used for
|
|
||||||
// BOTH the H-pass and V-pass) and the composite pass (instanced unit-quad over
|
|
||||||
// Gaussian_Blur_Primitive storage buffer, mode 1) for the second PSO of the backdrop bracket.
|
|
||||||
// The first PSO (downsample) uses backdrop_fullscreen.vert.
|
|
||||||
//
|
|
||||||
// No vertex buffer for either mode. Mode 0 uses gl_VertexIndex 0..2 for a single
|
|
||||||
// fullscreen triangle; mode 1 uses gl_VertexIndex 0..5 for a unit-quad (two
|
|
||||||
// triangles, TRIANGLELIST topology) and gl_InstanceIndex to select the primitive.
|
|
||||||
//
|
|
||||||
// Mode 0 viewport+scissor are CPU-set per sigma group to the work region (union AABB
|
|
||||||
// of that group's backdrop primitives + halo, clamped to swapchain bounds). Mode 1
|
|
||||||
// renders into source_texture with the screen-space orthographic projection; the
|
|
||||||
// per-primitive bounds drive the quad in screen space.
|
|
||||||
//
|
|
||||||
// Backdrop primitives have NO rotation — backdrop sampling is in screen space, so
|
|
||||||
// a rotated mask over a stationary blur sample would look wrong.
|
|
||||||
|
|
||||||
// --- Outputs to fragment shader ---
|
|
||||||
// p_local: shape-local position in physical pixels (origin at shape center).
|
|
||||||
// Only meaningful in mode 1 (V-composite). Zero-init for mode 0.
|
|
||||||
layout(location = 0) out vec2 p_local;
|
|
||||||
// f_color: tint, unpacked from primitive.color. Only meaningful in mode 1.
|
|
||||||
layout(location = 1) out mediump vec4 f_color;
|
|
||||||
// f_half_size_ppx: RRect half extents in physical pixels (mode 1 only).
|
|
||||||
layout(location = 2) flat out vec2 f_half_size_ppx;
|
|
||||||
// f_radii_ppx: per-corner radii in physical pixels (mode 1 only).
|
|
||||||
layout(location = 3) flat out vec4 f_radii_ppx;
|
|
||||||
// f_half_feather_ppx: SDF anti-aliasing feather in physical pixels (mode 1 only).
|
|
||||||
layout(location = 4) flat out float f_half_feather_ppx;
|
|
||||||
|
|
||||||
// --- Uniforms (set 1) ---
|
|
||||||
// Backdrop pipeline's own uniform block — distinct from the main pipeline's
|
|
||||||
// Vertex_Uniforms_2D. `mode` selects between H-blur (0) and V-composite (1).
|
|
||||||
layout(set = 1, binding = 0) uniform Uniforms {
|
|
||||||
mat4 projection;
|
|
||||||
float dpi_scale;
|
|
||||||
uint mode; // 0 = H-blur, 1 = V-composite
|
|
||||||
vec2 _pad0;
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- Gaussian blur primitive storage buffer (set 0) ---
|
|
||||||
// 48 bytes, std430-natural layout (no implicit padding). vec4 members are
|
|
||||||
// front-loaded so their 16-byte alignment is satisfied without holes; the
|
|
||||||
// vec2 and scalar tail packs tight to land the struct at a clean 48-byte
|
|
||||||
// stride (a multiple of 16, so the array stride needs no rounding either).
|
|
||||||
// Field semantics match the CPU-side Gaussian_Blur_Primitive declared in
|
|
||||||
// levlib/draw/backdrop.odin; keep both in sync.
|
|
||||||
//
|
|
||||||
// Gaussian blur primitives are tint-only: outline is intentionally absent. Specialized
|
|
||||||
// edge effects (e.g. liquid-glass-style refraction outlines) would be a dedicated
|
|
||||||
// primitive type with its own pipeline rather than a flag bit here.
|
|
||||||
struct Gaussian_Blur_Primitive {
|
|
||||||
vec4 bounds; // 0-15: min_xy, max_xy (world-space, logical px)
|
|
||||||
vec4 radii_ppx; // 16-31: per-corner radii
|
|
||||||
vec2 half_size_ppx; // 32-39: RRect half extents
|
|
||||||
float half_feather_ppx; // 40-43: SDF anti-aliasing feather
|
|
||||||
uint color; // 44-47: tint, packed RGBA u8x4
|
|
||||||
};
|
|
||||||
|
|
||||||
layout(std430, set = 0, binding = 0) readonly buffer Gaussian_Blur_Primitives {
|
|
||||||
Gaussian_Blur_Primitive primitives[];
|
|
||||||
};
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
if (mode == 0u) {
|
|
||||||
// ---- Mode 0: H-blur fullscreen triangle ----
|
|
||||||
// gl_VertexIndex 0 -> ( -1, -1)
|
|
||||||
// gl_VertexIndex 1 -> ( 3, -1)
|
|
||||||
// gl_VertexIndex 2 -> ( -1, 3)
|
|
||||||
vec2 ndc = vec2(
|
|
||||||
(gl_VertexIndex == 1) ? 3.0 : -1.0,
|
|
||||||
(gl_VertexIndex == 2) ? 3.0 : -1.0);
|
|
||||||
gl_Position = vec4(ndc, 0.0, 1.0);
|
|
||||||
|
|
||||||
// Mode 0 doesn't read the per-primitive varyings; zero-init for safety.
|
|
||||||
p_local = vec2(0.0);
|
|
||||||
f_color = vec4(0.0);
|
|
||||||
f_half_size_ppx = vec2(0.0);
|
|
||||||
f_radii_ppx = vec4(0.0);
|
|
||||||
f_half_feather_ppx = 0.0;
|
|
||||||
} else {
|
|
||||||
// ---- Mode 1: V-composite instanced unit-quad over Gaussian_Blur_Primitive ----
|
|
||||||
Gaussian_Blur_Primitive p = primitives[gl_InstanceIndex];
|
|
||||||
|
|
||||||
// Unit-quad corners for TRIANGLELIST (2 triangles, 6 vertices):
|
|
||||||
// index 0 -> (0,0) index 3 -> (0,1)
|
|
||||||
// index 1 -> (1,0) index 4 -> (1,0)
|
|
||||||
// index 2 -> (0,1) index 5 -> (1,1)
|
|
||||||
vec2 quad_corners[6] = vec2[6](
|
|
||||||
vec2(0.0, 0.0), vec2(1.0, 0.0), vec2(0.0, 1.0),
|
|
||||||
vec2(0.0, 1.0), vec2(1.0, 0.0), vec2(1.0, 1.0));
|
|
||||||
vec2 corner = quad_corners[gl_VertexIndex];
|
|
||||||
|
|
||||||
vec2 world_pos = mix(p.bounds.xy, p.bounds.zw, corner);
|
|
||||||
vec2 center = 0.5 * (p.bounds.xy + p.bounds.zw);
|
|
||||||
|
|
||||||
// Shape-local position in physical pixels (no rotation for backdrops).
|
|
||||||
p_local = (world_pos - center) * dpi_scale;
|
|
||||||
|
|
||||||
f_color = unpackUnorm4x8(p.color);
|
|
||||||
f_half_size_ppx = p.half_size_ppx;
|
|
||||||
f_radii_ppx = p.radii_ppx;
|
|
||||||
f_half_feather_ppx = p.half_feather_ppx;
|
|
||||||
|
|
||||||
gl_Position = projection * vec4(world_pos * dpi_scale, 0.0, 1.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
#version 450 core
|
|
||||||
|
|
||||||
// Backdrop downsample fragment shader.
|
|
||||||
// Reads source_texture (full-resolution snapshot of pre-bracket framebuffer contents) and
|
|
||||||
// writes a downsampled copy at factor 1, 2, or 4. The output is the working texture (sized
|
|
||||||
// at full swapchain resolution); larger factors only fill a sub-rect of it via the CPU-set
|
|
||||||
// viewport. See backdrop.odin for the factor selection table (Flutter-style).
|
|
||||||
//
|
|
||||||
// Shader paths by factor:
|
|
||||||
//
|
|
||||||
// factor=1: identity copy. One bilinear tap aligned to the source pixel center. Useful
|
|
||||||
// when sigma is small enough that any downsample round-trip would visibly soften
|
|
||||||
// the output (Flutter does this for sigma_phys ≤ 4).
|
|
||||||
//
|
|
||||||
// factor=2: each output covers a 2×2 source block. Single bilinear tap at the shared
|
|
||||||
// corner reads all 4 source pixels with 0.25 weight.
|
|
||||||
//
|
|
||||||
// factor=4: each output covers a 4×4 source block. We use 4 bilinear taps, each at the
|
|
||||||
// shared corner of a 2×2 sub-block. Each tap reads 4 source pixels uniformly;
|
|
||||||
// combined, the 4 taps sample 16 source pixels arranged uniformly across the
|
|
||||||
// block (full coverage at factor=4). The factor>=4 path is structured so the
|
|
||||||
// same shader code would extend to factor=8 (16 pixels of 64) or factor=16 (16
|
|
||||||
// of 256) if the CPU-side cap is ever raised, though the current cap is 4.
|
|
||||||
//
|
|
||||||
// The viewport+scissor are set by the CPU to limit output to the layer's work region in
|
|
||||||
// working-texture coords (work_region_phys / factor), clamped to the texture bounds.
|
|
||||||
|
|
||||||
layout(set = 3, binding = 0) uniform Uniforms {
|
|
||||||
vec2 inv_source_size; // 1.0 / source_texture pixel dimensions
|
|
||||||
uint downsample_factor; // 1, 2, 4, 8, or 16
|
|
||||||
uint _pad0;
|
|
||||||
};
|
|
||||||
|
|
||||||
layout(set = 2, binding = 0) uniform sampler2D source_tex;
|
|
||||||
|
|
||||||
layout(location = 0) out vec4 out_color;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
// Output pixel index (i): gl_FragCoord.xy - 0.5. Source-pixel block top-left for this
|
|
||||||
// output: i * factor. Center of the block: i*factor + factor/2 = gl_FragCoord.xy * factor.
|
|
||||||
vec2 src_block_center = gl_FragCoord.xy * float(downsample_factor);
|
|
||||||
|
|
||||||
if (downsample_factor == 1u) {
|
|
||||||
// Identity copy. UV at src_block_center hits the source pixel center directly.
|
|
||||||
vec2 uv = src_block_center * inv_source_size;
|
|
||||||
out_color = texture(source_tex, uv);
|
|
||||||
} else if (downsample_factor == 2u) {
|
|
||||||
// Single tap at the shared corner of the 2×2 source block; one bilinear sample reads
|
|
||||||
// all 4 source pixels with equal 0.25 weights — uniform 2×2 box filter for free.
|
|
||||||
vec2 uv = src_block_center * inv_source_size;
|
|
||||||
out_color = texture(source_tex, uv);
|
|
||||||
} else {
|
|
||||||
// Four taps at offsets ±(factor/4) from the block center. Each tap lands on a corner
|
|
||||||
// shared by 4 source pixels of a (factor/2)×(factor/2) sub-block (equivalent at the
|
|
||||||
// bilinear level), giving a 4-tap = 16-source-pixel uniform sample of the block.
|
|
||||||
float off = float(downsample_factor) * 0.25;
|
|
||||||
vec2 uv_tl = (src_block_center + vec2(-off, -off)) * inv_source_size;
|
|
||||||
vec2 uv_tr = (src_block_center + vec2(off, -off)) * inv_source_size;
|
|
||||||
vec2 uv_bl = (src_block_center + vec2(-off, off)) * inv_source_size;
|
|
||||||
vec2 uv_br = (src_block_center + vec2(off, off)) * inv_source_size;
|
|
||||||
vec4 c = texture(source_tex, uv_tl)
|
|
||||||
+ texture(source_tex, uv_tr)
|
|
||||||
+ texture(source_tex, uv_bl)
|
|
||||||
+ texture(source_tex, uv_br);
|
|
||||||
out_color = c * 0.25;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
#version 450 core
|
|
||||||
|
|
||||||
// Fullscreen-triangle vertex shader for the backdrop downsample and H-blur sub-passes.
|
|
||||||
// Emits a single triangle covering NDC [-1,1]^2; the rasterizer clips edges outside.
|
|
||||||
// No vertex buffer; uses gl_VertexIndex to pick corners.
|
|
||||||
//
|
|
||||||
// The CPU sets the viewport (and matching scissor) per layer-bracket to limit work to
|
|
||||||
// the union AABB of the layer's backdrop primitives, expanded by 3*max_sigma and
|
|
||||||
// clamped to swapchain bounds. The fragment shader uses gl_FragCoord (absolute pixel
|
|
||||||
// space in the bound target) plus an inv-size uniform to compute its own UVs — see
|
|
||||||
// each fragment shader for the per-pass sampling math.
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
// gl_VertexIndex 0 -> ( -1, -1)
|
|
||||||
// gl_VertexIndex 1 -> ( 3, -1)
|
|
||||||
// gl_VertexIndex 2 -> ( -1, 3)
|
|
||||||
vec2 ndc = vec2(
|
|
||||||
(gl_VertexIndex == 1) ? 3.0 : -1.0,
|
|
||||||
(gl_VertexIndex == 2) ? 3.0 : -1.0);
|
|
||||||
gl_Position = vec4(ndc, 0.0, 1.0);
|
|
||||||
}
|
|
||||||
@@ -6,8 +6,8 @@ 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_flags;
|
||||||
layout(location = 6) flat in vec4 f_uv_rect;
|
layout(location = 5) flat in uint f_rotation_sc;
|
||||||
layout(location = 7) flat in uvec4 f_effects;
|
layout(location = 6) flat in uvec4 f_uv_or_effects;
|
||||||
|
|
||||||
// --- Output ---
|
// --- Output ---
|
||||||
layout(location = 0) out vec4 out_color;
|
layout(location = 0) out vec4 out_color;
|
||||||
@@ -45,7 +45,7 @@ float sdRegularPolygon(vec2 p, float r, float n) {
|
|||||||
return length(p) * cos(bn) - r;
|
return length(p) * cos(bn) - r;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Coverage from SDF distance using half-feather width (feather_ppx * 0.5, pre-computed on CPU).
|
// Coverage from SDF distance using half-feather width (feather_px * 0.5, pre-computed on CPU).
|
||||||
// Produces a symmetric transition centered on d=0: smoothstep(-h, h, d).
|
// Produces a symmetric transition centered on d=0: smoothstep(-h, h, d).
|
||||||
float sdf_alpha(float d, float h) {
|
float sdf_alpha(float d, float h) {
|
||||||
return 1.0 - smoothstep(-h, h, d);
|
return 1.0 - smoothstep(-h, h, d);
|
||||||
@@ -80,56 +80,65 @@ void main() {
|
|||||||
|
|
||||||
// SDF path — dispatch on kind
|
// SDF path — dispatch on kind
|
||||||
float d = 1e30;
|
float d = 1e30;
|
||||||
float h = 0.5; // half-feather width (physical px); overwritten per shape kind
|
float h = 0.5; // half-feather width; overwritten per shape kind
|
||||||
vec2 half_size_ppx = f_params.xy; // used by RRect and as reference size for gradients
|
vec2 half_size = f_params.xy; // used by RRect and as reference size for gradients
|
||||||
|
|
||||||
vec2 p_local_ppx = f_local_or_uv; // arrives rotated; vertex shader handled .Rotated
|
vec2 p_local = f_local_or_uv;
|
||||||
|
|
||||||
|
// Apply inverse rotation using pre-computed sin/cos (no per-pixel trig).
|
||||||
|
// .Rotated flag = bit 4 = 16u
|
||||||
|
if ((flags & 16u) != 0u) {
|
||||||
|
vec2 sc = unpackHalf2x16(f_rotation_sc); // .x = sin(angle), .y = cos(angle)
|
||||||
|
// Inverse rotation matrix R(-angle) = [[cos, sin], [-sin, cos]]
|
||||||
|
p_local = vec2(sc.y * p_local.x + sc.x * p_local.y,
|
||||||
|
-sc.x * p_local.x + sc.y * p_local.y);
|
||||||
|
}
|
||||||
|
|
||||||
if (kind == 1u) {
|
if (kind == 1u) {
|
||||||
// RRect — half_feather_ppx in params2.z
|
// RRect — half_feather in params2.z
|
||||||
vec4 corner_radii_ppx = vec4(f_params.zw, f_params2.xy);
|
vec4 corner_radii = vec4(f_params.zw, f_params2.xy);
|
||||||
h = f_params2.z;
|
h = f_params2.z;
|
||||||
d = sdRoundedBox(p_local_ppx, half_size_ppx, corner_radii_ppx);
|
d = sdRoundedBox(p_local, half_size, corner_radii);
|
||||||
}
|
}
|
||||||
else if (kind == 2u) {
|
else if (kind == 2u) {
|
||||||
// NGon — half_feather_ppx in params.z
|
// NGon — half_feather in params.z
|
||||||
float radius_ppx = f_params.x;
|
float radius = f_params.x;
|
||||||
float sides = f_params.y;
|
float sides = f_params.y;
|
||||||
h = f_params.z;
|
h = f_params.z;
|
||||||
d = sdRegularPolygon(p_local_ppx, radius_ppx, sides);
|
d = sdRegularPolygon(p_local, radius, sides);
|
||||||
half_size_ppx = vec2(radius_ppx); // for gradient UV computation
|
half_size = vec2(radius); // for gradient UV computation
|
||||||
}
|
}
|
||||||
else if (kind == 3u) {
|
else if (kind == 3u) {
|
||||||
// Ellipse — half_feather_ppx in params.z
|
// Ellipse — half_feather in params.z
|
||||||
vec2 radii_ppx = f_params.xy;
|
vec2 ab = f_params.xy;
|
||||||
h = f_params.z;
|
h = f_params.z;
|
||||||
d = sdEllipseApprox(p_local_ppx, radii_ppx);
|
d = sdEllipseApprox(p_local, ab);
|
||||||
half_size_ppx = radii_ppx; // for gradient UV computation
|
half_size = ab; // for gradient UV computation
|
||||||
}
|
}
|
||||||
else if (kind == 4u) {
|
else if (kind == 4u) {
|
||||||
// Ring_Arc — half_feather_ppx in params2.z
|
// Ring_Arc — half_feather in params2.z
|
||||||
// Arc mode from flag bits 5-6: 0 = full, 1 = narrow (≤π), 2 = wide (>π)
|
// Arc mode from flag bits 5-6: 0 = full, 1 = narrow (≤π), 2 = wide (>π)
|
||||||
float inner_radius_ppx = f_params.x;
|
float inner = f_params.x;
|
||||||
float outer_radius_ppx = f_params.y;
|
float outer = f_params.y;
|
||||||
vec2 n_start = f_params.zw;
|
vec2 n_start = f_params.zw;
|
||||||
vec2 n_end = f_params2.xy;
|
vec2 n_end = f_params2.xy;
|
||||||
uint arc_bits = (flags >> 5u) & 3u;
|
uint arc_bits = (flags >> 5u) & 3u;
|
||||||
|
|
||||||
h = f_params2.z;
|
h = f_params2.z;
|
||||||
|
|
||||||
float r = length(p_local_ppx);
|
float r = length(p_local);
|
||||||
d = max(inner_radius_ppx - r, r - outer_radius_ppx);
|
d = max(inner - r, r - outer);
|
||||||
|
|
||||||
if (arc_bits != 0u) {
|
if (arc_bits != 0u) {
|
||||||
float d_start = dot(p_local_ppx, n_start);
|
float d_start = dot(p_local, n_start);
|
||||||
float d_end = dot(p_local_ppx, n_end);
|
float d_end = dot(p_local, n_end);
|
||||||
float d_wedge = (arc_bits == 1u)
|
float d_wedge = (arc_bits == 1u)
|
||||||
? max(d_start, d_end) // arc ≤ π: intersect half-planes
|
? max(d_start, d_end) // arc ≤ π: intersect half-planes
|
||||||
: min(d_start, d_end); // arc > π: union half-planes
|
: min(d_start, d_end); // arc > π: union half-planes
|
||||||
d = max(d, d_wedge);
|
d = max(d, d_wedge);
|
||||||
}
|
}
|
||||||
|
|
||||||
half_size_ppx = vec2(outer_radius_ppx); // for gradient UV computation
|
half_size = vec2(outer); // for gradient UV computation
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- fwidth-based normalization for correct AA and stroke width ---
|
// --- fwidth-based normalization for correct AA and stroke width ---
|
||||||
@@ -142,22 +151,22 @@ void main() {
|
|||||||
if ((flags & 2u) != 0u) {
|
if ((flags & 2u) != 0u) {
|
||||||
// Gradient active (bit 1)
|
// Gradient active (bit 1)
|
||||||
mediump vec4 gradient_start = f_color;
|
mediump vec4 gradient_start = f_color;
|
||||||
mediump vec4 gradient_end = unpackUnorm4x8(f_effects.x);
|
mediump vec4 gradient_end = unpackUnorm4x8(f_uv_or_effects.x);
|
||||||
|
|
||||||
if ((flags & 4u) != 0u) {
|
if ((flags & 4u) != 0u) {
|
||||||
// Radial gradient (bit 2): t from distance to center
|
// Radial gradient (bit 2): t from distance to center
|
||||||
mediump float t = length(p_local_ppx / half_size_ppx);
|
mediump float t = length(p_local / half_size);
|
||||||
shape_color = gradient_2color(gradient_start, gradient_end, t);
|
shape_color = gradient_2color(gradient_start, gradient_end, t);
|
||||||
} else {
|
} else {
|
||||||
// Linear gradient: direction pre-computed on CPU as (cos, sin) f16 pair
|
// Linear gradient: direction pre-computed on CPU as (cos, sin) f16 pair
|
||||||
vec2 direction = unpackHalf2x16(f_effects.z);
|
vec2 direction = unpackHalf2x16(f_uv_or_effects.z);
|
||||||
mediump float t = dot(p_local_ppx / half_size_ppx, direction) * 0.5 + 0.5;
|
mediump float t = dot(p_local / half_size, direction) * 0.5 + 0.5;
|
||||||
shape_color = gradient_2color(gradient_start, gradient_end, t);
|
shape_color = gradient_2color(gradient_start, gradient_end, t);
|
||||||
}
|
}
|
||||||
} else if ((flags & 1u) != 0u) {
|
} else if ((flags & 1u) != 0u) {
|
||||||
// Textured (bit 0)
|
// Textured (bit 0) — RRect only in practice
|
||||||
vec4 uv_rect = f_uv_rect;
|
vec4 uv_rect = uintBitsToFloat(f_uv_or_effects);
|
||||||
vec2 local_uv = p_local_ppx / half_size_ppx * 0.5 + 0.5;
|
vec2 local_uv = p_local / half_size * 0.5 + 0.5;
|
||||||
vec2 uv = mix(uv_rect.xy, uv_rect.zw, local_uv);
|
vec2 uv = mix(uv_rect.xy, uv_rect.zw, local_uv);
|
||||||
shape_color = f_color * texture(tex, uv);
|
shape_color = f_color * texture(tex, uv);
|
||||||
} else {
|
} else {
|
||||||
@@ -171,9 +180,9 @@ void main() {
|
|||||||
// AA at d=ol_width. The outline band's coverage is total_cov - fill_cov.
|
// 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.
|
// Output is premultiplied: blend state is ONE, ONE_MINUS_SRC_ALPHA.
|
||||||
if ((flags & 8u) != 0u) {
|
if ((flags & 8u) != 0u) {
|
||||||
mediump vec4 ol_color = unpackUnorm4x8(f_effects.y);
|
mediump vec4 ol_color = unpackUnorm4x8(f_uv_or_effects.y);
|
||||||
// Outline width in f_effects.w (low f16 half)
|
// Outline width in f_uv_or_effects.w (low f16 half)
|
||||||
float ol_width = unpackHalf2x16(f_effects.w).x / grad_magnitude;
|
float ol_width = unpackHalf2x16(f_uv_or_effects.w).x / grad_magnitude;
|
||||||
|
|
||||||
float fill_cov = sdf_alpha(d, h);
|
float fill_cov = sdf_alpha(d, h);
|
||||||
float total_cov = sdf_alpha(d - ol_width, h);
|
float total_cov = sdf_alpha(d - ol_width, h);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#version 450 core
|
#version 450 core
|
||||||
|
|
||||||
// ---------- Vertex attributes (used in all modes) ----------
|
// ---------- Vertex attributes (used in both modes) ----------
|
||||||
layout(location = 0) in vec2 v_position;
|
layout(location = 0) in vec2 v_position;
|
||||||
layout(location = 1) in vec2 v_uv;
|
layout(location = 1) in vec2 v_uv;
|
||||||
layout(location = 2) in vec4 v_color;
|
layout(location = 2) in vec4 v_color;
|
||||||
@@ -11,30 +11,18 @@ 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_flags;
|
||||||
|
layout(location = 5) flat out uint f_rotation_sc;
|
||||||
layout(location = 6) flat out vec4 f_uv_rect;
|
layout(location = 6) flat out uvec4 f_uv_or_effects;
|
||||||
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) ----------
|
||||||
// Mode values mirror Core_2D_Mode in core_2d.odin:
|
|
||||||
// 0 = Tessellated v_position is in logical pixels; shader scales by dpi_scale.
|
|
||||||
// 1 = SDF v_position is a unit-quad corner; world-space comes from
|
|
||||||
// primitives[gl_InstanceIndex].bounds (logical px). Shader
|
|
||||||
// scales by dpi_scale.
|
|
||||||
// 2 = Text v_position is in *physical* pixels already (the CPU baked
|
|
||||||
// the anchor snap and SDL_ttf glyph offsets, both physical).
|
|
||||||
// Shader must NOT rescale.
|
|
||||||
layout(set = 1, binding = 0) uniform Uniforms {
|
layout(set = 1, binding = 0) uniform Uniforms {
|
||||||
mat4 projection;
|
mat4 projection;
|
||||||
float dpi_scale;
|
float dpi_scale;
|
||||||
uint mode;
|
uint mode; // 0 = tessellated, 1 = SDF
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---------- 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
|
|
||||||
// declaration and the Odin declaration.
|
|
||||||
struct Core_2D_Primitive {
|
|
||||||
vec4 bounds; // 0-15
|
vec4 bounds; // 0-15
|
||||||
uint color; // 16-19
|
uint color; // 16-19
|
||||||
uint flags; // 20-23
|
uint flags; // 20-23
|
||||||
@@ -42,66 +30,42 @@ struct Core_2D_Primitive {
|
|||||||
float _pad; // 28-31
|
float _pad; // 28-31
|
||||||
vec4 params; // 32-47
|
vec4 params; // 32-47
|
||||||
vec4 params2; // 48-63
|
vec4 params2; // 48-63
|
||||||
vec4 uv_rect; // 64-79: texture UV coordinates (read when .Textured)
|
uvec4 uv_or_effects; // 64-79
|
||||||
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 == 1u) {
|
if (mode == 0u) {
|
||||||
// ---- Mode 1: SDF instanced quads ----
|
// ---- Mode 0: Tessellated (legacy) ----
|
||||||
Core_2D_Primitive p = primitives[gl_InstanceIndex];
|
|
||||||
|
|
||||||
vec2 corner = v_position; // unit quad corners: (0,0)-(1,1)
|
|
||||||
vec2 world_pos = mix(p.bounds.xy, p.bounds.zw, corner);
|
|
||||||
vec2 center = 0.5 * (p.bounds.xy + p.bounds.zw);
|
|
||||||
|
|
||||||
// Compute shape-local position. Apply inverse rotation here in the vertex
|
|
||||||
// shader; the rasterizer interpolates the rotated values across the quad,
|
|
||||||
// which is mathematically equivalent to per-fragment rotation under 2D ortho
|
|
||||||
// projection. Frees one fragment-shader varying and per-pixel rotation math.
|
|
||||||
vec2 local = (world_pos - center) * dpi_scale;
|
|
||||||
uint flags = (p.flags >> 8u) & 0xFFu;
|
|
||||||
if ((flags & 16u) != 0u) {
|
|
||||||
// Rotated flag (bit 4); rotation_sc holds packed f16 (sin, cos).
|
|
||||||
// Inverse rotation matrix R(-angle) = [[cos, sin], [-sin, cos]].
|
|
||||||
vec2 sc = unpackHalf2x16(p.rotation_sc);
|
|
||||||
local = vec2(sc.y * local.x + sc.x * local.y,
|
|
||||||
-sc.x * local.x + sc.y * local.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
f_color = unpackUnorm4x8(p.color);
|
|
||||||
f_local_or_uv = local; // shape-local physical pixels (rotated if .Rotated set)
|
|
||||||
f_params = p.params;
|
|
||||||
f_params2 = p.params2;
|
|
||||||
f_flags = p.flags;
|
|
||||||
f_uv_rect = p.uv_rect;
|
|
||||||
f_effects = p.effects;
|
|
||||||
|
|
||||||
gl_Position = projection * vec4(world_pos * dpi_scale, 0.0, 1.0);
|
|
||||||
} else {
|
|
||||||
// ---- Mode 0 (Tessellated) and Mode 2 (Text) ----
|
|
||||||
// Both feed the raw-vertex pipeline (kind 0 in the fragment shader).
|
|
||||||
// They differ only in what coord space `v_position` is in:
|
|
||||||
// Mode 0 — logical pixels, scale here by dpi_scale.
|
|
||||||
// Mode 2 — physical pixels (CPU pre-scaled and snapped to integer
|
|
||||||
// physical pixels for atlas-aligned bilinear sampling).
|
|
||||||
// Do NOT rescale.
|
|
||||||
// `mode` is uniform across the workgroup, so the select compiles to a
|
|
||||||
// uniform-controlled branch with no SIMT divergence cost.
|
|
||||||
f_color = v_color;
|
f_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_flags = 0u;
|
||||||
f_uv_rect = vec4(0.0);
|
f_rotation_sc = 0u;
|
||||||
f_effects = uvec4(0);
|
f_uv_or_effects = uvec4(0);
|
||||||
|
|
||||||
vec2 pos = (mode == 2u) ? v_position : (v_position * dpi_scale);
|
gl_Position = projection * vec4(v_position * dpi_scale, 0.0, 1.0);
|
||||||
gl_Position = projection * vec4(pos, 0.0, 1.0);
|
} else {
|
||||||
|
// ---- Mode 1: SDF instanced quads ----
|
||||||
|
Primitive p = primitives[gl_InstanceIndex];
|
||||||
|
|
||||||
|
vec2 corner = v_position; // unit quad corners: (0,0)-(1,1)
|
||||||
|
vec2 world_pos = mix(p.bounds.xy, p.bounds.zw, corner);
|
||||||
|
vec2 center = 0.5 * (p.bounds.xy + p.bounds.zw);
|
||||||
|
|
||||||
|
f_color = unpackUnorm4x8(p.color);
|
||||||
|
f_local_or_uv = (world_pos - center) * dpi_scale; // shape-centered physical pixels
|
||||||
|
f_params = p.params;
|
||||||
|
f_params2 = p.params2;
|
||||||
|
f_flags = p.flags;
|
||||||
|
f_rotation_sc = p.rotation_sc;
|
||||||
|
f_uv_or_effects = p.uv_or_effects;
|
||||||
|
|
||||||
|
gl_Position = projection * vec4(world_pos * dpi_scale, 0.0, 1.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,776 @@
|
|||||||
|
package draw
|
||||||
|
|
||||||
|
import "core:math"
|
||||||
|
|
||||||
|
// ----- Internal helpers ----
|
||||||
|
|
||||||
|
// Internal
|
||||||
|
extrude_line :: proc(
|
||||||
|
start, end_pos: Vec2,
|
||||||
|
thickness: f32,
|
||||||
|
color: Color,
|
||||||
|
vertices: []Vertex,
|
||||||
|
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 := 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a vertex for solid-color shape drawing (no texture, UV defaults to zero).
|
||||||
|
// Color is premultiplied: the tessellated fragment shader passes it through directly
|
||||||
|
// and the blend state is ONE, ONE_MINUS_SRC_ALPHA.
|
||||||
|
solid_vertex :: proc(position: Vec2, color: Color) -> Vertex {
|
||||||
|
return Vertex{position = position, color = premultiply_color(color)}
|
||||||
|
}
|
||||||
|
|
||||||
|
emit_rectangle :: proc(x, y, width, height: f32, color: Color, vertices: []Vertex, 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
|
||||||
|
prepare_sdf_primitive_textured :: proc(
|
||||||
|
layer: ^Layer,
|
||||||
|
prim: Primitive,
|
||||||
|
texture_id: Texture_Id,
|
||||||
|
sampler: Sampler_Preset,
|
||||||
|
) {
|
||||||
|
offset := u32(len(GLOB.tmp_primitives))
|
||||||
|
append(&GLOB.tmp_primitives, prim)
|
||||||
|
scissor := &GLOB.scissors[layer.scissor_start + layer.scissor_len - 1]
|
||||||
|
append_or_extend_sub_batch(scissor, layer, .SDF, offset, 1, texture_id, sampler)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Internal
|
||||||
|
//
|
||||||
|
// Compute the visual center of a center-parametrized shape after applying
|
||||||
|
// Convention B origin semantics: `center` is where the origin-point lands in
|
||||||
|
// world space; the visual center is offset by -origin and then rotated around
|
||||||
|
// the landing point.
|
||||||
|
// visual_center = center + R(θ) · (-origin)
|
||||||
|
// When θ=0: visual_center = center - origin (pure positioning shift).
|
||||||
|
// When origin={0,0}: visual_center = center (no change).
|
||||||
|
compute_pivot_center :: proc(center: Vec2, origin: Vec2, sin_angle, cos_angle: f32) -> Vec2 {
|
||||||
|
if origin == {0, 0} do return center
|
||||||
|
return(
|
||||||
|
center +
|
||||||
|
{cos_angle * (-origin.x) - sin_angle * (-origin.y), sin_angle * (-origin.x) + cos_angle * (-origin.y)} \
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute the AABB half-extents of a rectangle with half-size (half_width, half_height) rotated by the given cos/sin.
|
||||||
|
rotated_aabb_half_extents :: proc(half_width, half_height, cos_angle, sin_angle: f32) -> [2]f32 {
|
||||||
|
cos_abs := abs(cos_angle)
|
||||||
|
sin_abs := abs(sin_angle)
|
||||||
|
return {half_width * cos_abs + half_height * sin_abs, half_width * sin_abs + half_height * cos_abs}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pack sin/cos into the Primitive.rotation_sc field as two f16 values.
|
||||||
|
pack_rotation_sc :: #force_inline proc(sin_angle, cos_angle: f32) -> u32 {
|
||||||
|
return pack_f16_pair(f16(sin_angle), f16(cos_angle))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Internal
|
||||||
|
//
|
||||||
|
// Build an RRect Primitive with bounds, params, and rotation computed from rectangle geometry.
|
||||||
|
// The caller sets color, flags, and uv fields on the returned primitive before submitting.
|
||||||
|
build_rrect_primitive :: proc(
|
||||||
|
rect: Rectangle,
|
||||||
|
radii: Rectangle_Radii,
|
||||||
|
origin: Vec2,
|
||||||
|
rotation: f32,
|
||||||
|
feather_px: f32,
|
||||||
|
) -> Primitive {
|
||||||
|
max_radius := min(rect.width, rect.height) * 0.5
|
||||||
|
clamped_top_left := clamp(radii.top_left, 0, max_radius)
|
||||||
|
clamped_top_right := clamp(radii.top_right, 0, max_radius)
|
||||||
|
clamped_bottom_right := clamp(radii.bottom_right, 0, max_radius)
|
||||||
|
clamped_bottom_left := clamp(radii.bottom_left, 0, max_radius)
|
||||||
|
|
||||||
|
half_feather := feather_px * 0.5
|
||||||
|
padding := half_feather / GLOB.dpi_scaling
|
||||||
|
dpi_scale := GLOB.dpi_scaling
|
||||||
|
|
||||||
|
half_width := rect.width * 0.5
|
||||||
|
half_height := rect.height * 0.5
|
||||||
|
center_x := rect.x + half_width - origin.x
|
||||||
|
center_y := rect.y + half_height - origin.y
|
||||||
|
sin_angle: f32 = 0
|
||||||
|
cos_angle: f32 = 1
|
||||||
|
has_rotation := false
|
||||||
|
|
||||||
|
if needs_transform(origin, rotation) {
|
||||||
|
rotation_radians := math.to_radians(rotation)
|
||||||
|
sin_angle, cos_angle = math.sincos(rotation_radians)
|
||||||
|
has_rotation = rotation != 0
|
||||||
|
transform := build_pivot_rotation_sc({rect.x + origin.x, rect.y + origin.y}, origin, cos_angle, sin_angle)
|
||||||
|
new_center := apply_transform(transform, {half_width, half_height})
|
||||||
|
center_x = new_center.x
|
||||||
|
center_y = new_center.y
|
||||||
|
}
|
||||||
|
|
||||||
|
bounds_half_width, bounds_half_height := half_width, half_height
|
||||||
|
if has_rotation {
|
||||||
|
expanded := rotated_aabb_half_extents(half_width, half_height, cos_angle, sin_angle)
|
||||||
|
bounds_half_width = expanded.x
|
||||||
|
bounds_half_height = expanded.y
|
||||||
|
}
|
||||||
|
|
||||||
|
prim := Primitive {
|
||||||
|
bounds = {
|
||||||
|
center_x - bounds_half_width - padding,
|
||||||
|
center_y - bounds_half_height - padding,
|
||||||
|
center_x + bounds_half_width + padding,
|
||||||
|
center_y + bounds_half_height + padding,
|
||||||
|
},
|
||||||
|
rotation_sc = has_rotation ? pack_rotation_sc(sin_angle, cos_angle) : 0,
|
||||||
|
}
|
||||||
|
prim.params.rrect = RRect_Params {
|
||||||
|
half_size = {half_width * dpi_scale, half_height * dpi_scale},
|
||||||
|
radii = {
|
||||||
|
clamped_bottom_right * dpi_scale,
|
||||||
|
clamped_top_right * dpi_scale,
|
||||||
|
clamped_bottom_left * dpi_scale,
|
||||||
|
clamped_top_left * dpi_scale,
|
||||||
|
},
|
||||||
|
half_feather = half_feather,
|
||||||
|
}
|
||||||
|
return prim
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal
|
||||||
|
//
|
||||||
|
// Build an RRect Primitive for a circle (fully-rounded square RRect).
|
||||||
|
// The caller sets color, flags, and uv fields on the returned primitive before submitting.
|
||||||
|
build_circle_primitive :: proc(
|
||||||
|
center: Vec2,
|
||||||
|
radius: f32,
|
||||||
|
origin: Vec2,
|
||||||
|
rotation: f32,
|
||||||
|
feather_px: f32,
|
||||||
|
) -> Primitive {
|
||||||
|
half_feather := feather_px * 0.5
|
||||||
|
padding := half_feather / GLOB.dpi_scaling
|
||||||
|
dpi_scale := GLOB.dpi_scaling
|
||||||
|
|
||||||
|
actual_center := center
|
||||||
|
if origin != {0, 0} {
|
||||||
|
sin_a, cos_a := math.sincos(math.to_radians(rotation))
|
||||||
|
actual_center = compute_pivot_center(center, origin, sin_a, cos_a)
|
||||||
|
}
|
||||||
|
|
||||||
|
prim := Primitive {
|
||||||
|
bounds = {
|
||||||
|
actual_center.x - radius - padding,
|
||||||
|
actual_center.y - radius - padding,
|
||||||
|
actual_center.x + radius + padding,
|
||||||
|
actual_center.y + radius + padding,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
scaled_radius := radius * dpi_scale
|
||||||
|
prim.params.rrect = RRect_Params {
|
||||||
|
half_size = {scaled_radius, scaled_radius},
|
||||||
|
radii = {scaled_radius, scaled_radius, scaled_radius, scaled_radius},
|
||||||
|
half_feather = half_feather,
|
||||||
|
}
|
||||||
|
return prim
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal
|
||||||
|
//
|
||||||
|
// Build an Ellipse Primitive with bounds, params, and rotation computed from ellipse geometry.
|
||||||
|
// The caller sets color, flags, and uv fields on the returned primitive before submitting.
|
||||||
|
build_ellipse_primitive :: proc(
|
||||||
|
center: Vec2,
|
||||||
|
radius_horizontal, radius_vertical: f32,
|
||||||
|
origin: Vec2,
|
||||||
|
rotation: f32,
|
||||||
|
feather_px: f32,
|
||||||
|
) -> Primitive {
|
||||||
|
half_feather := feather_px * 0.5
|
||||||
|
padding := half_feather / GLOB.dpi_scaling
|
||||||
|
dpi_scale := GLOB.dpi_scaling
|
||||||
|
|
||||||
|
actual_center := center
|
||||||
|
sin_angle: f32 = 0
|
||||||
|
cos_angle: f32 = 1
|
||||||
|
has_rotation := false
|
||||||
|
|
||||||
|
if needs_transform(origin, rotation) {
|
||||||
|
rotation_radians := math.to_radians(rotation)
|
||||||
|
sin_angle, cos_angle = math.sincos(rotation_radians)
|
||||||
|
actual_center = compute_pivot_center(center, origin, sin_angle, cos_angle)
|
||||||
|
has_rotation = rotation != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
bound_horizontal, bound_vertical := radius_horizontal, radius_vertical
|
||||||
|
if has_rotation {
|
||||||
|
expanded := rotated_aabb_half_extents(radius_horizontal, radius_vertical, cos_angle, sin_angle)
|
||||||
|
bound_horizontal = expanded.x
|
||||||
|
bound_vertical = expanded.y
|
||||||
|
}
|
||||||
|
|
||||||
|
prim := Primitive {
|
||||||
|
bounds = {
|
||||||
|
actual_center.x - bound_horizontal - padding,
|
||||||
|
actual_center.y - bound_vertical - padding,
|
||||||
|
actual_center.x + bound_horizontal + padding,
|
||||||
|
actual_center.y + bound_vertical + padding,
|
||||||
|
},
|
||||||
|
rotation_sc = has_rotation ? pack_rotation_sc(sin_angle, cos_angle) : 0,
|
||||||
|
}
|
||||||
|
prim.params.ellipse = Ellipse_Params {
|
||||||
|
radii = {radius_horizontal * dpi_scale, radius_vertical * dpi_scale},
|
||||||
|
half_feather = half_feather,
|
||||||
|
}
|
||||||
|
return prim
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal
|
||||||
|
//
|
||||||
|
// Build an NGon Primitive with bounds, params, and rotation computed from polygon geometry.
|
||||||
|
// The caller sets color, flags, and uv fields on the returned primitive before submitting.
|
||||||
|
build_polygon_primitive :: proc(
|
||||||
|
center: Vec2,
|
||||||
|
sides: int,
|
||||||
|
radius: f32,
|
||||||
|
origin: Vec2,
|
||||||
|
rotation: f32,
|
||||||
|
feather_px: f32,
|
||||||
|
) -> Primitive {
|
||||||
|
half_feather := feather_px * 0.5
|
||||||
|
padding := half_feather / GLOB.dpi_scaling
|
||||||
|
dpi_scale := GLOB.dpi_scaling
|
||||||
|
|
||||||
|
actual_center := center
|
||||||
|
if origin != {0, 0} && rotation != 0 {
|
||||||
|
sin_a, cos_a := math.sincos(math.to_radians(rotation))
|
||||||
|
actual_center = compute_pivot_center(center, origin, sin_a, cos_a)
|
||||||
|
}
|
||||||
|
|
||||||
|
rotation_radians := math.to_radians(rotation)
|
||||||
|
sin_rot, cos_rot := math.sincos(rotation_radians)
|
||||||
|
|
||||||
|
prim := Primitive {
|
||||||
|
bounds = {
|
||||||
|
actual_center.x - radius - padding,
|
||||||
|
actual_center.y - radius - padding,
|
||||||
|
actual_center.x + radius + padding,
|
||||||
|
actual_center.y + radius + padding,
|
||||||
|
},
|
||||||
|
rotation_sc = rotation != 0 ? pack_rotation_sc(sin_rot, cos_rot) : 0,
|
||||||
|
}
|
||||||
|
prim.params.ngon = NGon_Params {
|
||||||
|
radius = radius * math.cos(math.PI / f32(sides)) * dpi_scale,
|
||||||
|
sides = f32(sides),
|
||||||
|
half_feather = half_feather,
|
||||||
|
}
|
||||||
|
return prim
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal
|
||||||
|
//
|
||||||
|
// Build a Ring_Arc Primitive with bounds and params computed from ring/arc geometry.
|
||||||
|
// Pre-computes the angular boundary normals on the CPU so the fragment shader needs
|
||||||
|
// no per-pixel sin/cos. The radial SDF uses max(inner-r, r-outer) which correctly
|
||||||
|
// handles pie slices (inner_radius = 0) and full rings.
|
||||||
|
// The caller sets color, flags, and uv fields on the returned primitive before submitting.
|
||||||
|
build_ring_arc_primitive :: proc(
|
||||||
|
center: Vec2,
|
||||||
|
inner_radius, outer_radius: f32,
|
||||||
|
start_angle: f32,
|
||||||
|
end_angle: f32,
|
||||||
|
origin: Vec2,
|
||||||
|
rotation: f32,
|
||||||
|
feather_px: f32,
|
||||||
|
) -> (
|
||||||
|
Primitive,
|
||||||
|
Shape_Flags,
|
||||||
|
) {
|
||||||
|
half_feather := feather_px * 0.5
|
||||||
|
padding := half_feather / GLOB.dpi_scaling
|
||||||
|
dpi_scale := GLOB.dpi_scaling
|
||||||
|
|
||||||
|
actual_center := center
|
||||||
|
rotation_offset: f32 = 0
|
||||||
|
if needs_transform(origin, rotation) {
|
||||||
|
sin_a, cos_a := math.sincos(math.to_radians(rotation))
|
||||||
|
actual_center = compute_pivot_center(center, origin, sin_a, cos_a)
|
||||||
|
rotation_offset = math.to_radians(rotation)
|
||||||
|
}
|
||||||
|
|
||||||
|
start_rad := math.to_radians(start_angle) + rotation_offset
|
||||||
|
end_rad := math.to_radians(end_angle) + rotation_offset
|
||||||
|
|
||||||
|
// Normalize arc span to [0, 2π]
|
||||||
|
arc_span := end_rad - start_rad
|
||||||
|
if arc_span < 0 {
|
||||||
|
arc_span += 2 * math.PI
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-compute edge normals and arc flags on CPU — no per-pixel trig needed.
|
||||||
|
// arc_flags: {} = full ring, {.Arc_Narrow} = span ≤ π (intersect), {.Arc_Wide} = span > π (union)
|
||||||
|
arc_flags: Shape_Flags = {}
|
||||||
|
normal_start: [2]f32 = {}
|
||||||
|
normal_end: [2]f32 = {}
|
||||||
|
|
||||||
|
if arc_span < 2 * math.PI - 0.001 {
|
||||||
|
sin_start, cos_start := math.sincos(start_rad)
|
||||||
|
sin_end, cos_end := math.sincos(end_rad)
|
||||||
|
normal_start = {sin_start, -cos_start}
|
||||||
|
normal_end = {-sin_end, cos_end}
|
||||||
|
arc_flags = arc_span <= math.PI ? {.Arc_Narrow} : {.Arc_Wide}
|
||||||
|
}
|
||||||
|
|
||||||
|
prim := Primitive {
|
||||||
|
bounds = {
|
||||||
|
actual_center.x - outer_radius - padding,
|
||||||
|
actual_center.y - outer_radius - padding,
|
||||||
|
actual_center.x + outer_radius + padding,
|
||||||
|
actual_center.y + outer_radius + padding,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
prim.params.ring_arc = Ring_Arc_Params {
|
||||||
|
inner_radius = inner_radius * dpi_scale,
|
||||||
|
outer_radius = outer_radius * dpi_scale,
|
||||||
|
normal_start = normal_start,
|
||||||
|
normal_end = normal_end,
|
||||||
|
half_feather = half_feather,
|
||||||
|
}
|
||||||
|
return prim, arc_flags
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply gradient and outline effects to a primitive. Sets flags, uv.effects, and expands bounds.
|
||||||
|
// All parameters (outline_width) are in logical pixels, matching the rest of the public API.
|
||||||
|
// The helper converts to physical pixels for GPU packing internally.
|
||||||
|
@(private)
|
||||||
|
apply_shape_effects :: proc(
|
||||||
|
prim: ^Primitive,
|
||||||
|
kind: Shape_Kind,
|
||||||
|
gradient: Gradient,
|
||||||
|
outline_color: Color,
|
||||||
|
outline_width: f32,
|
||||||
|
extra_flags: Shape_Flags = {},
|
||||||
|
) {
|
||||||
|
flags: Shape_Flags = extra_flags
|
||||||
|
gradient_dir_sc: u32 = 0
|
||||||
|
|
||||||
|
switch g in gradient {
|
||||||
|
case Linear_Gradient:
|
||||||
|
flags += {.Gradient}
|
||||||
|
prim.uv.effects.gradient_color = g.end_color
|
||||||
|
rad := math.to_radians(g.angle)
|
||||||
|
sin_a, cos_a := math.sincos(rad)
|
||||||
|
gradient_dir_sc = pack_f16_pair(f16(cos_a), f16(sin_a))
|
||||||
|
case Radial_Gradient:
|
||||||
|
flags += {.Gradient, .Gradient_Radial}
|
||||||
|
prim.uv.effects.gradient_color = g.outer_color
|
||||||
|
case:
|
||||||
|
}
|
||||||
|
|
||||||
|
outline_packed: u32 = 0
|
||||||
|
if outline_width > 0 {
|
||||||
|
flags += {.Outline}
|
||||||
|
prim.uv.effects.outline_color = outline_color
|
||||||
|
outline_packed = pack_f16_pair(f16(outline_width * GLOB.dpi_scaling), 0)
|
||||||
|
// Expand bounds to contain the outline (bounds are in logical pixels)
|
||||||
|
prim.bounds[0] -= outline_width
|
||||||
|
prim.bounds[1] -= outline_width
|
||||||
|
prim.bounds[2] += outline_width
|
||||||
|
prim.bounds[3] += outline_width
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set .Rotated flag if rotation_sc was populated by the build proc
|
||||||
|
if prim.rotation_sc != 0 {
|
||||||
|
flags += {.Rotated}
|
||||||
|
}
|
||||||
|
|
||||||
|
prim.uv.effects.gradient_dir_sc = gradient_dir_sc
|
||||||
|
prim.uv.effects.outline_packed = outline_packed
|
||||||
|
prim.flags = pack_kind_flags(kind, flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
// ----- SDF Rectangle procs -----------
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Draw a filled rectangle via SDF with optional per-corner rounding radii.
|
||||||
|
// Use `uniform_radii(rect, roundness)` to compute uniform radii from a 0–1 fraction.
|
||||||
|
//
|
||||||
|
// Origin semantics:
|
||||||
|
// `origin` is a local offset from the rect's top-left corner that selects both the positioning
|
||||||
|
// anchor and the rotation pivot. `rect.x, rect.y` specifies where that anchor point lands in
|
||||||
|
// world space. When `origin = {0, 0}` (default), `rect.x, rect.y` is the top-left corner.
|
||||||
|
// Rotation always occurs around the anchor point.
|
||||||
|
rectangle :: proc(
|
||||||
|
layer: ^Layer,
|
||||||
|
rect: Rectangle,
|
||||||
|
color: Color,
|
||||||
|
gradient: Gradient = nil,
|
||||||
|
outline_color: Color = {},
|
||||||
|
outline_width: f32 = 0,
|
||||||
|
radii: Rectangle_Radii = {},
|
||||||
|
origin: Vec2 = {},
|
||||||
|
rotation: f32 = 0,
|
||||||
|
feather_px: f32 = DFT_FEATHER_PX,
|
||||||
|
) {
|
||||||
|
prim := build_rrect_primitive(rect, radii, origin, rotation, feather_px)
|
||||||
|
prim.color = color
|
||||||
|
apply_shape_effects(&prim, .RRect, gradient, outline_color, outline_width)
|
||||||
|
prepare_sdf_primitive(layer, prim)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw a rectangle with a texture fill via SDF with optional per-corner rounding radii.
|
||||||
|
// Texture and gradient/outline are mutually exclusive (they share the same storage in the
|
||||||
|
// primitive). To outline a textured rect, draw the texture first, then a stroke-only rect on top.
|
||||||
|
// Origin semantics: see `rectangle`.
|
||||||
|
rectangle_texture :: proc(
|
||||||
|
layer: ^Layer,
|
||||||
|
rect: Rectangle,
|
||||||
|
id: Texture_Id,
|
||||||
|
tint: Color = DFT_TINT,
|
||||||
|
uv_rect: Rectangle = DFT_UV_RECT,
|
||||||
|
sampler: Sampler_Preset = DFT_SAMPLER,
|
||||||
|
radii: Rectangle_Radii = {},
|
||||||
|
origin: Vec2 = {},
|
||||||
|
rotation: f32 = 0,
|
||||||
|
feather_px: f32 = DFT_FEATHER_PX,
|
||||||
|
) {
|
||||||
|
prim := build_rrect_primitive(rect, radii, origin, rotation, feather_px)
|
||||||
|
prim.color = tint
|
||||||
|
tex_flags: Shape_Flags = {.Textured}
|
||||||
|
if prim.rotation_sc != 0 {
|
||||||
|
tex_flags += {.Rotated}
|
||||||
|
}
|
||||||
|
prim.flags = pack_kind_flags(.RRect, tex_flags)
|
||||||
|
prim.uv.uv_rect = {uv_rect.x, uv_rect.y, uv_rect.width, uv_rect.height}
|
||||||
|
prepare_sdf_primitive_textured(layer, prim, id, sampler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
// ----- SDF Circle procs (emit RRect primitives) ------
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Draw a filled circle via SDF (emitted as a fully-rounded RRect).
|
||||||
|
//
|
||||||
|
// Origin semantics (Convention B):
|
||||||
|
// `origin` is a local offset from the shape's center that selects both the positioning anchor
|
||||||
|
// and the rotation pivot. The `center` parameter specifies where that anchor point lands in
|
||||||
|
// world space. When `origin = {0, 0}` (default), `center` is the visual center.
|
||||||
|
// When `origin = {r, 0}`, the point `r` pixels to the right of the shape center lands at
|
||||||
|
// `center`, shifting the shape left by `r`.
|
||||||
|
circle :: proc(
|
||||||
|
layer: ^Layer,
|
||||||
|
center: Vec2,
|
||||||
|
radius: f32,
|
||||||
|
color: Color,
|
||||||
|
gradient: Gradient = nil,
|
||||||
|
outline_color: Color = {},
|
||||||
|
outline_width: f32 = 0,
|
||||||
|
origin: Vec2 = {},
|
||||||
|
rotation: f32 = 0,
|
||||||
|
feather_px: f32 = DFT_FEATHER_PX,
|
||||||
|
) {
|
||||||
|
prim := build_circle_primitive(center, radius, origin, rotation, feather_px)
|
||||||
|
prim.color = color
|
||||||
|
apply_shape_effects(&prim, .RRect, gradient, outline_color, outline_width)
|
||||||
|
prepare_sdf_primitive(layer, prim)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
// ----- SDF Ellipse procs (emit Ellipse primitives) ---
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Draw a filled ellipse via SDF.
|
||||||
|
// Origin semantics: see `circle`.
|
||||||
|
ellipse :: proc(
|
||||||
|
layer: ^Layer,
|
||||||
|
center: Vec2,
|
||||||
|
radius_horizontal, radius_vertical: f32,
|
||||||
|
color: Color,
|
||||||
|
gradient: Gradient = nil,
|
||||||
|
outline_color: Color = {},
|
||||||
|
outline_width: f32 = 0,
|
||||||
|
origin: Vec2 = {},
|
||||||
|
rotation: f32 = 0,
|
||||||
|
feather_px: f32 = DFT_FEATHER_PX,
|
||||||
|
) {
|
||||||
|
prim := build_ellipse_primitive(center, radius_horizontal, radius_vertical, origin, rotation, feather_px)
|
||||||
|
prim.color = color
|
||||||
|
apply_shape_effects(&prim, .Ellipse, gradient, outline_color, outline_width)
|
||||||
|
prepare_sdf_primitive(layer, prim)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
// ----- SDF Polygon procs (emit NGon primitives) ------
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Draw a filled regular polygon via SDF.
|
||||||
|
// `sides` must be >= 3. The polygon is inscribed in a circle of the given `radius`.
|
||||||
|
// Origin semantics: see `circle`.
|
||||||
|
polygon :: proc(
|
||||||
|
layer: ^Layer,
|
||||||
|
center: Vec2,
|
||||||
|
sides: int,
|
||||||
|
radius: f32,
|
||||||
|
color: Color,
|
||||||
|
gradient: Gradient = nil,
|
||||||
|
outline_color: Color = {},
|
||||||
|
outline_width: f32 = 0,
|
||||||
|
origin: Vec2 = {},
|
||||||
|
rotation: f32 = 0,
|
||||||
|
feather_px: f32 = DFT_FEATHER_PX,
|
||||||
|
) {
|
||||||
|
if sides < 3 do return
|
||||||
|
|
||||||
|
prim := build_polygon_primitive(center, sides, radius, origin, rotation, feather_px)
|
||||||
|
prim.color = color
|
||||||
|
apply_shape_effects(&prim, .NGon, gradient, outline_color, outline_width)
|
||||||
|
prepare_sdf_primitive(layer, prim)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
// ----- SDF Ring / Arc procs (emit Ring_Arc primitives) ----
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Draw a ring, arc, or pie slice via SDF.
|
||||||
|
// Full ring by default. Pass start_angle/end_angle (degrees) for partial arcs.
|
||||||
|
// Use inner_radius = 0 for pie slices (sectors).
|
||||||
|
// Origin semantics: see `circle`.
|
||||||
|
ring :: proc(
|
||||||
|
layer: ^Layer,
|
||||||
|
center: Vec2,
|
||||||
|
inner_radius, outer_radius: f32,
|
||||||
|
color: Color,
|
||||||
|
gradient: Gradient = nil,
|
||||||
|
outline_color: Color = {},
|
||||||
|
outline_width: f32 = 0,
|
||||||
|
start_angle: f32 = 0,
|
||||||
|
end_angle: f32 = DFT_CIRC_END_ANGLE,
|
||||||
|
origin: Vec2 = {},
|
||||||
|
rotation: f32 = 0,
|
||||||
|
feather_px: f32 = DFT_FEATHER_PX,
|
||||||
|
) {
|
||||||
|
prim, arc_flags := build_ring_arc_primitive(
|
||||||
|
center,
|
||||||
|
inner_radius,
|
||||||
|
outer_radius,
|
||||||
|
start_angle,
|
||||||
|
end_angle,
|
||||||
|
origin,
|
||||||
|
rotation,
|
||||||
|
feather_px,
|
||||||
|
)
|
||||||
|
prim.color = color
|
||||||
|
apply_shape_effects(&prim, .Ring_Arc, gradient, outline_color, outline_width, arc_flags)
|
||||||
|
prepare_sdf_primitive(layer, prim)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
// ----- SDF Line procs (emit rotated RRect primitives) ----
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Draw a line segment via SDF (emitted as a rotated capsule-shaped RRect).
|
||||||
|
// Round caps are produced by setting corner radii equal to half the thickness.
|
||||||
|
line :: proc(
|
||||||
|
layer: ^Layer,
|
||||||
|
start_position, end_position: Vec2,
|
||||||
|
color: Color,
|
||||||
|
thickness: f32 = DFT_STROKE_THICKNESS,
|
||||||
|
outline_color: Color = {},
|
||||||
|
outline_width: f32 = 0,
|
||||||
|
feather_px: f32 = DFT_FEATHER_PX,
|
||||||
|
) {
|
||||||
|
delta_x := end_position.x - start_position.x
|
||||||
|
delta_y := end_position.y - start_position.y
|
||||||
|
seg_length := math.sqrt(delta_x * delta_x + delta_y * delta_y)
|
||||||
|
if seg_length < 0.0001 do return
|
||||||
|
rotation_radians := math.atan2(delta_y, delta_x)
|
||||||
|
sin_angle, cos_angle := math.sincos(rotation_radians)
|
||||||
|
|
||||||
|
center_x := (start_position.x + end_position.x) * 0.5
|
||||||
|
center_y := (start_position.y + end_position.y) * 0.5
|
||||||
|
|
||||||
|
half_length := seg_length * 0.5
|
||||||
|
half_thickness := thickness * 0.5
|
||||||
|
cap_radius := half_thickness
|
||||||
|
|
||||||
|
half_feather := feather_px * 0.5
|
||||||
|
padding := half_feather / GLOB.dpi_scaling
|
||||||
|
dpi_scale := GLOB.dpi_scaling
|
||||||
|
|
||||||
|
// Expand bounds for rotation
|
||||||
|
bounds_half := rotated_aabb_half_extents(half_length + cap_radius, half_thickness, cos_angle, sin_angle)
|
||||||
|
|
||||||
|
prim := Primitive {
|
||||||
|
bounds = {
|
||||||
|
center_x - bounds_half.x - padding,
|
||||||
|
center_y - bounds_half.y - padding,
|
||||||
|
center_x + bounds_half.x + padding,
|
||||||
|
center_y + bounds_half.y + padding,
|
||||||
|
},
|
||||||
|
color = color,
|
||||||
|
rotation_sc = pack_rotation_sc(sin_angle, cos_angle),
|
||||||
|
}
|
||||||
|
prim.params.rrect = RRect_Params {
|
||||||
|
half_size = {(half_length + cap_radius) * dpi_scale, half_thickness * dpi_scale},
|
||||||
|
radii = {
|
||||||
|
cap_radius * dpi_scale,
|
||||||
|
cap_radius * dpi_scale,
|
||||||
|
cap_radius * dpi_scale,
|
||||||
|
cap_radius * dpi_scale,
|
||||||
|
},
|
||||||
|
half_feather = half_feather,
|
||||||
|
}
|
||||||
|
apply_shape_effects(&prim, .RRect, nil, outline_color, outline_width)
|
||||||
|
prepare_sdf_primitive(layer, prim)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw a line strip via decomposed SDF line segments.
|
||||||
|
line_strip :: proc(
|
||||||
|
layer: ^Layer,
|
||||||
|
points: []Vec2,
|
||||||
|
color: Color,
|
||||||
|
thickness: f32 = DFT_STROKE_THICKNESS,
|
||||||
|
outline_color: Color = {},
|
||||||
|
outline_width: f32 = 0,
|
||||||
|
feather_px: f32 = DFT_FEATHER_PX,
|
||||||
|
) {
|
||||||
|
if len(points) < 2 do return
|
||||||
|
for i in 0 ..< len(points) - 1 {
|
||||||
|
line(layer, points[i], points[i + 1], color, thickness, outline_color, outline_width, feather_px)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
// ----- Helpers ----------------
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Returns uniform radii (all corners the same) as a fraction of the shorter side.
|
||||||
|
// `roundness` is clamped to [0, 1]; 0 = sharp corners, 1 = fully rounded (stadium or circle).
|
||||||
|
uniform_radii :: #force_inline proc(rect: Rectangle, roundness: f32) -> Rectangle_Radii {
|
||||||
|
cr := min(rect.width, rect.height) * clamp(roundness, 0, 1) * 0.5
|
||||||
|
return {cr, cr, cr, cr}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return Vec2 pixel offsets for use as the `origin` parameter of draw calls.
|
||||||
|
// Composable with normal vector +/- arithmetic.
|
||||||
|
//
|
||||||
|
// Text anchor helpers are in text.odin (they depend on measure_text / SDL_ttf).
|
||||||
|
|
||||||
|
// ----- Rectangle anchors (origin measured from rectangle's top-left) ---------------------------------------------
|
||||||
|
|
||||||
|
center_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
|
||||||
|
return {rectangle.width * 0.5, rectangle.height * 0.5}
|
||||||
|
}
|
||||||
|
|
||||||
|
top_left_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
|
||||||
|
return {0, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
top_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
|
||||||
|
return {rectangle.width * 0.5, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
top_right_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
|
||||||
|
return {rectangle.width, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
left_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
|
||||||
|
return {0, rectangle.height * 0.5}
|
||||||
|
}
|
||||||
|
|
||||||
|
right_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
|
||||||
|
return {rectangle.width, rectangle.height * 0.5}
|
||||||
|
}
|
||||||
|
|
||||||
|
bottom_left_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
|
||||||
|
return {0, rectangle.height}
|
||||||
|
}
|
||||||
|
|
||||||
|
bottom_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
|
||||||
|
return {rectangle.width * 0.5, rectangle.height}
|
||||||
|
}
|
||||||
|
|
||||||
|
bottom_right_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
|
||||||
|
return {rectangle.width, rectangle.height}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Triangle anchors (origin measured from AABB top-left) -----------------------------------------------------
|
||||||
|
|
||||||
|
center_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
|
||||||
|
bounds_min := Vec2{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
|
||||||
|
return (v1 + v2 + v3) / 3 - bounds_min
|
||||||
|
}
|
||||||
|
|
||||||
|
top_left_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
|
||||||
|
return {0, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
top_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
|
||||||
|
min_x := min(v1.x, v2.x, v3.x)
|
||||||
|
max_x := max(v1.x, v2.x, v3.x)
|
||||||
|
return {(max_x - min_x) * 0.5, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
top_right_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
|
||||||
|
min_x := min(v1.x, v2.x, v3.x)
|
||||||
|
max_x := max(v1.x, v2.x, v3.x)
|
||||||
|
return {max_x - min_x, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
left_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
|
||||||
|
min_y := min(v1.y, v2.y, v3.y)
|
||||||
|
max_y := max(v1.y, v2.y, v3.y)
|
||||||
|
return {0, (max_y - min_y) * 0.5}
|
||||||
|
}
|
||||||
|
|
||||||
|
right_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
|
||||||
|
bounds_min := Vec2{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
|
||||||
|
bounds_max := Vec2{max(v1.x, v2.x, v3.x), max(v1.y, v2.y, v3.y)}
|
||||||
|
return {bounds_max.x - bounds_min.x, (bounds_max.y - bounds_min.y) * 0.5}
|
||||||
|
}
|
||||||
|
|
||||||
|
bottom_left_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
|
||||||
|
min_y := min(v1.y, v2.y, v3.y)
|
||||||
|
max_y := max(v1.y, v2.y, v3.y)
|
||||||
|
return {0, max_y - min_y}
|
||||||
|
}
|
||||||
|
|
||||||
|
bottom_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
|
||||||
|
bounds_min := Vec2{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
|
||||||
|
bounds_max := Vec2{max(v1.x, v2.x, v3.x), max(v1.y, v2.y, v3.y)}
|
||||||
|
return {(bounds_max.x - bounds_min.x) * 0.5, bounds_max.y - bounds_min.y}
|
||||||
|
}
|
||||||
|
|
||||||
|
bottom_right_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
|
||||||
|
bounds_min := Vec2{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
|
||||||
|
bounds_max := Vec2{max(v1.x, v2.x, v3.x), max(v1.y, v2.y, v3.y)}
|
||||||
|
return bounds_max - bounds_min
|
||||||
|
}
|
||||||
+20
-59
@@ -4,7 +4,6 @@ import "core:math"
|
|||||||
|
|
||||||
import draw ".."
|
import draw ".."
|
||||||
|
|
||||||
//INTERNAL
|
|
||||||
SMOOTH_CIRCLE_ERROR_RATE :: 0.1
|
SMOOTH_CIRCLE_ERROR_RATE :: 0.1
|
||||||
|
|
||||||
auto_segments :: proc(radius: f32, arc_degrees: f32) -> int {
|
auto_segments :: proc(radius: f32, arc_degrees: f32) -> int {
|
||||||
@@ -21,20 +20,13 @@ auto_segments :: proc(radius: f32, arc_degrees: f32) -> int {
|
|||||||
|
|
||||||
// ----- Internal helpers -----
|
// ----- Internal helpers -----
|
||||||
|
|
||||||
// Premultiplies the color before storing it on the vertex (see draw package doc's
|
// Color is premultiplied: the tessellated fragment shader passes it through directly
|
||||||
// "Color and blending" section for why).
|
// and the blend state is ONE, ONE_MINUS_SRC_ALPHA.
|
||||||
//INTERNAL
|
solid_vertex :: proc(position: draw.Vec2, color: draw.Color) -> draw.Vertex {
|
||||||
solid_vertex :: proc(position: draw.Vec2, color: draw.Color) -> draw.Vertex_2D {
|
return draw.Vertex{position = position, color = draw.premultiply_color(color)}
|
||||||
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, offset: int) {
|
||||||
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 + 0] = solid_vertex({x, y}, color)
|
||||||
vertices[offset + 1] = solid_vertex({x + width, y}, color)
|
vertices[offset + 1] = solid_vertex({x + width, y}, color)
|
||||||
vertices[offset + 2] = solid_vertex({x + width, y + height}, color)
|
vertices[offset + 2] = solid_vertex({x + width, y + height}, color)
|
||||||
@@ -43,12 +35,11 @@ emit_rectangle :: proc(
|
|||||||
vertices[offset + 5] = solid_vertex({x, y + height}, color)
|
vertices[offset + 5] = solid_vertex({x, y + height}, color)
|
||||||
}
|
}
|
||||||
|
|
||||||
//INTERNAL
|
|
||||||
extrude_line :: proc(
|
extrude_line :: proc(
|
||||||
start, end_pos: draw.Vec2,
|
start, end_pos: draw.Vec2,
|
||||||
thickness: f32,
|
thickness: f32,
|
||||||
color: draw.Color,
|
color: draw.Color,
|
||||||
vertices: []draw.Vertex_2D,
|
vertices: []draw.Vertex,
|
||||||
offset: int,
|
offset: int,
|
||||||
) -> int {
|
) -> int {
|
||||||
direction := end_pos - start
|
direction := end_pos - start
|
||||||
@@ -78,7 +69,7 @@ extrude_line :: proc(
|
|||||||
// ----- Public draw -----
|
// ----- Public draw -----
|
||||||
|
|
||||||
pixel :: proc(layer: ^draw.Layer, pos: draw.Vec2, color: draw.Color) {
|
pixel :: proc(layer: ^draw.Layer, pos: draw.Vec2, color: draw.Color) {
|
||||||
vertices: [6]draw.Vertex_2D
|
vertices: [6]draw.Vertex
|
||||||
emit_rectangle(pos[0], pos[1], 1, 1, color, vertices[:], 0)
|
emit_rectangle(pos[0], pos[1], 1, 1, color, vertices[:], 0)
|
||||||
draw.prepare_shape(layer, vertices[:])
|
draw.prepare_shape(layer, vertices[:])
|
||||||
}
|
}
|
||||||
@@ -91,7 +82,7 @@ triangle :: proc(
|
|||||||
rotation: f32 = 0,
|
rotation: f32 = 0,
|
||||||
) {
|
) {
|
||||||
if !draw.needs_transform(origin, rotation) {
|
if !draw.needs_transform(origin, rotation) {
|
||||||
vertices := [3]draw.Vertex_2D{solid_vertex(v1, color), solid_vertex(v2, color), solid_vertex(v3, color)}
|
vertices := [3]draw.Vertex{solid_vertex(v1, color), solid_vertex(v2, color), solid_vertex(v3, color)}
|
||||||
draw.prepare_shape(layer, vertices[:])
|
draw.prepare_shape(layer, vertices[:])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -100,7 +91,7 @@ triangle :: proc(
|
|||||||
local_v1 := v1 - bounds_min
|
local_v1 := v1 - bounds_min
|
||||||
local_v2 := v2 - bounds_min
|
local_v2 := v2 - bounds_min
|
||||||
local_v3 := v3 - bounds_min
|
local_v3 := v3 - bounds_min
|
||||||
vertices := [3]draw.Vertex_2D {
|
vertices := [3]draw.Vertex {
|
||||||
solid_vertex(draw.apply_transform(transform, local_v1), color),
|
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_v2), color),
|
||||||
solid_vertex(draw.apply_transform(transform, local_v3), color),
|
solid_vertex(draw.apply_transform(transform, local_v3), color),
|
||||||
@@ -108,23 +99,16 @@ triangle :: proc(
|
|||||||
draw.prepare_shape(layer, vertices[:])
|
draw.prepare_shape(layer, vertices[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw an anti-aliased triangle via extruded edge quads plus corner fan caps.
|
// 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).
|
// Interior vertices get the full premultiplied color; outer fringe vertices get BLANK (0,0,0,0).
|
||||||
// The rasterizer linearly interpolates between them, producing a smooth ~1-physical-pixel AA band.
|
// The rasterizer linearly interpolates between them, producing a smooth 1-pixel AA band.
|
||||||
// `aa_ppx` controls the extrusion width in *physical* pixels (default 1.0). The CPU divides by
|
// `aa_px` controls the extrusion width in logical pixels (default 1.0).
|
||||||
// `dpi_scaling` here so the vertex stream stays in logical px; the mode-0 vertex shader scales
|
// This proc emits 21 vertices (3 interior + 6 edge quads × 3 verts each).
|
||||||
// back to physical at draw time. Net AA band is ~aa_ppx physical pixels regardless of DPI.
|
|
||||||
//
|
|
||||||
// Topology: 3 interior verts + 6 edge-quad triangles (×3 verts) + 3 corner-fan triangles (×3 verts)
|
|
||||||
// = 30 verts total. The corner fans plug the wedge gaps that would otherwise appear between
|
|
||||||
// adjacent edge fringes at each triangle vertex; without them, sharp corners show a small
|
|
||||||
// background-colored crescent. Apex vertex is full color, both fringe verts are BLANK, so the
|
|
||||||
// fan rasterizes as an alpha-falloff triangle that blends visually into the adjacent edge bands.
|
|
||||||
triangle_aa :: proc(
|
triangle_aa :: proc(
|
||||||
layer: ^draw.Layer,
|
layer: ^draw.Layer,
|
||||||
v1, v2, v3: draw.Vec2,
|
v1, v2, v3: draw.Vec2,
|
||||||
color: draw.Color,
|
color: draw.Color,
|
||||||
aa_ppx: f32 = draw.DFT_FEATHER_PPX,
|
aa_px: f32 = draw.DFT_FEATHER_PX,
|
||||||
origin: draw.Vec2 = {},
|
origin: draw.Vec2 = {},
|
||||||
rotation: f32 = 0,
|
rotation: f32 = 0,
|
||||||
) {
|
) {
|
||||||
@@ -171,9 +155,7 @@ triangle_aa :: proc(
|
|||||||
normal_12 := edge_normal(p1, p2, centroid_x, centroid_y)
|
normal_12 := edge_normal(p1, p2, centroid_x, centroid_y)
|
||||||
normal_20 := edge_normal(p2, p0, centroid_x, centroid_y)
|
normal_20 := edge_normal(p2, p0, centroid_x, centroid_y)
|
||||||
|
|
||||||
// aa_ppx is in physical pixels; divide by dpi_scaling so the extrusion lives in logical-pixel
|
extrude_distance := aa_px * draw.GLOB.dpi_scaling
|
||||||
// space (the mode-0 vertex shader will scale back to physical at draw time).
|
|
||||||
extrude_distance := aa_ppx / draw.GLOB.dpi_scaling
|
|
||||||
|
|
||||||
// Outer fringe vertices: each edge vertex extruded outward
|
// Outer fringe vertices: each edge vertex extruded outward
|
||||||
outer_0_01 := p0 + normal_01 * extrude_distance
|
outer_0_01 := p0 + normal_01 * extrude_distance
|
||||||
@@ -187,8 +169,8 @@ triangle_aa :: proc(
|
|||||||
// Outer fringe is BLANK = {0,0,0,0} which is already premul.
|
// Outer fringe is BLANK = {0,0,0,0} which is already premul.
|
||||||
transparent := draw.BLANK
|
transparent := draw.BLANK
|
||||||
|
|
||||||
// 3 interior + 6 edge-quad tris (×3 verts) + 3 corner-fan tris (×3 verts) = 30 vertices
|
// 3 interior + 6 × 3 edge-quad = 21 vertices
|
||||||
vertices: [30]draw.Vertex_2D
|
vertices: [21]draw.Vertex
|
||||||
|
|
||||||
// Interior triangle
|
// Interior triangle
|
||||||
vertices[0] = solid_vertex(p0, color)
|
vertices[0] = solid_vertex(p0, color)
|
||||||
@@ -219,27 +201,6 @@ triangle_aa :: proc(
|
|||||||
vertices[19] = solid_vertex(outer_0_20, transparent)
|
vertices[19] = solid_vertex(outer_0_20, transparent)
|
||||||
vertices[20] = solid_vertex(outer_2_20, transparent)
|
vertices[20] = solid_vertex(outer_2_20, transparent)
|
||||||
|
|
||||||
// Corner fan caps: each fills the wedge gap between the two edge fringes meeting at a
|
|
||||||
// triangle vertex. Apex is full color; both fringe verts are BLANK, so the rasterizer
|
|
||||||
// produces a smooth alpha falloff across the wedge (matches the adjacent edge-band
|
|
||||||
// gradients at the shared edges, so the seams are invisible). Vertex order per fan:
|
|
||||||
// [apex, fringe-from-incoming-edge, fringe-from-outgoing-edge].
|
|
||||||
|
|
||||||
// Cap at p0 (between incoming edge p2→p0 and outgoing edge p0→p1)
|
|
||||||
vertices[21] = solid_vertex(p0, color)
|
|
||||||
vertices[22] = solid_vertex(outer_0_20, transparent)
|
|
||||||
vertices[23] = solid_vertex(outer_0_01, transparent)
|
|
||||||
|
|
||||||
// Cap at p1 (between incoming edge p0→p1 and outgoing edge p1→p2)
|
|
||||||
vertices[24] = solid_vertex(p1, color)
|
|
||||||
vertices[25] = solid_vertex(outer_1_01, transparent)
|
|
||||||
vertices[26] = solid_vertex(outer_1_12, transparent)
|
|
||||||
|
|
||||||
// Cap at p2 (between incoming edge p1→p2 and outgoing edge p2→p0)
|
|
||||||
vertices[27] = solid_vertex(p2, color)
|
|
||||||
vertices[28] = solid_vertex(outer_2_12, transparent)
|
|
||||||
vertices[29] = solid_vertex(outer_2_20, transparent)
|
|
||||||
|
|
||||||
draw.prepare_shape(layer, vertices[:])
|
draw.prepare_shape(layer, vertices[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,7 +213,7 @@ triangle_lines :: proc(
|
|||||||
rotation: f32 = 0,
|
rotation: f32 = 0,
|
||||||
temp_allocator := context.temp_allocator,
|
temp_allocator := context.temp_allocator,
|
||||||
) {
|
) {
|
||||||
vertices := make([]draw.Vertex_2D, 18, temp_allocator)
|
vertices := make([]draw.Vertex, 18, temp_allocator)
|
||||||
defer delete(vertices, temp_allocator)
|
defer delete(vertices, temp_allocator)
|
||||||
write_offset := 0
|
write_offset := 0
|
||||||
|
|
||||||
@@ -288,7 +249,7 @@ triangle_fan :: proc(
|
|||||||
|
|
||||||
triangle_count := len(points) - 2
|
triangle_count := len(points) - 2
|
||||||
vertex_count := triangle_count * 3
|
vertex_count := triangle_count * 3
|
||||||
vertices := make([]draw.Vertex_2D, vertex_count, temp_allocator)
|
vertices := make([]draw.Vertex, vertex_count, temp_allocator)
|
||||||
defer delete(vertices, temp_allocator)
|
defer delete(vertices, temp_allocator)
|
||||||
|
|
||||||
if !draw.needs_transform(origin, rotation) {
|
if !draw.needs_transform(origin, rotation) {
|
||||||
@@ -328,7 +289,7 @@ triangle_strip :: proc(
|
|||||||
|
|
||||||
triangle_count := len(points) - 2
|
triangle_count := len(points) - 2
|
||||||
vertex_count := triangle_count * 3
|
vertex_count := triangle_count * 3
|
||||||
vertices := make([]draw.Vertex_2D, vertex_count, temp_allocator)
|
vertices := make([]draw.Vertex, vertex_count, temp_allocator)
|
||||||
defer delete(vertices, temp_allocator)
|
defer delete(vertices, temp_allocator)
|
||||||
|
|
||||||
if !draw.needs_transform(origin, rotation) {
|
if !draw.needs_transform(origin, rotation) {
|
||||||
|
|||||||
+3
-11
@@ -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,7 +77,6 @@ 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: Vec2,
|
||||||
@@ -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 {
|
||||||
@@ -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)
|
||||||
|
|||||||
+23
-12
@@ -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,
|
||||||
@@ -56,6 +57,16 @@ Texture_Slot :: struct {
|
|||||||
// GLOB.pending_texture_releases : [dynamic]Texture_Id
|
// GLOB.pending_texture_releases : [dynamic]Texture_Id
|
||||||
// GLOB.samplers : [SAMPLER_PRESET_COUNT]^sdl.GPUSampler
|
// GLOB.samplers : [SAMPLER_PRESET_COUNT]^sdl.GPUSampler
|
||||||
|
|
||||||
|
Clay_Image_Data :: struct {
|
||||||
|
texture_id: Texture_Id,
|
||||||
|
fit: Fit_Mode,
|
||||||
|
tint: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
clay_image_data :: proc(id: Texture_Id, fit: Fit_Mode = .Stretch, tint: Color = WHITE) -> Clay_Image_Data {
|
||||||
|
return {texture_id = id, fit = fit, tint = tint}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
// ----- Registration -------------
|
// ----- Registration -------------
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
@@ -308,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)
|
||||||
@@ -317,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 {
|
||||||
@@ -335,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]
|
||||||
@@ -368,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 {
|
||||||
@@ -387,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 {
|
||||||
|
|||||||
+57
-132
@@ -120,52 +120,10 @@ spinlock_try_lock :: #force_inline proc "contextless" (lock: ^Spinlock) -> bool
|
|||||||
return lock_acquired
|
return lock_acquired
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spins until the lock is acquired, relaxing the CPU between attempts.
|
|
||||||
spinlock_lock :: #force_inline proc "contextless" (lock: ^Spinlock) {
|
|
||||||
for !spinlock_try_lock(lock) {
|
|
||||||
intrinsics.cpu_relax()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
spinlock_unlock :: #force_inline proc "contextless" (lock: ^Spinlock) {
|
spinlock_unlock :: #force_inline proc "contextless" (lock: ^Spinlock) {
|
||||||
intrinsics.atomic_store_explicit(lock, false, .Release)
|
intrinsics.atomic_store_explicit(lock, false, .Release)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spins until the lock is acquired, then unlocks at the end of the calling scope. Always returns
|
|
||||||
// true so it can guard a critical section from within an `if`:
|
|
||||||
//
|
|
||||||
// if spinlock_guard(&lock) {
|
|
||||||
// // critical section
|
|
||||||
// }
|
|
||||||
@(deferred_in = spinlock_unlock)
|
|
||||||
spinlock_guard :: #force_inline proc "contextless" (lock: ^Spinlock) -> bool {
|
|
||||||
spinlock_lock(lock)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tries to acquire the lock once without spinning. Returns true and unlocks at the end of the
|
|
||||||
// calling scope if acquired, otherwise returns false and does nothing:
|
|
||||||
//
|
|
||||||
// if spinlock_try_guard(&lock) {
|
|
||||||
// // critical section, entered only if the lock was acquired
|
|
||||||
// }
|
|
||||||
@(deferred_in_out = spinlock_try_guard_unlock)
|
|
||||||
spinlock_try_guard :: #force_inline proc "contextless" (lock: ^Spinlock) -> bool {
|
|
||||||
return spinlock_try_lock(lock)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deferred companion of `spinlock_try_guard`; unlocks only when the lock was actually acquired.
|
|
||||||
@(private)
|
|
||||||
spinlock_try_guard_unlock :: #force_inline proc "contextless" (lock: ^Spinlock, locked: bool) {
|
|
||||||
if locked {
|
|
||||||
spinlock_unlock(lock)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lock :: proc {
|
|
||||||
spinlock_lock,
|
|
||||||
}
|
|
||||||
|
|
||||||
try_lock :: proc {
|
try_lock :: proc {
|
||||||
spinlock_try_lock,
|
spinlock_try_lock,
|
||||||
}
|
}
|
||||||
@@ -174,14 +132,6 @@ unlock :: proc {
|
|||||||
spinlock_unlock,
|
spinlock_unlock,
|
||||||
}
|
}
|
||||||
|
|
||||||
guard :: proc {
|
|
||||||
spinlock_guard,
|
|
||||||
}
|
|
||||||
|
|
||||||
try_guard :: proc {
|
|
||||||
spinlock_try_guard,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
// ----- Tests ------------------------
|
// ----- Tests ------------------------
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
@@ -189,10 +139,10 @@ import "core:sync"
|
|||||||
import "core:testing"
|
import "core:testing"
|
||||||
import "core:thread"
|
import "core:thread"
|
||||||
|
|
||||||
// Multiple threads will each add 1.0 this many times.
|
|
||||||
// If any updates are lost due to race conditions, the final sum will be wrong.
|
|
||||||
@(test)
|
@(test)
|
||||||
test_concurrent_atomic_add_no_lost_updates :: proc(t: ^testing.T) {
|
test_concurrent_atomic_add_no_lost_updates :: proc(t: ^testing.T) {
|
||||||
|
// Multiple threads will each add 1.0 this many times.
|
||||||
|
// If any updates are lost due to race conditions, the final sum will be wrong.
|
||||||
NUM_THREADS :: 8
|
NUM_THREADS :: 8
|
||||||
ITERATIONS_PER_THREAD :: 10_000
|
ITERATIONS_PER_THREAD :: 10_000
|
||||||
|
|
||||||
@@ -234,10 +184,10 @@ test_concurrent_atomic_add_no_lost_updates :: proc(t: ^testing.T) {
|
|||||||
testing.expect_value(t, shared_value, expected)
|
testing.expect_value(t, shared_value, expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start with a known value, multiple threads subtract.
|
|
||||||
// If any updates are lost due to race conditions, the final result will be wrong.
|
|
||||||
@(test)
|
@(test)
|
||||||
test_concurrent_atomic_sub_no_lost_updates :: proc(t: ^testing.T) {
|
test_concurrent_atomic_sub_no_lost_updates :: proc(t: ^testing.T) {
|
||||||
|
// Start with a known value, multiple threads subtract.
|
||||||
|
// If any updates are lost due to race conditions, the final result will be wrong.
|
||||||
NUM_THREADS :: 8
|
NUM_THREADS :: 8
|
||||||
ITERATIONS_PER_THREAD :: 10_000
|
ITERATIONS_PER_THREAD :: 10_000
|
||||||
|
|
||||||
@@ -278,11 +228,11 @@ test_concurrent_atomic_sub_no_lost_updates :: proc(t: ^testing.T) {
|
|||||||
testing.expect_value(t, shared_value, 0.0)
|
testing.expect_value(t, shared_value, 0.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Each thread multiplies by 2.0 then divides by 2.0.
|
|
||||||
// Since these are inverses, the final value should equal the starting value
|
|
||||||
// regardless of how operations interleave.
|
|
||||||
@(test)
|
@(test)
|
||||||
test_concurrent_atomic_mul_div_round_trip :: proc(t: ^testing.T) {
|
test_concurrent_atomic_mul_div_round_trip :: proc(t: ^testing.T) {
|
||||||
|
// Each thread multiplies by 2.0 then divides by 2.0.
|
||||||
|
// Since these are inverses, the final value should equal the starting value
|
||||||
|
// regardless of how operations interleave.
|
||||||
NUM_THREADS :: 8
|
NUM_THREADS :: 8
|
||||||
ITERATIONS_PER_THREAD :: 10_000
|
ITERATIONS_PER_THREAD :: 10_000
|
||||||
|
|
||||||
@@ -324,10 +274,10 @@ test_concurrent_atomic_mul_div_round_trip :: proc(t: ^testing.T) {
|
|||||||
testing.expect_value(t, shared_value, 1000.0)
|
testing.expect_value(t, shared_value, 1000.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the f32 type dispatch works correctly under contention.
|
|
||||||
// Same approach as the f64 add test but with f32.
|
|
||||||
@(test)
|
@(test)
|
||||||
test_atomic_add_with_f32 :: proc(t: ^testing.T) {
|
test_atomic_add_with_f32 :: proc(t: ^testing.T) {
|
||||||
|
// Verify the f32 type dispatch works correctly under contention.
|
||||||
|
// Same approach as the f64 add test but with f32.
|
||||||
NUM_THREADS :: 8
|
NUM_THREADS :: 8
|
||||||
ITERATIONS_PER_THREAD :: 10_000
|
ITERATIONS_PER_THREAD :: 10_000
|
||||||
|
|
||||||
@@ -369,17 +319,17 @@ test_atomic_add_with_f32 :: proc(t: ^testing.T) {
|
|||||||
testing.expect_value(t, shared_value, expected)
|
testing.expect_value(t, shared_value, expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tests that the memory order passed to atomic_float_op's CAS success condition
|
|
||||||
// provides full ordering guarantees for the entire float operation.
|
|
||||||
//
|
|
||||||
// Both sides use atomic_add_float (not raw intrinsics) to verify:
|
|
||||||
// - Release on CAS success publishes prior non-atomic writes
|
|
||||||
// - Acquire on CAS success makes those writes visible to the reader
|
|
||||||
//
|
|
||||||
// NOTE: This test may pass even with Relaxed ordering on x86 due to its strong memory model.
|
|
||||||
// On ARM or other weak-memory architectures, using Relaxed here would likely cause failures.
|
|
||||||
@(test)
|
@(test)
|
||||||
test_atomic_release_acquire_publish_visibility :: proc(t: ^testing.T) {
|
test_atomic_release_acquire_publish_visibility :: proc(t: ^testing.T) {
|
||||||
|
// Tests that the memory order passed to atomic_float_op's CAS success condition
|
||||||
|
// provides full ordering guarantees for the entire float operation.
|
||||||
|
//
|
||||||
|
// Both sides use atomic_add_float (not raw intrinsics) to verify:
|
||||||
|
// - Release on CAS success publishes prior non-atomic writes
|
||||||
|
// - Acquire on CAS success makes those writes visible to the reader
|
||||||
|
//
|
||||||
|
// NOTE: This test may pass even with Relaxed ordering on x86 due to its strong memory model.
|
||||||
|
// On ARM or other weak-memory architectures, using Relaxed here would likely cause failures.
|
||||||
NUM_READERS :: 4
|
NUM_READERS :: 4
|
||||||
|
|
||||||
Shared_State :: struct {
|
Shared_State :: struct {
|
||||||
@@ -476,20 +426,17 @@ test_atomic_release_acquire_publish_visibility :: proc(t: ^testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stress test for every spinlock acquisition variant: N threads contend on a
|
|
||||||
// single lock and perform a deliberate non-atomic read-modify-write on shared
|
|
||||||
// data. Each iteration rotates through spinlock_try_lock, spinlock_lock,
|
|
||||||
// spinlock_guard, and spinlock_try_guard so every variant runs concurrently and
|
|
||||||
// must uphold mutual exclusion on the same lock.
|
|
||||||
//
|
|
||||||
// If mutual exclusion holds:
|
|
||||||
// - `counter` ends at exactly NUM_THREADS * ITERATIONS_PER_THREAD
|
|
||||||
// - `concurrent_holders` never exceeds 1
|
|
||||||
//
|
|
||||||
// A multi-step RMW (read → relax → write) widens the critical section so
|
|
||||||
// any failure to exclude is virtually guaranteed to corrupt the counter.
|
|
||||||
@(test)
|
@(test)
|
||||||
test_spinlock_mutual_exclusion :: proc(t: ^testing.T) {
|
test_spinlock_try_lock_mutual_exclusion :: proc(t: ^testing.T) {
|
||||||
|
// Stress test for spinlock_try_lock: N threads spin-acquire the lock and
|
||||||
|
// perform a deliberate non-atomic read-modify-write on shared data.
|
||||||
|
//
|
||||||
|
// If mutual exclusion holds:
|
||||||
|
// - `counter` ends at exactly NUM_THREADS * ITERATIONS_PER_THREAD
|
||||||
|
// - `concurrent_holders` never exceeds 1
|
||||||
|
//
|
||||||
|
// A multi-step RMW (read → relax → write) widens the critical section so
|
||||||
|
// any failure to exclude is virtually guaranteed to corrupt the counter.
|
||||||
NUM_THREADS :: 8
|
NUM_THREADS :: 8
|
||||||
ITERATIONS_PER_THREAD :: 50_000
|
ITERATIONS_PER_THREAD :: 50_000
|
||||||
|
|
||||||
@@ -514,29 +461,6 @@ test_spinlock_mutual_exclusion :: proc(t: ^testing.T) {
|
|||||||
barrier: sync.Barrier
|
barrier: sync.Barrier
|
||||||
sync.barrier_init(&barrier, NUM_THREADS)
|
sync.barrier_init(&barrier, NUM_THREADS)
|
||||||
|
|
||||||
// The single critical section every acquisition variant must protect. Sharing
|
|
||||||
// it guarantees they all stress the exact same non-atomic read-modify-write.
|
|
||||||
critical_section :: proc(s: ^Shared) {
|
|
||||||
// Atomically bump the holder count so we can detect overlapping holders.
|
|
||||||
holders := intrinsics.atomic_add_explicit(&s.concurrent_holders, 1, .Relaxed)
|
|
||||||
|
|
||||||
// Track the maximum we ever observed (relaxed is fine, this is
|
|
||||||
// purely diagnostic and protected by the spinlock for writes).
|
|
||||||
if holders + 1 > s.max_holders {
|
|
||||||
s.max_holders = holders + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Non-atomic RMW: read, spin a tiny bit, then write.
|
|
||||||
// This deliberately creates a wide window where a second holder
|
|
||||||
// would cause a lost update.
|
|
||||||
val := s.counter
|
|
||||||
intrinsics.cpu_relax()
|
|
||||||
intrinsics.cpu_relax()
|
|
||||||
s.counter = val + 1
|
|
||||||
|
|
||||||
intrinsics.atomic_sub_explicit(&s.concurrent_holders, 1, .Relaxed)
|
|
||||||
}
|
|
||||||
|
|
||||||
thread_proc :: proc(th: ^thread.Thread) {
|
thread_proc :: proc(th: ^thread.Thread) {
|
||||||
ctx := cast(^Thread_Data)th.data
|
ctx := cast(^Thread_Data)th.data
|
||||||
s := ctx.shared
|
s := ctx.shared
|
||||||
@@ -544,35 +468,36 @@ test_spinlock_mutual_exclusion :: proc(t: ^testing.T) {
|
|||||||
// All threads rendezvous here for maximum contention.
|
// All threads rendezvous here for maximum contention.
|
||||||
sync.barrier_wait(ctx.barrier)
|
sync.barrier_wait(ctx.barrier)
|
||||||
|
|
||||||
for i in 0 ..< ITERATIONS_PER_THREAD {
|
for _ in 0 ..< ITERATIONS_PER_THREAD {
|
||||||
// Rotate through every acquisition variant so they all contend on the
|
// Spin on try_lock until we acquire it.
|
||||||
// same lock simultaneously and must each uphold mutual exclusion.
|
for !spinlock_try_lock(&s.lock) {
|
||||||
switch i & 3 {
|
intrinsics.cpu_relax()
|
||||||
case 0:
|
|
||||||
// Manual spin on try_lock until we acquire it.
|
|
||||||
for !spinlock_try_lock(&s.lock) {
|
|
||||||
intrinsics.cpu_relax()
|
|
||||||
}
|
|
||||||
critical_section(s)
|
|
||||||
spinlock_unlock(&s.lock)
|
|
||||||
case 1:
|
|
||||||
// Blocking lock that loops internally until acquired.
|
|
||||||
spinlock_lock(&s.lock)
|
|
||||||
critical_section(s)
|
|
||||||
spinlock_unlock(&s.lock)
|
|
||||||
case 2: // Scoped guard: unlocks automatically at the end of the block.
|
|
||||||
if spinlock_guard(&s.lock) {
|
|
||||||
critical_section(s)
|
|
||||||
}
|
|
||||||
case 3: // Scoped try-guard: retry until acquired, auto-unlocks on success.
|
|
||||||
for {
|
|
||||||
if spinlock_try_guard(&s.lock) {
|
|
||||||
critical_section(s)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
intrinsics.cpu_relax()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- critical section start ---
|
||||||
|
|
||||||
|
// Atomically bump the holder count so we can detect overlapping holders.
|
||||||
|
holders := intrinsics.atomic_add_explicit(&s.concurrent_holders, 1, .Relaxed)
|
||||||
|
|
||||||
|
// Track the maximum we ever observed (relaxed is fine, this is
|
||||||
|
// purely diagnostic and protected by the spinlock for writes).
|
||||||
|
if holders + 1 > s.max_holders {
|
||||||
|
s.max_holders = holders + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-atomic RMW: read, spin a tiny bit, then write.
|
||||||
|
// This deliberately creates a wide window where a second holder
|
||||||
|
// would cause a lost update.
|
||||||
|
val := s.counter
|
||||||
|
intrinsics.cpu_relax()
|
||||||
|
intrinsics.cpu_relax()
|
||||||
|
s.counter = val + 1
|
||||||
|
|
||||||
|
intrinsics.atomic_sub_explicit(&s.concurrent_holders, 1, .Relaxed)
|
||||||
|
|
||||||
|
// --- critical section end ---
|
||||||
|
|
||||||
|
spinlock_unlock(&s.lock)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,45 +9,46 @@ import qr ".."
|
|||||||
|
|
||||||
main :: proc() {
|
main :: proc() {
|
||||||
//----- General setup ----------------------------------
|
//----- General setup ----------------------------------
|
||||||
// Temp
|
{
|
||||||
track_temp: mem.Tracking_Allocator
|
// Temp
|
||||||
mem.tracking_allocator_init(&track_temp, context.temp_allocator)
|
track_temp: mem.Tracking_Allocator
|
||||||
context.temp_allocator = mem.tracking_allocator(&track_temp)
|
mem.tracking_allocator_init(&track_temp, context.temp_allocator)
|
||||||
|
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.
|
// 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.
|
// This could be fine for some global state or it could be a memory leak.
|
||||||
defer {
|
defer {
|
||||||
// Temp allocator
|
// Temp allocator
|
||||||
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
|
||||||
}
|
if len(track.allocation_map) > 0 {
|
||||||
// Default allocator
|
fmt.eprintf("=== %v allocations not freed - main allocator: ===\n", len(track.allocation_map))
|
||||||
if len(track.allocation_map) > 0 {
|
for _, entry in track.allocation_map {
|
||||||
fmt.eprintf("=== %v allocations not freed - main allocator: ===\n", len(track.allocation_map))
|
fmt.eprintf("- %v bytes @ %v\n", entry.size, entry.location)
|
||||||
for _, entry in track.allocation_map {
|
}
|
||||||
fmt.eprintf("- %v bytes @ %v\n", entry.size, entry.location)
|
|
||||||
}
|
}
|
||||||
}
|
if len(track.bad_free_array) > 0 {
|
||||||
if len(track.bad_free_array) > 0 {
|
fmt.eprintf("=== %v incorrect frees - main allocator: ===\n", len(track.bad_free_array))
|
||||||
fmt.eprintf("=== %v incorrect frees - main allocator: ===\n", len(track.bad_free_array))
|
for entry in track.bad_free_array {
|
||||||
for entry in track.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)
|
||||||
}
|
}
|
||||||
mem.tracking_allocator_destroy(&track)
|
// Logger
|
||||||
|
context.logger = log.create_console_logger()
|
||||||
|
defer log.destroy_console_logger(context.logger)
|
||||||
}
|
}
|
||||||
// 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 {
|
||||||
|
|||||||
+10
-52
@@ -2,8 +2,6 @@ package quantity
|
|||||||
|
|
||||||
import "base:intrinsics"
|
import "base:intrinsics"
|
||||||
|
|
||||||
LITERS_PER_GALLON :: 3.785411784
|
|
||||||
|
|
||||||
//----- Liters ----------------------------------
|
//----- Liters ----------------------------------
|
||||||
Liters :: struct($V: typeid) where intrinsics.type_is_numeric(V) {
|
Liters :: struct($V: typeid) where intrinsics.type_is_numeric(V) {
|
||||||
v: V,
|
v: V,
|
||||||
@@ -11,16 +9,9 @@ Liters :: struct($V: typeid) where intrinsics.type_is_numeric(V) {
|
|||||||
|
|
||||||
@(private = "file")
|
@(private = "file")
|
||||||
liters_to_milli_liters :: #force_inline proc "contextless" (
|
liters_to_milli_liters :: #force_inline proc "contextless" (
|
||||||
liters: Liters($V),
|
liters: Liters($V),
|
||||||
) -> Milli_Liters(V) where intrinsics.type_is_numeric(V) {
|
) -> Milli_Liters(V) where intrinsics.type_is_numeric(V) {
|
||||||
return Milli_Liters(V){liters.v * MILLI}
|
return Milli_Liters(V){liters.v * MILLI}
|
||||||
}
|
|
||||||
|
|
||||||
@(private = "file")
|
|
||||||
liters_to_gallons :: #force_inline proc "contextless" (
|
|
||||||
liters: Liters($V),
|
|
||||||
) -> Gallons(V) where intrinsics.type_is_float(V) {
|
|
||||||
return Gallons(V){liters.v / LITERS_PER_GALLON}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//----- Milliliters ----------------------------------
|
//----- Milliliters ----------------------------------
|
||||||
@@ -35,32 +26,15 @@ milli_liters_to_liters :: #force_inline proc "contextless" (
|
|||||||
return Liters(V){milli_liters.v / MILLI}
|
return Liters(V){milli_liters.v / MILLI}
|
||||||
}
|
}
|
||||||
|
|
||||||
//----- Gallons ----------------------------------
|
|
||||||
Gallons :: struct($V: typeid) where intrinsics.type_is_numeric(V) {
|
|
||||||
v: V,
|
|
||||||
}
|
|
||||||
|
|
||||||
@(private = "file")
|
|
||||||
gallons_to_liters :: #force_inline proc "contextless" (
|
|
||||||
gallons: Gallons($V),
|
|
||||||
) -> Liters(V) where intrinsics.type_is_float(V) {
|
|
||||||
return Liters(V){gallons.v * LITERS_PER_GALLON}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
// ----- Conversion Overloads ------------------------
|
// ----- Conversion Overloads ------------------------
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
to_liters :: proc {
|
to_liters :: proc {
|
||||||
milli_liters_to_liters,
|
milli_liters_to_liters,
|
||||||
gallons_to_liters,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
to_milli_liters :: proc {
|
to_milli_liters :: proc {
|
||||||
liters_to_milli_liters,
|
liters_to_milli_liters,
|
||||||
}
|
|
||||||
|
|
||||||
to_gallons :: proc {
|
|
||||||
liters_to_gallons,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
@@ -70,32 +44,16 @@ import "core:testing"
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_liters_to_milli_liters :: proc(t: ^testing.T) {
|
test_liters_to_milli_liters :: proc(t: ^testing.T) {
|
||||||
liters := Liters(int){12}
|
liters := Liters(int){12}
|
||||||
milli_liters := to_milli_liters(liters)
|
milli_liters := to_milli_liters(liters)
|
||||||
|
|
||||||
testing.expect_value(t, milli_liters, Milli_Liters(int){12_000})
|
testing.expect_value(t, milli_liters, Milli_Liters(int){12_000})
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_milli_liters_to_liters :: proc(t: ^testing.T) {
|
test_milli_liters_to_liters :: proc(t: ^testing.T) {
|
||||||
milli_liters := Milli_Liters(int){12_000}
|
milli_liters := Milli_Liters(int){12_000}
|
||||||
liters := to_liters(milli_liters)
|
liters := to_liters(milli_liters)
|
||||||
|
|
||||||
testing.expect_value(t, liters, Liters(int){12})
|
testing.expect_value(t, liters, Liters(int){12})
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_gallons_to_liters :: proc(t: ^testing.T) {
|
|
||||||
gallons := Gallons(f32){1}
|
|
||||||
liters := to_liters(gallons)
|
|
||||||
|
|
||||||
testing.expect(t, liters.v > 3.78 && liters.v < 3.79)
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_liters_to_gallons :: proc(t: ^testing.T) {
|
|
||||||
liters := Liters(f32){3.785411784}
|
|
||||||
gallons := to_gallons(liters)
|
|
||||||
|
|
||||||
testing.expect(t, gallons.v > 0.99 && gallons.v < 1.01)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,58 +2,6 @@ package quantity
|
|||||||
|
|
||||||
import "base:intrinsics"
|
import "base:intrinsics"
|
||||||
|
|
||||||
//----- Liters Per Minute ----------------------------------
|
|
||||||
Liters_Per_Minute :: struct($V: typeid) where intrinsics.type_is_numeric(V) {
|
Liters_Per_Minute :: struct($V: typeid) where intrinsics.type_is_numeric(V) {
|
||||||
v: V,
|
v: V,
|
||||||
}
|
}
|
||||||
|
|
||||||
@(private = "file")
|
|
||||||
liters_per_minute_to_gallons_per_minute :: #force_inline proc "contextless" (
|
|
||||||
liters_per_minute: Liters_Per_Minute($V),
|
|
||||||
) -> Gallons_Per_Minute(V) where intrinsics.type_is_float(V) {
|
|
||||||
return Gallons_Per_Minute(V){liters_per_minute.v / LITERS_PER_GALLON}
|
|
||||||
}
|
|
||||||
|
|
||||||
//----- Gallons Per Minute ----------------------------------
|
|
||||||
Gallons_Per_Minute :: struct($V: typeid) where intrinsics.type_is_numeric(V) {
|
|
||||||
v: V,
|
|
||||||
}
|
|
||||||
|
|
||||||
@(private = "file")
|
|
||||||
gallons_per_minute_to_liters_per_minute :: #force_inline proc "contextless" (
|
|
||||||
gallons_per_minute: Gallons_Per_Minute($V),
|
|
||||||
) -> Liters_Per_Minute(V) where intrinsics.type_is_float(V) {
|
|
||||||
return Liters_Per_Minute(V){gallons_per_minute.v * LITERS_PER_GALLON}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
|
||||||
// ----- Conversion Overloads ------------------------
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
|
||||||
to_liters_per_minute :: proc {
|
|
||||||
gallons_per_minute_to_liters_per_minute,
|
|
||||||
}
|
|
||||||
|
|
||||||
to_gallons_per_minute :: proc {
|
|
||||||
liters_per_minute_to_gallons_per_minute,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
|
||||||
// ----- Tests ------------------------
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
|
||||||
import "core:testing"
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_gallons_per_minute_to_liters_per_minute :: proc(t: ^testing.T) {
|
|
||||||
gallons_per_minute := Gallons_Per_Minute(f32){1}
|
|
||||||
liters_per_minute := to_liters_per_minute(gallons_per_minute)
|
|
||||||
|
|
||||||
testing.expect(t, liters_per_minute.v > 3.78 && liters_per_minute.v < 3.79)
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_liters_per_minute_to_gallons_per_minute :: proc(t: ^testing.T) {
|
|
||||||
liters_per_minute := Liters_Per_Minute(f32){3.785411784}
|
|
||||||
gallons_per_minute := to_gallons_per_minute(liters_per_minute)
|
|
||||||
|
|
||||||
testing.expect(t, gallons_per_minute.v > 0.99 && gallons_per_minute.v < 1.01)
|
|
||||||
}
|
|
||||||
|
|||||||
Vendored
+74
-192
@@ -18,14 +18,14 @@ when ODIN_OS == .Windows {
|
|||||||
|
|
||||||
String :: struct {
|
String :: struct {
|
||||||
isStaticallyAllocated: c.bool,
|
isStaticallyAllocated: c.bool,
|
||||||
length: c.int32_t,
|
length: c.int32_t,
|
||||||
chars: [^]c.char,
|
chars: [^]c.char,
|
||||||
}
|
}
|
||||||
|
|
||||||
StringSlice :: struct {
|
StringSlice :: struct {
|
||||||
length: c.int32_t,
|
length: c.int32_t,
|
||||||
chars: [^]c.char,
|
chars: [^]c.char,
|
||||||
baseChars: [^]c.char,
|
baseChars: [^]c.char,
|
||||||
}
|
}
|
||||||
|
|
||||||
Vector2 :: [2]c.float
|
Vector2 :: [2]c.float
|
||||||
@@ -57,6 +57,11 @@ CornerRadius :: struct {
|
|||||||
bottomRight: c.float,
|
bottomRight: c.float,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BorderData :: struct {
|
||||||
|
width: u32,
|
||||||
|
color: Color,
|
||||||
|
}
|
||||||
|
|
||||||
ElementId :: struct {
|
ElementId :: struct {
|
||||||
id: u32,
|
id: u32,
|
||||||
offset: u32,
|
offset: u32,
|
||||||
@@ -64,12 +69,6 @@ ElementId :: struct {
|
|||||||
stringId: String,
|
stringId: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
ElementIdArray :: struct {
|
|
||||||
capacity: i32,
|
|
||||||
length: i32,
|
|
||||||
internalArray: [^]ElementId,
|
|
||||||
}
|
|
||||||
|
|
||||||
when ODIN_OS == .Windows {
|
when ODIN_OS == .Windows {
|
||||||
EnumBackingType :: u32
|
EnumBackingType :: u32
|
||||||
} else {
|
} else {
|
||||||
@@ -84,13 +83,11 @@ RenderCommandType :: enum EnumBackingType {
|
|||||||
Image,
|
Image,
|
||||||
ScissorStart,
|
ScissorStart,
|
||||||
ScissorEnd,
|
ScissorEnd,
|
||||||
OverlayColorStart,
|
|
||||||
OverlayColorEnd,
|
|
||||||
Custom,
|
Custom,
|
||||||
}
|
}
|
||||||
|
|
||||||
RectangleElementConfig :: struct {
|
RectangleElementConfig :: struct {
|
||||||
color: Color,
|
color: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
TextWrapMode :: enum EnumBackingType {
|
TextWrapMode :: enum EnumBackingType {
|
||||||
@@ -106,22 +103,22 @@ TextAlignment :: enum EnumBackingType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TextElementConfig :: struct {
|
TextElementConfig :: struct {
|
||||||
userData: rawptr,
|
userData: rawptr,
|
||||||
textColor: Color,
|
textColor: Color,
|
||||||
fontId: u16,
|
fontId: u16,
|
||||||
fontSize: u16,
|
fontSize: u16,
|
||||||
letterSpacing: u16,
|
letterSpacing: u16,
|
||||||
lineHeight: u16,
|
lineHeight: u16,
|
||||||
wrapMode: TextWrapMode,
|
wrapMode: TextWrapMode,
|
||||||
textAlignment: TextAlignment,
|
textAlignment: TextAlignment,
|
||||||
}
|
}
|
||||||
|
|
||||||
AspectRatioElementConfig :: struct {
|
AspectRatioElementConfig :: struct {
|
||||||
aspectRatio: f32,
|
aspectRatio: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageElementConfig :: struct {
|
ImageElementConfig :: struct {
|
||||||
imageData: rawptr,
|
imageData: rawptr,
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomElementConfig :: struct {
|
CustomElementConfig :: struct {
|
||||||
@@ -129,10 +126,10 @@ CustomElementConfig :: struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
BorderWidth :: struct {
|
BorderWidth :: struct {
|
||||||
left: u16,
|
left: u16,
|
||||||
right: u16,
|
right: u16,
|
||||||
top: u16,
|
top: u16,
|
||||||
bottom: u16,
|
bottom: u16,
|
||||||
betweenChildren: u16,
|
betweenChildren: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,92 +138,6 @@ BorderElementConfig :: struct {
|
|||||||
width: BorderWidth,
|
width: BorderWidth,
|
||||||
}
|
}
|
||||||
|
|
||||||
TransitionData :: struct {
|
|
||||||
boundingBox: BoundingBox,
|
|
||||||
backgroundColor: Color,
|
|
||||||
overlayColor: Color,
|
|
||||||
borderColor: Color,
|
|
||||||
borderWidth: BorderWidth,
|
|
||||||
}
|
|
||||||
|
|
||||||
TransitionState :: enum c.int {
|
|
||||||
Idle,
|
|
||||||
Entering,
|
|
||||||
Transitioning,
|
|
||||||
Exiting,
|
|
||||||
}
|
|
||||||
|
|
||||||
TransitionProperty :: enum c.int {
|
|
||||||
X,
|
|
||||||
Y,
|
|
||||||
Width,
|
|
||||||
Height,
|
|
||||||
BackgroundColor,
|
|
||||||
OverlayColor,
|
|
||||||
CornerRadius,
|
|
||||||
BorderColor,
|
|
||||||
BorderWidth,
|
|
||||||
}
|
|
||||||
|
|
||||||
TransitionPropertyFlags :: bit_set[TransitionProperty;c.int]
|
|
||||||
TransitionPropertyPosition :: TransitionPropertyFlags{.X, .Y}
|
|
||||||
TransitionPropertyDimensions :: TransitionPropertyFlags{.Width, .Height}
|
|
||||||
TransitionPropertyBoundingBox :: TransitionPropertyPosition + TransitionPropertyDimensions
|
|
||||||
TransitionPropertyBorder :: TransitionPropertyFlags{.BorderColor, .BorderWidth}
|
|
||||||
|
|
||||||
TransitionCallbackArguments :: struct {
|
|
||||||
transitionState: TransitionState,
|
|
||||||
initial: TransitionData,
|
|
||||||
current: ^TransitionData,
|
|
||||||
target: TransitionData,
|
|
||||||
elapsedTime: f32,
|
|
||||||
duration: f32,
|
|
||||||
properties: TransitionPropertyFlags,
|
|
||||||
}
|
|
||||||
|
|
||||||
TransitionEnterTriggerType :: enum EnumBackingType {
|
|
||||||
SkipOnFirstParentFrame,
|
|
||||||
TriggerOnFirstParentFrame,
|
|
||||||
}
|
|
||||||
|
|
||||||
TransitionExitTriggerType :: enum EnumBackingType {
|
|
||||||
SkipWhenParentExits,
|
|
||||||
TriggerWhenParentExits,
|
|
||||||
}
|
|
||||||
|
|
||||||
TransitionInteractionHandlingType :: enum EnumBackingType {
|
|
||||||
DisableInteractionsWhileTransitioningPosition,
|
|
||||||
AllowInteractionsWhileTransitioningPosition,
|
|
||||||
}
|
|
||||||
|
|
||||||
ExitTransitionSiblingOrdering :: enum EnumBackingType {
|
|
||||||
UnderneathSiblings,
|
|
||||||
NaturalOrder,
|
|
||||||
AboveSiblings,
|
|
||||||
}
|
|
||||||
|
|
||||||
TransitionElementConfig :: struct {
|
|
||||||
handler: proc "c" (args: TransitionCallbackArguments) -> bool,
|
|
||||||
duration: f32,
|
|
||||||
properties: TransitionPropertyFlags,
|
|
||||||
interactionHandling: TransitionInteractionHandlingType,
|
|
||||||
enter: struct {
|
|
||||||
setInitialState: proc "c" (
|
|
||||||
initialState: TransitionData,
|
|
||||||
properties: TransitionPropertyFlags,
|
|
||||||
) -> TransitionData,
|
|
||||||
trigger: TransitionEnterTriggerType,
|
|
||||||
},
|
|
||||||
exit: struct {
|
|
||||||
setFinalState: proc "c" (
|
|
||||||
finalState: TransitionData,
|
|
||||||
properties: TransitionPropertyFlags,
|
|
||||||
) -> TransitionData,
|
|
||||||
trigger: TransitionExitTriggerType,
|
|
||||||
siblingOrdering: ExitTransitionSiblingOrdering,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
ClipElementConfig :: struct {
|
ClipElementConfig :: struct {
|
||||||
horizontal: bool, // clip overflowing elements on the "X" axis
|
horizontal: bool, // clip overflowing elements on the "X" axis
|
||||||
vertical: bool, // clip overflowing elements on the "Y" axis
|
vertical: bool, // clip overflowing elements on the "Y" axis
|
||||||
@@ -275,67 +186,56 @@ FloatingElementConfig :: struct {
|
|||||||
attachment: FloatingAttachPoints,
|
attachment: FloatingAttachPoints,
|
||||||
pointerCaptureMode: PointerCaptureMode,
|
pointerCaptureMode: PointerCaptureMode,
|
||||||
attachTo: FloatingAttachToElement,
|
attachTo: FloatingAttachToElement,
|
||||||
clipTo: FloatingClipToElement,
|
clipTo: FloatingClipToElement,
|
||||||
}
|
}
|
||||||
|
|
||||||
TextRenderData :: struct {
|
TextRenderData :: struct {
|
||||||
stringContents: StringSlice,
|
stringContents: StringSlice,
|
||||||
textColor: Color,
|
textColor: Color,
|
||||||
fontId: u16,
|
fontId: u16,
|
||||||
fontSize: u16,
|
fontSize: u16,
|
||||||
letterSpacing: u16,
|
letterSpacing: u16,
|
||||||
lineHeight: u16,
|
lineHeight: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
RectangleRenderData :: struct {
|
RectangleRenderData :: struct {
|
||||||
backgroundColor: Color,
|
backgroundColor: Color,
|
||||||
cornerRadius: CornerRadius,
|
cornerRadius: CornerRadius,
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageRenderData :: struct {
|
ImageRenderData :: struct {
|
||||||
backgroundColor: Color,
|
backgroundColor: Color,
|
||||||
cornerRadius: CornerRadius,
|
cornerRadius: CornerRadius,
|
||||||
imageData: rawptr,
|
imageData: rawptr,
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomRenderData :: struct {
|
CustomRenderData :: struct {
|
||||||
backgroundColor: Color,
|
backgroundColor: Color,
|
||||||
cornerRadius: CornerRadius,
|
cornerRadius: CornerRadius,
|
||||||
customData: rawptr,
|
customData: rawptr,
|
||||||
}
|
|
||||||
|
|
||||||
ClipRenderData :: struct {
|
|
||||||
horizontal: bool,
|
|
||||||
vertical: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
OverlayColorRenderData :: struct {
|
|
||||||
color: Color,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BorderRenderData :: struct {
|
BorderRenderData :: struct {
|
||||||
color: Color,
|
color: Color,
|
||||||
cornerRadius: CornerRadius,
|
cornerRadius: CornerRadius,
|
||||||
width: BorderWidth,
|
width: BorderWidth,
|
||||||
}
|
}
|
||||||
|
|
||||||
RenderCommandData :: struct #raw_union {
|
RenderCommandData :: struct #raw_union {
|
||||||
rectangle: RectangleRenderData,
|
rectangle: RectangleRenderData,
|
||||||
text: TextRenderData,
|
text: TextRenderData,
|
||||||
image: ImageRenderData,
|
image: ImageRenderData,
|
||||||
custom: CustomRenderData,
|
custom: CustomRenderData,
|
||||||
border: BorderRenderData,
|
border: BorderRenderData,
|
||||||
clip: ClipRenderData,
|
|
||||||
overlayColor: OverlayColorRenderData,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RenderCommand :: struct {
|
RenderCommand :: struct {
|
||||||
boundingBox: BoundingBox,
|
boundingBox: BoundingBox,
|
||||||
renderData: RenderCommandData,
|
renderData: RenderCommandData,
|
||||||
userData: rawptr,
|
userData: rawptr,
|
||||||
id: u32,
|
id: u32,
|
||||||
zIndex: i16,
|
zIndex: i16,
|
||||||
commandType: RenderCommandType,
|
commandType: RenderCommandType,
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrollContainerData :: struct {
|
ScrollContainerData :: struct {
|
||||||
@@ -395,9 +295,9 @@ Sizing :: struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Padding :: struct {
|
Padding :: struct {
|
||||||
left: u16,
|
left: u16,
|
||||||
right: u16,
|
right: u16,
|
||||||
top: u16,
|
top: u16,
|
||||||
bottom: u16,
|
bottom: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -438,17 +338,16 @@ ClayArray :: struct($type: typeid) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ElementDeclaration :: struct {
|
ElementDeclaration :: struct {
|
||||||
|
id: ElementId,
|
||||||
layout: LayoutConfig,
|
layout: LayoutConfig,
|
||||||
backgroundColor: Color,
|
backgroundColor: Color,
|
||||||
overlayColor: Color,
|
|
||||||
cornerRadius: CornerRadius,
|
cornerRadius: CornerRadius,
|
||||||
aspectRatio: AspectRatioElementConfig,
|
aspectRatio: AspectRatioElementConfig,
|
||||||
image: ImageElementConfig,
|
image: ImageElementConfig,
|
||||||
floating: FloatingElementConfig,
|
floating: FloatingElementConfig,
|
||||||
custom: CustomElementConfig,
|
custom: CustomElementConfig,
|
||||||
clip: ClipElementConfig,
|
clip: ClipElementConfig,
|
||||||
border: BorderElementConfig,
|
border: BorderElementConfig,
|
||||||
transition: TransitionElementConfig,
|
|
||||||
userData: rawptr,
|
userData: rawptr,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -461,17 +360,16 @@ ErrorType :: enum EnumBackingType {
|
|||||||
FloatingContainerParentNotFound,
|
FloatingContainerParentNotFound,
|
||||||
PercentageOver1,
|
PercentageOver1,
|
||||||
InternalError,
|
InternalError,
|
||||||
UnbalancedOpenClose,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ErrorData :: struct {
|
ErrorData :: struct {
|
||||||
errorType: ErrorType,
|
errorType: ErrorType,
|
||||||
errorText: String,
|
errorText: String,
|
||||||
userData: rawptr,
|
userData: rawptr,
|
||||||
}
|
}
|
||||||
|
|
||||||
ErrorHandler :: struct {
|
ErrorHandler :: struct {
|
||||||
handler: proc "c" (errorData: ErrorData),
|
handler: proc "c" (errorData: ErrorData),
|
||||||
userData: rawptr,
|
userData: rawptr,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -480,27 +378,23 @@ Context :: struct {} // opaque structure, only use as a pointer
|
|||||||
@(link_prefix = "Clay_", default_calling_convention = "c")
|
@(link_prefix = "Clay_", default_calling_convention = "c")
|
||||||
foreign Clay {
|
foreign Clay {
|
||||||
_OpenElement :: proc() ---
|
_OpenElement :: proc() ---
|
||||||
_OpenElementWithId :: proc(id: ElementId) ---
|
|
||||||
_CloseElement :: proc() ---
|
_CloseElement :: proc() ---
|
||||||
MinMemorySize :: proc() -> u32 ---
|
MinMemorySize :: proc() -> u32 ---
|
||||||
CreateArenaWithCapacityAndMemory :: proc(capacity: c.size_t, offset: [^]u8) -> Arena ---
|
CreateArenaWithCapacityAndMemory :: proc(capacity: c.size_t, offset: [^]u8) -> Arena ---
|
||||||
SetPointerState :: proc(position: Vector2, pointerDown: bool) ---
|
SetPointerState :: proc(position: Vector2, pointerDown: bool) ---
|
||||||
GetPointerState :: proc() -> PointerData ---
|
|
||||||
Initialize :: proc(arena: Arena, layoutDimensions: Dimensions, errorHandler: ErrorHandler) -> ^Context ---
|
Initialize :: proc(arena: Arena, layoutDimensions: Dimensions, errorHandler: ErrorHandler) -> ^Context ---
|
||||||
GetCurrentContext :: proc() -> ^Context ---
|
GetCurrentContext :: proc() -> ^Context ---
|
||||||
SetCurrentContext :: proc(ctx: ^Context) ---
|
SetCurrentContext :: proc(ctx: ^Context) ---
|
||||||
UpdateScrollContainers :: proc(enableDragScrolling: bool, scrollDelta: Vector2, deltaTime: c.float) ---
|
UpdateScrollContainers :: proc(enableDragScrolling: bool, scrollDelta: Vector2, deltaTime: c.float) ---
|
||||||
SetLayoutDimensions :: proc(dimensions: Dimensions) ---
|
SetLayoutDimensions :: proc(dimensions: Dimensions) ---
|
||||||
BeginLayout :: proc() ---
|
BeginLayout :: proc() ---
|
||||||
EndLayout :: proc(deltaTime: c.float) -> ClayArray(RenderCommand) ---
|
EndLayout :: proc() -> ClayArray(RenderCommand) ---
|
||||||
GetOpenElementId :: proc() -> u32 ---
|
|
||||||
GetElementId :: proc(id: String) -> ElementId ---
|
GetElementId :: proc(id: String) -> ElementId ---
|
||||||
GetElementIdWithIndex :: proc(id: String, index: u32) -> ElementId ---
|
GetElementIdWithIndex :: proc(id: String, index: u32) -> ElementId ---
|
||||||
GetElementData :: proc(id: ElementId) -> ElementData ---
|
GetElementData :: proc(id: ElementId) -> ElementData ---
|
||||||
Hovered :: proc() -> bool ---
|
Hovered :: proc() -> bool ---
|
||||||
OnHover :: proc(onHoverFunction: proc "c" (id: ElementId, pointerData: PointerData, userData: rawptr), userData: rawptr) ---
|
OnHover :: proc(onHoverFunction: proc "c" (id: ElementId, pointerData: PointerData, userData: rawptr), userData: rawptr) ---
|
||||||
PointerOver :: proc(id: ElementId) -> bool ---
|
PointerOver :: proc(id: ElementId) -> bool ---
|
||||||
GetPointerOverIds :: proc() -> ElementIdArray ---
|
|
||||||
GetScrollOffset :: proc() -> Vector2 ---
|
GetScrollOffset :: proc() -> Vector2 ---
|
||||||
GetScrollContainerData :: proc(id: ElementId) -> ScrollContainerData ---
|
GetScrollContainerData :: proc(id: ElementId) -> ScrollContainerData ---
|
||||||
SetMeasureTextFunction :: proc(measureTextFunction: proc "c" (text: StringSlice, config: ^TextElementConfig, userData: rawptr) -> Dimensions, userData: rawptr) ---
|
SetMeasureTextFunction :: proc(measureTextFunction: proc "c" (text: StringSlice, config: ^TextElementConfig, userData: rawptr) -> Dimensions, userData: rawptr) ---
|
||||||
@@ -514,15 +408,15 @@ foreign Clay {
|
|||||||
GetMaxMeasureTextCacheWordCount :: proc() -> i32 ---
|
GetMaxMeasureTextCacheWordCount :: proc() -> i32 ---
|
||||||
SetMaxMeasureTextCacheWordCount :: proc(maxMeasureTextCacheWordCount: i32) ---
|
SetMaxMeasureTextCacheWordCount :: proc(maxMeasureTextCacheWordCount: i32) ---
|
||||||
ResetMeasureTextCache :: proc() ---
|
ResetMeasureTextCache :: proc() ---
|
||||||
EaseOut :: proc(arguments: TransitionCallbackArguments) -> bool ---
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@(link_prefix = "Clay_", default_calling_convention = "c", private)
|
@(link_prefix = "Clay_", default_calling_convention = "c", private)
|
||||||
foreign Clay {
|
foreign Clay {
|
||||||
_ConfigureOpenElement :: proc(config: ElementDeclaration) ---
|
_ConfigureOpenElement :: proc(config: ElementDeclaration) ---
|
||||||
_HashString :: proc(key: String, seed: u32) -> ElementId ---
|
_HashString :: proc(key: String, offset: u32, seed: u32) -> ElementId ---
|
||||||
_HashStringWithOffset :: proc(key: String, index: u32, seed: u32) -> ElementId ---
|
_OpenTextElement :: proc(text: String, textConfig: ^TextElementConfig) ---
|
||||||
_OpenTextElement :: proc(text: String, textConfig: TextElementConfig) ---
|
_StoreTextElementConfig :: proc(config: TextElementConfig) -> ^TextElementConfig ---
|
||||||
|
_GetParentElementId :: proc() -> u32 ---
|
||||||
}
|
}
|
||||||
|
|
||||||
ConfigureOpenElement :: proc(config: ElementDeclaration) -> bool {
|
ConfigureOpenElement :: proc(config: ElementDeclaration) -> bool {
|
||||||
@@ -531,39 +425,27 @@ ConfigureOpenElement :: proc(config: ElementDeclaration) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@(deferred_none = _CloseElement)
|
@(deferred_none = _CloseElement)
|
||||||
UI_WithId :: proc(id: ElementId) -> proc(config: ElementDeclaration) -> bool {
|
UI :: proc() -> proc (config: ElementDeclaration) -> bool {
|
||||||
_OpenElementWithId(id)
|
|
||||||
return ConfigureOpenElement
|
|
||||||
}
|
|
||||||
|
|
||||||
@(deferred_none = _CloseElement)
|
|
||||||
UI_AutoId :: proc() -> proc(config: ElementDeclaration) -> bool {
|
|
||||||
_OpenElement()
|
_OpenElement()
|
||||||
return ConfigureOpenElement
|
return ConfigureOpenElement
|
||||||
}
|
}
|
||||||
|
|
||||||
UI :: proc {
|
Text :: proc($text: string, config: ^TextElementConfig) {
|
||||||
UI_WithId,
|
|
||||||
UI_AutoId,
|
|
||||||
}
|
|
||||||
|
|
||||||
Text :: proc {
|
|
||||||
TextStatic,
|
|
||||||
TextDynamic,
|
|
||||||
}
|
|
||||||
|
|
||||||
TextStatic :: proc($text: string, config: TextElementConfig) {
|
|
||||||
wrapped := MakeString(text)
|
wrapped := MakeString(text)
|
||||||
wrapped.isStaticallyAllocated = true
|
wrapped.isStaticallyAllocated = true
|
||||||
_OpenTextElement(wrapped, config)
|
_OpenTextElement(wrapped, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
TextDynamic :: proc(text: string, config: TextElementConfig) {
|
TextDynamic :: proc(text: string, config: ^TextElementConfig) {
|
||||||
_OpenTextElement(MakeString(text), config)
|
_OpenTextElement(MakeString(text), config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TextConfig :: proc(config: TextElementConfig) -> ^TextElementConfig {
|
||||||
|
return _StoreTextElementConfig(config)
|
||||||
|
}
|
||||||
|
|
||||||
PaddingAll :: proc(allPadding: u16) -> Padding {
|
PaddingAll :: proc(allPadding: u16) -> Padding {
|
||||||
return {left = allPadding, right = allPadding, top = allPadding, bottom = allPadding}
|
return { left = allPadding, right = allPadding, top = allPadding, bottom = allPadding }
|
||||||
}
|
}
|
||||||
|
|
||||||
BorderOutside :: proc(width: u16) -> BorderWidth {
|
BorderOutside :: proc(width: u16) -> BorderWidth {
|
||||||
@@ -578,11 +460,11 @@ CornerRadiusAll :: proc(radius: f32) -> CornerRadius {
|
|||||||
return CornerRadius{radius, radius, radius, radius}
|
return CornerRadius{radius, radius, radius, radius}
|
||||||
}
|
}
|
||||||
|
|
||||||
SizingFit :: proc(sizeMinMax: SizingConstraintsMinMax = {}) -> SizingAxis {
|
SizingFit :: proc(sizeMinMax: SizingConstraintsMinMax) -> SizingAxis {
|
||||||
return SizingAxis{type = SizingType.Fit, constraints = {sizeMinMax = sizeMinMax}}
|
return SizingAxis{type = SizingType.Fit, constraints = {sizeMinMax = sizeMinMax}}
|
||||||
}
|
}
|
||||||
|
|
||||||
SizingGrow :: proc(sizeMinMax: SizingConstraintsMinMax = {}) -> SizingAxis {
|
SizingGrow :: proc(sizeMinMax: SizingConstraintsMinMax) -> SizingAxis {
|
||||||
return SizingAxis{type = SizingType.Grow, constraints = {sizeMinMax = sizeMinMax}}
|
return SizingAxis{type = SizingType.Grow, constraints = {sizeMinMax = sizeMinMax}}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -599,9 +481,9 @@ MakeString :: proc(label: string) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ID :: proc(label: string, index: u32 = 0) -> ElementId {
|
ID :: proc(label: string, index: u32 = 0) -> ElementId {
|
||||||
return _HashString(MakeString(label), index)
|
return _HashString(MakeString(label), index, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
ID_LOCAL :: proc(label: string, index: u32 = 0) -> ElementId {
|
ID_LOCAL :: proc(label: string, index: u32 = 0) -> ElementId {
|
||||||
return _HashStringWithOffset(MakeString(label), index, GetOpenElementId())
|
return _HashString(MakeString(label), index, _GetParentElementId())
|
||||||
}
|
}
|
||||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
+6
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://raw.githubusercontent.com/DanielGavin/ols/master/misc/odinfmt.schema.json",
|
||||||
|
"character_width": 180,
|
||||||
|
"sort_imports": true,
|
||||||
|
"tabs": false
|
||||||
|
}
|
||||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
+38
-37
@@ -14,45 +14,46 @@ DB_PATH :: "out/debug/lmdb_example_db"
|
|||||||
|
|
||||||
main :: proc() {
|
main :: proc() {
|
||||||
//----- General setup ----------------------------------
|
//----- General setup ----------------------------------
|
||||||
// Temp
|
{
|
||||||
track_temp: mem.Tracking_Allocator
|
// Temp
|
||||||
mem.tracking_allocator_init(&track_temp, context.temp_allocator)
|
track_temp: mem.Tracking_Allocator
|
||||||
context.temp_allocator = mem.tracking_allocator(&track_temp)
|
mem.tracking_allocator_init(&track_temp, context.temp_allocator)
|
||||||
|
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.
|
// 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.
|
// This could be fine for some global state or it could be a memory leak.
|
||||||
defer {
|
defer {
|
||||||
// Temp allocator
|
// Temp allocator
|
||||||
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
|
||||||
}
|
if len(track.allocation_map) > 0 {
|
||||||
// Default allocator
|
fmt.eprintf("=== %v allocations not freed - main allocator: ===\n", len(track.allocation_map))
|
||||||
if len(track.allocation_map) > 0 {
|
for _, entry in track.allocation_map {
|
||||||
fmt.eprintf("=== %v allocations not freed - main allocator: ===\n", len(track.allocation_map))
|
fmt.eprintf("- %v bytes @ %v\n", entry.size, entry.location)
|
||||||
for _, entry in track.allocation_map {
|
}
|
||||||
fmt.eprintf("- %v bytes @ %v\n", entry.size, entry.location)
|
|
||||||
}
|
}
|
||||||
}
|
if len(track.bad_free_array) > 0 {
|
||||||
if len(track.bad_free_array) > 0 {
|
fmt.eprintf("=== %v incorrect frees - main allocator: ===\n", len(track.bad_free_array))
|
||||||
fmt.eprintf("=== %v incorrect frees - main allocator: ===\n", len(track.bad_free_array))
|
for entry in track.bad_free_array {
|
||||||
for entry in track.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)
|
||||||
}
|
}
|
||||||
mem.tracking_allocator_destroy(&track)
|
// Logger
|
||||||
|
context.logger = log.create_console_logger()
|
||||||
|
defer log.destroy_console_logger(context.logger)
|
||||||
}
|
}
|
||||||
// Logger
|
|
||||||
context.logger = log.create_console_logger()
|
|
||||||
defer log.destroy_console_logger(context.logger)
|
|
||||||
|
|
||||||
|
|
||||||
environment: ^mdb.Env
|
environment: ^mdb.Env
|
||||||
|
|
||||||
@@ -68,9 +69,9 @@ main :: proc() {
|
|||||||
db_handle: mdb.Dbi
|
db_handle: mdb.Dbi
|
||||||
// Put transaction
|
// Put transaction
|
||||||
key := 7
|
key := 7
|
||||||
key_val := mdb.pod_val(&key)
|
key_val := mdb.blittable_val(&key)
|
||||||
put_data := 12
|
put_data := 12
|
||||||
put_data_val := mdb.pod_val(&put_data)
|
put_data_val := mdb.blittable_val(&put_data)
|
||||||
mdb.panic_on_err(mdb.txn_begin(environment, nil, {}, &txn_handle))
|
mdb.panic_on_err(mdb.txn_begin(environment, nil, {}, &txn_handle))
|
||||||
mdb.panic_on_err(mdb.dbi_open(txn_handle, nil, {}, &db_handle))
|
mdb.panic_on_err(mdb.dbi_open(txn_handle, nil, {}, &db_handle))
|
||||||
mdb.panic_on_err(mdb.put(txn_handle, db_handle, &key_val, &put_data_val, {}))
|
mdb.panic_on_err(mdb.put(txn_handle, db_handle, &key_val, &put_data_val, {}))
|
||||||
@@ -80,7 +81,7 @@ main :: proc() {
|
|||||||
data_val: mdb.Val
|
data_val: mdb.Val
|
||||||
mdb.panic_on_err(mdb.txn_begin(environment, nil, {}, &txn_handle))
|
mdb.panic_on_err(mdb.txn_begin(environment, nil, {}, &txn_handle))
|
||||||
mdb.panic_on_err(mdb.get(txn_handle, db_handle, &key_val, &data_val))
|
mdb.panic_on_err(mdb.get(txn_handle, db_handle, &key_val, &data_val))
|
||||||
data_cpy := mdb.pod_copy(data_val, int)
|
data_cpy := mdb.blittable_copy(&data_val, int)
|
||||||
mdb.txn_abort(txn_handle)
|
mdb.panic_on_err(mdb.txn_commit(txn_handle))
|
||||||
fmt.println("Get result:", data_cpy)
|
fmt.println("Get result:", data_cpy)
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+35
-72
@@ -169,86 +169,58 @@ import "core:fmt"
|
|||||||
import "core:reflect"
|
import "core:reflect"
|
||||||
import "core:sys/posix"
|
import "core:sys/posix"
|
||||||
|
|
||||||
import b "../../basic"
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
// ----- Added Odin Helpers ------------------------
|
// ----- Added Odin Helpers ------------------------
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
// Wrap a POD value's bytes as an LMDB Val.
|
// 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.).
|
// T must be a contiguous type with no indirection (no pointers, slices, strings, maps, etc.).
|
||||||
pod_val :: #force_inline proc(val_ptr: ^$T) -> Val {
|
blittable_val :: #force_inline proc(val_ptr: ^$T) -> Val {
|
||||||
when ODIN_DEBUG {
|
fmt.assertf(
|
||||||
fmt.assertf(
|
reflect.has_no_indirections(type_info_of(T)),
|
||||||
reflect.has_no_indirections(type_info_of(T)),
|
"blitval: type '%v' contains indirection and cannot be stored directly in LMDB",
|
||||||
"pod_val: type '%v' contains indirection and cannot be stored directly in LMDB",
|
typeid_of(T),
|
||||||
typeid_of(T),
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
return Val{size_of(T), val_ptr}
|
return Val{size_of(T), val_ptr}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reads a POD T out of the LMDB memory map by copying it into caller
|
// 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.
|
// storage. The returned T has no lifetime tie to the transaction.
|
||||||
pod_copy :: #force_inline proc(val: Val, $T: typeid) -> T {
|
blittable_copy :: #force_inline proc(val: ^Val, $T: typeid) -> T {
|
||||||
when ODIN_DEBUG {
|
fmt.assertf(
|
||||||
fmt.assertf(
|
reflect.has_no_indirections(type_info_of(T)),
|
||||||
reflect.has_no_indirections(type_info_of(T)),
|
"blitval_copy: type '%v' contains indirection and cannot be read directly from LMDB",
|
||||||
"pod_copy: type '%v' contains indirection and cannot be read directly from LMDB",
|
typeid_of(T),
|
||||||
typeid_of(T),
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
when b.ODIN_BOUNDS_CHECK {
|
|
||||||
fmt.assertf(
|
|
||||||
val.size == size_of(T),
|
|
||||||
"size_of(%v) (%v) != val.size (%v)",
|
|
||||||
typeid_of(T),
|
|
||||||
size_of(T),
|
|
||||||
val.size,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return (cast(^T)val.data)^
|
return (cast(^T)val.data)^
|
||||||
}
|
}
|
||||||
|
|
||||||
// Zero-copy pointer view into the LMDB memory map as a ^T.
|
// Zero-copy pointer view into the LMDB memory map as a ^T.
|
||||||
// Useful for large POD types where you want to read individual fields
|
// Useful for large blittable types where you want to read individual fields
|
||||||
// without copying the entire value (e.g. ptr.timestamp, ptr.flags).
|
// without copying the entire value (e.g. ptr.timestamp, ptr.flags).
|
||||||
// MUST NOT be written through — writes either segfault (default env mode)
|
// MUST NOT be written through — writes either segfault (default env mode)
|
||||||
// or silently corrupt the database (ENV_WRITEMAP).
|
// or silently corrupt the database (ENV_WRITEMAP).
|
||||||
// MUST NOT be retained past txn_commit, txn_abort, or any subsequent write
|
// MUST NOT be retained past txn_commit, txn_abort, or any subsequent write
|
||||||
// operation on the same env — the pointer is invalidated.
|
// operation on the same env — the pointer is invalidated.
|
||||||
pod_view :: #force_inline proc(val: Val, $T: typeid) -> ^T {
|
blittable_view :: #force_inline proc(val: ^Val, $T: typeid) -> ^T {
|
||||||
when ODIN_DEBUG {
|
fmt.assertf(
|
||||||
fmt.assertf(
|
reflect.has_no_indirections(type_info_of(T)),
|
||||||
reflect.has_no_indirections(type_info_of(T)),
|
"blitval_view: type '%v' contains indirection and cannot be viewed directly from LMDB",
|
||||||
"pod_view: type '%v' contains indirection and cannot be viewed directly from LMDB",
|
typeid_of(T),
|
||||||
typeid_of(T),
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
when b.ODIN_BOUNDS_CHECK {
|
|
||||||
fmt.assertf(
|
|
||||||
val.size == size_of(T),
|
|
||||||
"size_of(%v) (%v) != val.size (%v)",
|
|
||||||
typeid_of(T),
|
|
||||||
size_of(T),
|
|
||||||
val.size,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return cast(^T)val.data
|
return cast(^T)val.data
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wrap a slice of POD elements as an LMDB Val for use with put/get.
|
// Wrap a slice of blittable elements as an LMDB Val for use with put/get.
|
||||||
// T must be a contiguous type with no indirection.
|
// T must be a contiguous type with no indirection.
|
||||||
// The caller's slice must remain valid (not freed, not resized) for the
|
// The caller's slice must remain valid (not freed, not resized) for the
|
||||||
// duration of the put call that consumes this Val.
|
// duration of the put call that consumes this Val.
|
||||||
pod_slice_val :: #force_inline proc(s: []$T) -> Val {
|
slice_val :: #force_inline proc(s: []$T) -> Val {
|
||||||
when ODIN_DEBUG {
|
fmt.assertf(
|
||||||
fmt.assertf(
|
reflect.has_no_indirections(type_info_of(T)),
|
||||||
reflect.has_no_indirections(type_info_of(T)),
|
"slice_val: element type '%v' contains indirection and cannot be stored directly in LMDB",
|
||||||
"pod_slice_val: element type '%v' contains indirection and cannot be stored directly in LMDB",
|
typeid_of(T),
|
||||||
typeid_of(T),
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
return Val{uint(len(s) * size_of(T)), raw_data(s)}
|
return Val{uint(len(s) * size_of(T)), raw_data(s)}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,21 +231,12 @@ pod_slice_val :: #force_inline proc(s: []$T) -> Val {
|
|||||||
// MUST be copied (e.g. slice.clone) if it needs to outlive the current
|
// 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
|
// transaction; the view is invalidated by txn_commit, txn_abort, or any
|
||||||
// subsequent write operation on the same env.
|
// subsequent write operation on the same env.
|
||||||
pod_slice_view :: #force_inline proc(val: Val, $T: typeid) -> []T {
|
slice_view :: #force_inline proc(val: ^Val, $T: typeid) -> []T {
|
||||||
when ODIN_DEBUG {
|
fmt.assertf(
|
||||||
fmt.assertf(
|
reflect.has_no_indirections(type_info_of(T)),
|
||||||
reflect.has_no_indirections(type_info_of(T)),
|
"slice_view: element type '%v' contains indirection and cannot be read directly from LMDB",
|
||||||
"pod_slice_view: element type '%v' contains indirection and cannot be read directly from LMDB",
|
typeid_of(T),
|
||||||
typeid_of(T),
|
)
|
||||||
)
|
|
||||||
fmt.assertf(
|
|
||||||
val.size % size_of(T) == 0,
|
|
||||||
"pod_slice_view: val.size (%v) is not a multiple of size_of(%v) (%v)",
|
|
||||||
val.size,
|
|
||||||
typeid_of(T),
|
|
||||||
size_of(T),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return (cast([^]T)val.data)[:val.size / size_of(T)]
|
return (cast([^]T)val.data)[:val.size / size_of(T)]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,7 +253,7 @@ string_val :: #force_inline proc(s: string) -> Val {
|
|||||||
// MUST be copied (e.g. strings.clone) if it needs to outlive the current
|
// 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
|
// transaction; the view is invalidated by txn_commit, txn_abort, or any
|
||||||
// subsequent write operation on the same env.
|
// subsequent write operation on the same env.
|
||||||
string_view :: #force_inline proc(val: Val) -> string {
|
string_view :: #force_inline proc(val: ^Val) -> string {
|
||||||
return string((cast([^]u8)val.data)[:val.size])
|
return string((cast([^]u8)val.data)[:val.size])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user