Backdrop Path + Cybersteel #23
@@ -75,6 +75,16 @@
|
||||
"command": "odin run draw/examples -debug -out=out/debug/draw-examples -- textures",
|
||||
"cwd": "$ZED_WORKTREE_ROOT",
|
||||
},
|
||||
{
|
||||
"label": "Run draw gaussian-blur example",
|
||||
"command": "odin run draw/examples -debug -out=out/debug/draw-examples -- gaussian-blur",
|
||||
"cwd": "$ZED_WORKTREE_ROOT",
|
||||
},
|
||||
{
|
||||
"label": "Run draw gaussian-blur-debug example",
|
||||
"command": "odin run draw/examples -debug -out=out/debug/draw-examples -- gaussian-blur-debug",
|
||||
"cwd": "$ZED_WORKTREE_ROOT",
|
||||
},
|
||||
{
|
||||
"label": "Run qrcode basic example",
|
||||
"command": "odin run qrcode/examples -debug -out=out/debug/qrcode-examples -- basic",
|
||||
|
||||
+92
-18
@@ -52,9 +52,12 @@ statically allocates registers for the worst-case path (Ring_Arc) regardless of
|
||||
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 opt-in (default `._1`, no MSAA) via `Init_Options.msaa_samples`. SDF rendering does not
|
||||
benefit from MSAA because fragment coverage is computed analytically. MSAA remains useful for text
|
||||
glyph edges and tessellated user geometry if desired.
|
||||
MSAA is intentionally not supported. SDF text and shapes compute fragment coverage analytically
|
||||
via `smoothstep`, so they don't benefit from multisampling. Tessellated user geometry submitted via
|
||||
`prepare_shape` is rendered without anti-aliasing — if AA is required for tessellated content, the
|
||||
caller must render it to their own offscreen target and submit the result as a texture. This
|
||||
decision matches RAD Debugger's architecture and aligns with the SBC target (Mali Valhall, where
|
||||
MSAA's per-tile bandwidth multiplier is expensive).
|
||||
|
||||
## 2D rendering pipeline plan
|
||||
|
||||
@@ -249,9 +252,9 @@ API where each layer draws shadows before quads before glyphs. Our design avoids
|
||||
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
|
||||
first use. 7 pipelines is ~175ms cold startup; 3 pipelines is ~75ms. Adding state axes (MSAA
|
||||
variants, blend modes, color formats) multiplies combinatorially — a 2.3× larger variant matrix per
|
||||
additional axis with 7 pipelines vs 3.
|
||||
first use. 7 pipelines is ~175ms cold startup; 3 pipelines is ~75ms. Adding state axes (blend
|
||||
modes, color formats) multiplies combinatorially — a 2.3× larger variant matrix per additional
|
||||
axis with 7 pipelines vs 3.
|
||||
|
||||
**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
|
||||
@@ -587,27 +590,29 @@ Wallace's variant) and vger-rs.
|
||||
### Backdrop pipeline
|
||||
|
||||
The backdrop pipeline handles effects that sample the current render target as input: frosted glass,
|
||||
refraction, mirror surfaces. It is separated from the effects pipeline for a structural reason, not
|
||||
register pressure.
|
||||
refraction, mirror surfaces. It is separated from the main and effects pipelines for a structural
|
||||
reason, not register pressure.
|
||||
|
||||
**Render-pass boundary.** Before any backdrop-sampling fragment can run, the current render target
|
||||
must be copied to a separate texture via `CopyGPUTextureToTexture`. This is a command-buffer-level
|
||||
operation that cannot happen mid-render-pass. The copy naturally creates a pipeline boundary that no
|
||||
amount of shader optimization can eliminate — it is a fundamental requirement of sampling a surface
|
||||
while also writing to it.
|
||||
must be in a sampler-readable state. A draw call that samples the render target it is also writing
|
||||
to is a hard GPU constraint; the only way to satisfy it is to end the current render pass and start
|
||||
a new one. That render-pass boundary is what a “bracket” is.
|
||||
|
||||
**Multi-pass implementation.** Backdrop effects are implemented as separable multi-pass sequences
|
||||
(downsample → horizontal blur → vertical blur → composite), following the standard approach used by
|
||||
(downsample → horizontal blur → vertical-blur+composite), following the standard approach used by
|
||||
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
|
||||
multi-pass approach avoids the monolithic 70+ register shader that a single-pass Gaussian blur would
|
||||
require, keeping each sub-pass well under the 32-register cliff.
|
||||
|
||||
**Bracketed execution.** All backdrop draws in a frame share a single bracketed region of the command
|
||||
buffer: end the current render pass, copy the render target, execute all backdrop sub-passes, then
|
||||
resume normal drawing. The entry/exit cost (texture copy + render-pass break) is paid once per frame
|
||||
regardless of how many backdrop effects are visible. When no backdrop effects are present, the bracket
|
||||
is never entered and the texture copy never happens — zero cost.
|
||||
**Approach B: render-target choice.** When any layer in the frame contains a backdrop draw, the
|
||||
entire frame renders into `source_texture` (a full-resolution single-sample texture owned by the
|
||||
backdrop pipeline) instead of directly into the swapchain. At the end of the frame, `source_texture`
|
||||
is copied to the swapchain via a single `CopyGPUTextureToTexture` call. This means the bracket has
|
||||
no mid-frame texture copy: by the time the bracket runs, `source_texture` already contains the pre-
|
||||
bracket frame contents 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
|
||||
registers, well under Valhall's 32-register cliff, so there is no occupancy motivation for splitting.
|
||||
@@ -617,6 +622,75 @@ all. Additionally, backdrop effects cover a small fraction of the frame's total
|
||||
typical UI scales), so even if a sub-pass did cross a cliff, the occupancy variation within the
|
||||
bracket would have negligible impact on frame time.
|
||||
|
||||
#### Bracket scheduling model
|
||||
|
||||
The bracket is scheduled per layer, anchored at the first backdrop sub-batch in the layer's
|
||||
submission order. Concretely, a layer with one or more backdrops splits into three groups:
|
||||
|
||||
1. **Pass A (pre-bracket)** — every non-backdrop sub-batch with index `< bracket_start_index`.
|
||||
Renders to `source_texture` in a single render pass.
|
||||
2. **The bracket** — every backdrop sub-batch in the layer (regardless of index). Runs one
|
||||
downsample pass, then one (H-blur + V-composite) pass pair per unique sigma.
|
||||
3. **Pass B (post-bracket)** — every non-backdrop sub-batch with index `>= bracket_start_index`.
|
||||
Renders to `source_texture` with `LOAD`, drawing on top of the composited backdrop output.
|
||||
|
||||
`bracket_start_index` is the absolute index of the first `.Backdrop` kind in the layer's sub-batch
|
||||
range. If the layer has no backdrops, none of this kicks in and the layer renders in a single render
|
||||
pass via the existing fast path.
|
||||
|
||||
The downsample runs once per layer, not once per sigma: it just copies `source_texture` to a ¼-
|
||||
resolution working texture and doesn't depend on the kernel. Each unique sigma in the layer triggers
|
||||
one H-blur (reads `downsample_texture`, writes `h_blur_texture`) and one V-composite (reads
|
||||
`h_blur_texture`, writes `source_texture` per-primitive with the SDF mask). Sub-batch coalescing in
|
||||
`append_or_extend_sub_batch` merges contiguous same-sigma backdrops into a single instanced V-
|
||||
composite draw call; non-contiguous same-sigma backdrops still share the H-blur output but issue
|
||||
separate V-composite draws.
|
||||
|
||||
#### Submission-order trade-off
|
||||
|
||||
Within Pass A and Pass B, sub-batches render in the user's submission order. What the bracket model
|
||||
sacrifices is *interleaved* ordering between backdrop and non-backdrop content within a single
|
||||
layer. A non-backdrop sub-batch submitted between two backdrops still renders in Pass B (after the
|
||||
bracket), not at its submission position. Worked example:
|
||||
|
||||
```
|
||||
draw.rectangle(layer, bg, GRAY) // 0 Tessellated → Pass A
|
||||
draw.rectangle(layer, card_blue, BLUE) // 1 SDF → Pass A
|
||||
draw.rectangle_backdrop(layer, panelA, 12) // 2 Backdrop → Bracket (sees: bg + blue card)
|
||||
draw.rectangle(layer, card_red, RED) // 3 SDF → Pass B (drawn ON TOP of panelA)
|
||||
draw.rectangle_backdrop(layer, panelB, 12) // 4 Backdrop → Bracket (sees: bg + blue card; same as panelA)
|
||||
draw.text(layer, "label", ...) // 5 Text → Pass B (drawn ON TOP of both panels)
|
||||
```
|
||||
|
||||
In this layer, panelB does *not* see card_red — even though card_red was submitted before panelB —
|
||||
because both backdrops sample `source_texture` as it stood at the bracket entry, which is after
|
||||
Pass A and before card_red has rendered. card_red ends up on top of panelA, not underneath it.
|
||||
|
||||
The user controls the alternative outcome by splitting layers. Putting card_red and panelB into a
|
||||
new layer (via `draw.new_layer`) gives panelB a fresh source snapshot that includes panelA and
|
||||
card_red:
|
||||
|
||||
```
|
||||
base := draw.begin(...)
|
||||
draw.rectangle(base, bg, GRAY)
|
||||
draw.rectangle(base, card_blue, BLUE)
|
||||
draw.rectangle_backdrop(base, panelA, 12) // panelA in base layer's bracket
|
||||
|
||||
top := draw.new_layer(base, ...)
|
||||
draw.rectangle(top, card_red, RED)
|
||||
draw.rectangle_backdrop(top, panelB, 12) // top layer's bracket; sees base + card_red
|
||||
draw.text(top, "label", ...)
|
||||
```
|
||||
|
||||
Why one bracket per layer and not one per backdrop? Each bracket adds three render passes
|
||||
(downsample + H-blur + V-composite) and at least three tile-cache flushes on tilers like Mali
|
||||
Valhall. Strict submission-order semantics would require one bracket per cluster of contiguous
|
||||
backdrops, which scales the GPU cost linearly with how interleaved the user's submission happens
|
||||
to be — a footgun. The current design caps the bracket cost per layer regardless of submission
|
||||
interleave, and gives the user explicit control over ordering through the existing layer
|
||||
abstraction. This matches the cost/complexity envelope of iOS `UIVisualEffectView` and CSS
|
||||
`backdrop-filter` (both of which constrain backdrop ordering implicitly).
|
||||
|
||||
### Vertex layout
|
||||
|
||||
The vertex struct is unchanged from the current 20-byte layout:
|
||||
|
||||
+1121
File diff suppressed because it is too large
Load Diff
+130
-123
@@ -4,7 +4,6 @@ import "base:runtime"
|
||||
import "core:c"
|
||||
import "core:log"
|
||||
import "core:math"
|
||||
|
||||
import "core:strings"
|
||||
import sdl "vendor:sdl3"
|
||||
import sdl_ttf "vendor:sdl3/ttf"
|
||||
@@ -16,11 +15,19 @@ when ODIN_OS == .Darwin {
|
||||
SHADER_ENTRY :: cstring("main0")
|
||||
BASE_VERT_2D_RAW :: #load("shaders/generated/base_2d.vert.metal")
|
||||
BASE_FRAG_2D_RAW :: #load("shaders/generated/base_2d.frag.metal")
|
||||
BACKDROP_FULLSCREEN_VERT_RAW :: #load("shaders/generated/backdrop_fullscreen.vert.metal")
|
||||
BACKDROP_DOWNSAMPLE_FRAG_RAW :: #load("shaders/generated/backdrop_downsample.frag.metal")
|
||||
BACKDROP_BLUR_VERT_RAW :: #load("shaders/generated/backdrop_blur.vert.metal")
|
||||
BACKDROP_BLUR_FRAG_RAW :: #load("shaders/generated/backdrop_blur.frag.metal")
|
||||
} else {
|
||||
PLATFORM_SHADER_FORMAT_FLAG :: sdl.GPUShaderFormatFlag.SPIRV
|
||||
SHADER_ENTRY :: cstring("main")
|
||||
BASE_VERT_2D_RAW :: #load("shaders/generated/base_2d.vert.spv")
|
||||
BASE_FRAG_2D_RAW :: #load("shaders/generated/base_2d.frag.spv")
|
||||
BACKDROP_FULLSCREEN_VERT_RAW :: #load("shaders/generated/backdrop_fullscreen.vert.spv")
|
||||
BACKDROP_DOWNSAMPLE_FRAG_RAW :: #load("shaders/generated/backdrop_downsample.frag.spv")
|
||||
BACKDROP_BLUR_VERT_RAW :: #load("shaders/generated/backdrop_blur.vert.spv")
|
||||
BACKDROP_BLUR_FRAG_RAW :: #load("shaders/generated/backdrop_blur.frag.spv")
|
||||
}
|
||||
PLATFORM_SHADER_FORMAT :: sdl.GPUShaderFormat{PLATFORM_SHADER_FORMAT_FLAG}
|
||||
|
||||
@@ -28,10 +35,6 @@ BUFFER_INIT_SIZE :: 256
|
||||
INITIAL_LAYER_SIZE :: 5
|
||||
INITIAL_SCISSOR_SIZE :: 10
|
||||
|
||||
// Sentinel value: when passed as msaa_samples, `init` will use the maximum MSAA sample count
|
||||
// supported by the GPU for the swapchain format.
|
||||
MSAA_MAX :: sdl.GPUSampleCount(0xFF)
|
||||
|
||||
// ----- Default parameter values -----
|
||||
// Named constants for non-zero default procedure parameters. Centralizes magic numbers
|
||||
// so they can be tuned in one place and referenced by name in proc signatures.
|
||||
@@ -39,8 +42,8 @@ DFT_FEATHER_PX :: 1 // Total AA feather width in physical pixels (half on each s
|
||||
DFT_STROKE_THICKNESS :: 1 // Default line/stroke thickness in logical pixels.
|
||||
DFT_FONT_SIZE :: 44 // Default font size in points for text rendering.
|
||||
DFT_CIRC_END_ANGLE :: 360 // Full-circle end angle in degrees (ring/arc).
|
||||
DFT_UV_RECT :: Rectangle{0, 0, 1, 1} // Full-texture UV rect (rectangle_texture).
|
||||
DFT_TINT :: WHITE // Default texture tint (rectangle_texture, clay_image).
|
||||
DFT_UV_RECT :: Rectangle{0, 0, 1, 1} // Full-texture UV rect (Texture_Fill default).
|
||||
DFT_TINT :: WHITE // Default texture tint (Texture_Fill, clay_image).
|
||||
DFT_TEXT_COLOR :: BLACK // Default text color.
|
||||
DFT_CLEAR_COLOR :: BLACK // Default clear color for end().
|
||||
DFT_SAMPLER :: Sampler_Preset.Linear_Clamp // Default texture sampler preset.
|
||||
@@ -53,9 +56,10 @@ Global :: struct {
|
||||
tmp_text_verts: [dynamic]Vertex, // Text vertices staged for GPU upload.
|
||||
tmp_text_indices: [dynamic]c.int, // Text index buffer staged for GPU upload.
|
||||
tmp_text_batches: [dynamic]TextBatch, // Text atlas batch metadata for indexed drawing.
|
||||
tmp_primitives: [dynamic]Primitive, // SDF primitives staged for GPU storage buffer upload.
|
||||
tmp_primitives: [dynamic]Base_2D_Primitive, // SDF primitives staged for GPU storage buffer upload (base 2D pipeline).
|
||||
tmp_sub_batches: [dynamic]Sub_Batch, // Sub-batch records that drive draw call dispatch.
|
||||
tmp_uncached_text: [dynamic]^sdl_ttf.Text, // Uncached TTF_Text objects destroyed after end() submits.
|
||||
tmp_backdrop_primitives: [dynamic]Backdrop_Primitive, // Backdrop primitives staged for GPU storage buffer upload.
|
||||
layers: [dynamic]Layer, // Draw layers, each with its own scissor stack.
|
||||
scissors: [dynamic]Scissor, // Scissor rects that clip drawing within each layer.
|
||||
|
||||
@@ -67,6 +71,7 @@ Global :: struct {
|
||||
|
||||
// -- Pipeline (accessed every draw_layer call) --
|
||||
pipeline_2d_base: Pipeline_2D_Base, // The unified 2D GPU pipeline (shaders, buffers, samplers).
|
||||
pipeline_2d_backdrop: Pipeline_2D_Backdrop, // Frosted-glass backdrop blur pipeline (downsample + blur PSOs, working textures).
|
||||
device: ^sdl.GPUDevice, // GPU device handle, stored at init.
|
||||
samplers: [SAMPLER_PRESET_COUNT]^sdl.GPUSampler, // Lazily-created sampler objects, one per Sampler_Preset.
|
||||
|
||||
@@ -78,12 +83,6 @@ Global :: struct {
|
||||
texture_slots: [dynamic]Texture_Slot, // Registered texture slots indexed by Texture_Id.
|
||||
texture_free_list: [dynamic]u32, // Recycled slot indices available for reuse.
|
||||
|
||||
// -- MSAA (once per frame in end()) --
|
||||
msaa_texture: ^sdl.GPUTexture, // Intermediate render target for multi-sample resolve.
|
||||
msaa_width: u32, // Cached width to detect when MSAA texture needs recreation.
|
||||
msaa_height: u32, // Cached height to detect when MSAA texture needs recreation.
|
||||
sample_count: sdl.GPUSampleCount, // Sample count chosen at init (._1 means MSAA disabled).
|
||||
|
||||
// -- Clay (once per frame in prepare_clay_batch) --
|
||||
clay_memory: [^]u8, // Raw memory block backing Clay's internal arena.
|
||||
|
||||
@@ -99,6 +98,7 @@ Global :: struct {
|
||||
max_text_batches: int,
|
||||
max_primitives: int,
|
||||
max_sub_batches: int,
|
||||
max_backdrop_primitives: int,
|
||||
|
||||
// -- Init-only (coldest — set once at init, never written again) --
|
||||
odin_context: runtime.Context, // Odin context captured at init for use in callbacks.
|
||||
@@ -128,8 +128,8 @@ Vec2 :: [2]f32
|
||||
// transparent. This matches the GPU-side layout: the shader unpacks via unpackUnorm4x8 which
|
||||
// reads the bytes in memory order as R, G, B, A and normalizes each to [0, 1].
|
||||
//
|
||||
// When used in the Primitive struct (Primitive.color), the 4 bytes are stored as a u32 in
|
||||
// native byte order and unpacked by the shader.
|
||||
// When used in the Base_2D_Primitive or Backdrop_Primitive structs (e.g. .color), the 4 bytes
|
||||
// are stored as a u32 in native byte order and unpacked by the shader.
|
||||
Color :: [4]u8
|
||||
|
||||
BLACK :: Color{0, 0, 0, 255}
|
||||
@@ -149,29 +149,42 @@ Rectangle_Radii :: struct {
|
||||
}
|
||||
|
||||
// A linear gradient between two colors along an arbitrary angle.
|
||||
// The `end_color` is the color at the end of the gradient direction; the shape's fill `color`
|
||||
// parameter acts as the start color. `angle` is in degrees: 0 = left-to-right, 90 = top-to-bottom.
|
||||
// `angle` is in degrees: 0 = left-to-right, 90 = top-to-bottom.
|
||||
Linear_Gradient :: struct {
|
||||
end_color: Color,
|
||||
angle: f32,
|
||||
start_color: Color,
|
||||
end_color: Color,
|
||||
angle: f32,
|
||||
}
|
||||
|
||||
// A radial gradient between two colors from center to edge.
|
||||
// The `outer_color` is the color at the shape's edge; the shape's fill `color` parameter
|
||||
// acts as the inner (center) color.
|
||||
Radial_Gradient :: struct {
|
||||
inner_color: Color,
|
||||
outer_color: Color,
|
||||
}
|
||||
|
||||
// Tagged union for specifying a gradient on any shape. Defaults to `nil` (no gradient).
|
||||
// When a gradient is active, the shape's `color` parameter becomes the start/inner color,
|
||||
// and the gradient struct carries the end/outer color plus any type-specific parameters.
|
||||
//
|
||||
// Gradient and Textured are mutually exclusive on the same primitive. If a shape uses
|
||||
// `rectangle_texture`, gradients are not applicable — use the tint color instead.
|
||||
Gradient :: union {
|
||||
// Sample a registered texture as the shape's fill source.
|
||||
// `tint` modulates the sampled texels per-pixel (constant multiply); WHITE passes through
|
||||
// unchanged. Translucent tints fade the texture; non-white tints recolor it.
|
||||
// Zero-initialized fields are treated as defaults by the shape procs:
|
||||
// tint == Color{} → WHITE
|
||||
// uv_rect == Rectangle{} → {0, 0, 1, 1} (full texture)
|
||||
// sampler == .Linear_Clamp (enum value 0)
|
||||
Texture_Fill :: struct {
|
||||
id: Texture_Id,
|
||||
tint: Color,
|
||||
uv_rect: Rectangle,
|
||||
sampler: Sampler_Preset,
|
||||
}
|
||||
|
||||
// Mutually exclusive fill sources for shape procs. Each shape proc accepts a Brush
|
||||
// as its third positional parameter. Texture and gradient are mutually exclusive at
|
||||
// the GPU level (they share the worst-case register path); outline is orthogonal and
|
||||
// composes with any Brush variant.
|
||||
Brush :: union {
|
||||
Color,
|
||||
Linear_Gradient,
|
||||
Radial_Gradient,
|
||||
Texture_Fill,
|
||||
}
|
||||
|
||||
// Convert clay.Color ([4]c.float in 0–255 range) to Color.
|
||||
@@ -207,17 +220,24 @@ Rectangle :: struct {
|
||||
}
|
||||
|
||||
Sub_Batch_Kind :: enum u8 {
|
||||
Tessellated, // non-indexed, white texture or user texture, mode 0
|
||||
Text, // indexed, atlas texture, mode 0
|
||||
SDF, // instanced unit quad, white texture or user texture, mode 1
|
||||
Tessellated, // non-indexed, white texture or user texture, base 2D mode 0
|
||||
Text, // indexed, atlas texture, base 2D mode 0
|
||||
SDF, // instanced unit quad, base 2D mode 1
|
||||
// instanced unit quad, backdrop pipeline V-composite (indexes Backdrop_Primitive).
|
||||
// Bracket-scheduled per layer; see README.md § "Backdrop pipeline" for ordering semantics.
|
||||
Backdrop,
|
||||
}
|
||||
|
||||
Sub_Batch :: struct {
|
||||
kind: Sub_Batch_Kind,
|
||||
offset: u32, // Tessellated: vertex offset; Text: text_batch index; SDF: primitive index
|
||||
count: u32, // Tessellated: vertex count; Text: always 1; SDF: primitive count
|
||||
texture_id: Texture_Id,
|
||||
sampler: Sampler_Preset,
|
||||
kind: Sub_Batch_Kind,
|
||||
offset: u32, // Tessellated: vertex offset; Text: text_batch index; SDF/Backdrop: primitive index
|
||||
count: u32, // Tessellated: vertex count; Text: always 1; SDF/Backdrop: primitive count
|
||||
texture_id: Texture_Id,
|
||||
sampler: Sampler_Preset,
|
||||
// Backdrop only — Gaussian std-dev in logical pixels. Named with the
|
||||
// distribution prefix because future kinds may want different sigma
|
||||
// shapes (e.g. drop-shadow penumbra) without overloading this field.
|
||||
gaussian_sigma: f32,
|
||||
}
|
||||
|
||||
Layer :: struct {
|
||||
@@ -234,39 +254,38 @@ Scissor :: struct {
|
||||
sub_batch_len: u32,
|
||||
}
|
||||
|
||||
Init_Options :: struct {
|
||||
// MSAA sample count. Default is ._1 (no MSAA). SDF rendering does not benefit from MSAA
|
||||
// because SDF fragments compute coverage analytically via `smoothstep`. MSAA helps for
|
||||
// text glyph edges and tessellated user geometry. Set to ._4 or ._8 for text-heavy UIs,
|
||||
// or use `MSAA_MAX` to request the highest sample count the GPU supports for the swapchain
|
||||
// format.
|
||||
msaa_samples: sdl.GPUSampleCount,
|
||||
}
|
||||
|
||||
// Initialize the renderer. Returns false if GPU pipeline or text engine creation fails.
|
||||
//
|
||||
// MSAA is intentionally NOT supported. SDF text and shapes compute coverage analytically via
|
||||
// `smoothstep`, so they don't benefit from multisampling. Tessellated user geometry submitted
|
||||
// via `prepare_shape` is not anti-aliased — if you need AA on tessellated content, render it
|
||||
// to your own offscreen target and submit it as a texture. RAD Debugger and the SBC target
|
||||
// (Mali Valhall, where MSAA's per-tile bandwidth multiplier is expensive) drove this decision.
|
||||
@(require_results)
|
||||
init :: proc(
|
||||
device: ^sdl.GPUDevice,
|
||||
window: ^sdl.Window,
|
||||
options: Init_Options = {},
|
||||
allocator := context.allocator,
|
||||
odin_context := context,
|
||||
) -> (
|
||||
ok: bool,
|
||||
) {
|
||||
min_memory_size: c.size_t = cast(c.size_t)clay.MinMemorySize()
|
||||
resolved_sample_count := options.msaa_samples
|
||||
if resolved_sample_count == MSAA_MAX {
|
||||
resolved_sample_count = max_sample_count(device, window)
|
||||
|
||||
pipeline, pipeline_ok := create_pipeline_2d_base(device, window)
|
||||
if !pipeline_ok {
|
||||
return false
|
||||
}
|
||||
|
||||
pipeline, pipeline_ok := create_pipeline_2d_base(device, window, resolved_sample_count)
|
||||
if !pipeline_ok {
|
||||
backdrop_pipeline, backdrop_pipeline_ok := create_pipeline_2d_backdrop(device, window)
|
||||
if !backdrop_pipeline_ok {
|
||||
destroy_pipeline_2d_base(device, &pipeline)
|
||||
return false
|
||||
}
|
||||
|
||||
text_cache, text_ok := init_text_cache(device, allocator)
|
||||
if !text_ok {
|
||||
destroy_pipeline_2d_backdrop(device, &backdrop_pipeline)
|
||||
destroy_pipeline_2d_base(device, &pipeline)
|
||||
return false
|
||||
}
|
||||
@@ -278,9 +297,10 @@ init :: proc(
|
||||
tmp_text_verts = make([dynamic]Vertex, 0, BUFFER_INIT_SIZE, allocator = allocator),
|
||||
tmp_text_indices = make([dynamic]c.int, 0, BUFFER_INIT_SIZE, allocator = allocator),
|
||||
tmp_text_batches = make([dynamic]TextBatch, 0, BUFFER_INIT_SIZE, allocator = allocator),
|
||||
tmp_primitives = make([dynamic]Primitive, 0, BUFFER_INIT_SIZE, allocator = allocator),
|
||||
tmp_primitives = make([dynamic]Base_2D_Primitive, 0, BUFFER_INIT_SIZE, allocator = allocator),
|
||||
tmp_sub_batches = make([dynamic]Sub_Batch, 0, BUFFER_INIT_SIZE, allocator = allocator),
|
||||
tmp_uncached_text = make([dynamic]^sdl_ttf.Text, 0, 16, allocator = allocator),
|
||||
tmp_backdrop_primitives = make([dynamic]Backdrop_Primitive, 0, BUFFER_INIT_SIZE, allocator = allocator),
|
||||
device = device,
|
||||
texture_slots = make([dynamic]Texture_Slot, 0, 16, allocator = allocator),
|
||||
texture_free_list = make([dynamic]u32, 0, 16, allocator = allocator),
|
||||
@@ -289,8 +309,8 @@ init :: proc(
|
||||
odin_context = odin_context,
|
||||
dpi_scaling = sdl.GetWindowDisplayScale(window),
|
||||
clay_memory = make([^]u8, min_memory_size, allocator = allocator),
|
||||
sample_count = resolved_sample_count,
|
||||
pipeline_2d_base = pipeline,
|
||||
pipeline_2d_backdrop = backdrop_pipeline,
|
||||
text_cache = text_cache,
|
||||
}
|
||||
|
||||
@@ -325,6 +345,8 @@ resize_global :: proc() {
|
||||
shrink(&GLOB.tmp_primitives, GLOB.max_primitives)
|
||||
if len(GLOB.tmp_sub_batches) > GLOB.max_sub_batches do GLOB.max_sub_batches = len(GLOB.tmp_sub_batches)
|
||||
shrink(&GLOB.tmp_sub_batches, GLOB.max_sub_batches)
|
||||
if len(GLOB.tmp_backdrop_primitives) > GLOB.max_backdrop_primitives do GLOB.max_backdrop_primitives = len(GLOB.tmp_backdrop_primitives)
|
||||
shrink(&GLOB.tmp_backdrop_primitives, GLOB.max_backdrop_primitives)
|
||||
}
|
||||
|
||||
destroy :: proc(device: ^sdl.GPUDevice, allocator := context.allocator) {
|
||||
@@ -336,17 +358,16 @@ destroy :: proc(device: ^sdl.GPUDevice, allocator := context.allocator) {
|
||||
delete(GLOB.tmp_text_batches)
|
||||
delete(GLOB.tmp_primitives)
|
||||
delete(GLOB.tmp_sub_batches)
|
||||
delete(GLOB.tmp_backdrop_primitives)
|
||||
for ttf_text in GLOB.tmp_uncached_text do sdl_ttf.DestroyText(ttf_text)
|
||||
delete(GLOB.tmp_uncached_text)
|
||||
free(GLOB.clay_memory, allocator)
|
||||
if GLOB.msaa_texture != nil {
|
||||
sdl.ReleaseGPUTexture(device, GLOB.msaa_texture)
|
||||
}
|
||||
process_pending_texture_releases()
|
||||
destroy_all_textures()
|
||||
destroy_sampler_pool()
|
||||
for ttf_text in GLOB.pending_text_releases do sdl_ttf.DestroyText(ttf_text)
|
||||
delete(GLOB.pending_text_releases)
|
||||
destroy_pipeline_2d_backdrop(device, &GLOB.pipeline_2d_backdrop)
|
||||
destroy_pipeline_2d_base(device, &GLOB.pipeline_2d_base)
|
||||
destroy_text_cache()
|
||||
}
|
||||
@@ -373,6 +394,7 @@ clear_global :: proc() {
|
||||
clear(&GLOB.tmp_text_batches)
|
||||
clear(&GLOB.tmp_primitives)
|
||||
clear(&GLOB.tmp_sub_batches)
|
||||
clear(&GLOB.tmp_backdrop_primitives)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
@@ -465,7 +487,7 @@ prepare_shape :: proc(layer: ^Layer, vertices: []Vertex) {
|
||||
}
|
||||
|
||||
// Submit an SDF primitive to the given layer for rendering.
|
||||
prepare_sdf_primitive :: proc(layer: ^Layer, prim: Primitive) {
|
||||
prepare_sdf_primitive :: proc(layer: ^Layer, prim: Base_2D_Primitive) {
|
||||
offset := u32(len(GLOB.tmp_primitives))
|
||||
append(&GLOB.tmp_primitives, prim)
|
||||
scissor := &GLOB.scissors[layer.scissor_start + layer.scissor_len - 1]
|
||||
@@ -578,6 +600,12 @@ prepare_text_transformed :: proc(layer: ^Layer, text: Text, transform: Transform
|
||||
}
|
||||
|
||||
// Append a new sub-batch or extend the last one if same kind and contiguous.
|
||||
//
|
||||
// `gaussian_sigma` is only consulted for kind == .Backdrop; two .Backdrop sub-batches with
|
||||
// different sigmas cannot coalesce because they require separate H+V blur passes in the
|
||||
// bracket scheduler. Float equality is intentional — user-supplied literal sigmas (e.g.
|
||||
// `sigma = 12`) produce bit-identical floats, and the worst case for two sigmas that differ
|
||||
// only by a ulp is one extra pass pair (correct, just slightly suboptimal).
|
||||
@(private)
|
||||
append_or_extend_sub_batch :: proc(
|
||||
scissor: ^Scissor,
|
||||
@@ -587,6 +615,7 @@ append_or_extend_sub_batch :: proc(
|
||||
count: u32,
|
||||
texture_id: Texture_Id = INVALID_TEXTURE,
|
||||
sampler: Sampler_Preset = DFT_SAMPLER,
|
||||
gaussian_sigma: f32 = 0,
|
||||
) {
|
||||
if scissor.sub_batch_len > 0 {
|
||||
last := &GLOB.tmp_sub_batches[scissor.sub_batch_start + scissor.sub_batch_len - 1]
|
||||
@@ -594,14 +623,22 @@ append_or_extend_sub_batch :: proc(
|
||||
kind != .Text &&
|
||||
last.offset + last.count == offset &&
|
||||
last.texture_id == texture_id &&
|
||||
last.sampler == sampler {
|
||||
last.sampler == sampler &&
|
||||
(kind != .Backdrop || last.gaussian_sigma == gaussian_sigma) {
|
||||
last.count += count
|
||||
return
|
||||
}
|
||||
}
|
||||
append(
|
||||
&GLOB.tmp_sub_batches,
|
||||
Sub_Batch{kind = kind, offset = offset, count = count, texture_id = texture_id, sampler = sampler},
|
||||
Sub_Batch {
|
||||
kind = kind,
|
||||
offset = offset,
|
||||
count = count,
|
||||
texture_id = texture_id,
|
||||
sampler = sampler,
|
||||
gaussian_sigma = gaussian_sigma,
|
||||
},
|
||||
)
|
||||
scissor.sub_batch_len += 1
|
||||
layer.sub_batch_len += 1
|
||||
@@ -710,7 +747,7 @@ prepare_clay_batch :: proc(
|
||||
|
||||
// Background color behind the image (Clay allows it)
|
||||
bg := color_from_clay(render_data.backgroundColor)
|
||||
if bg[3] > 0 {
|
||||
if bg.a > 0 {
|
||||
rectangle(layer, bounds, bg, radii = radii)
|
||||
}
|
||||
|
||||
@@ -718,7 +755,12 @@ prepare_clay_batch :: proc(
|
||||
uv, sampler, inner := fit_params(img_data.fit, bounds, img_data.texture_id)
|
||||
|
||||
// Draw the image
|
||||
rectangle_texture(layer, inner, img_data.texture_id, img_data.tint, uv, sampler, radii)
|
||||
rectangle(
|
||||
layer,
|
||||
inner,
|
||||
Texture_Fill{id = img_data.texture_id, tint = img_data.tint, uv_rect = uv, sampler = sampler},
|
||||
radii = radii,
|
||||
)
|
||||
case clay.RenderCommandType.ScissorStart:
|
||||
if bounds.width == 0 || bounds.height == 0 do continue
|
||||
|
||||
@@ -787,9 +829,19 @@ end :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window, clear_color: Color = DF
|
||||
log.panicf("Failed to acquire GPU command buffer: %s", sdl.GetError())
|
||||
}
|
||||
|
||||
// Upload primitives to GPU
|
||||
// Pre-scan: if any layer this frame has a backdrop sub-batch, route the entire frame to
|
||||
// source_texture (Approach B) so the bracket can sample the pre-bracket framebuffer
|
||||
// without a mid-frame texture copy. Frames without any backdrop hit the existing fast
|
||||
// path and never touch the backdrop pipeline's working textures.
|
||||
has_backdrop := frame_has_backdrop()
|
||||
|
||||
// Upload primitives to GPU (vertices, indices, SDF prims, and backdrop prims share one
|
||||
// copy pass so we pay the BeginGPUCopyPass / EndGPUCopyPass cost once per frame).
|
||||
copy_pass := sdl.BeginGPUCopyPass(cmd_buffer)
|
||||
upload(device, copy_pass)
|
||||
if has_backdrop {
|
||||
upload_backdrop_primitives(device, copy_pass)
|
||||
}
|
||||
sdl.EndGPUCopyPass(copy_pass)
|
||||
|
||||
swapchain_texture: ^sdl.GPUTexture
|
||||
@@ -806,12 +858,10 @@ end :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window, clear_color: Color = DF
|
||||
return
|
||||
}
|
||||
|
||||
use_msaa := GLOB.sample_count != ._1
|
||||
render_texture := swapchain_texture
|
||||
|
||||
if use_msaa {
|
||||
ensure_msaa_texture(device, sdl.GetGPUSwapchainTextureFormat(device, window), width, height)
|
||||
render_texture = GLOB.msaa_texture
|
||||
if has_backdrop {
|
||||
ensure_backdrop_textures(device, sdl.GetGPUSwapchainTextureFormat(device, window), width, height)
|
||||
render_texture = GLOB.pipeline_2d_backdrop.source_texture
|
||||
}
|
||||
|
||||
// Premultiply clear color: the blend state is ONE, ONE_MINUS_SRC_ALPHA (premultiplied),
|
||||
@@ -827,24 +877,23 @@ end :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window, clear_color: Color = DF
|
||||
|
||||
// Draw layers. One render pass per layer; sub-batches draw in submission order within each scissor.
|
||||
for &layer, index in GLOB.layers {
|
||||
log.debug("Drawing layer", index)
|
||||
draw_layer(device, window, cmd_buffer, render_texture, width, height, clear_color_f32, &layer)
|
||||
}
|
||||
|
||||
// Resolve MSAA render texture to the swapchain.
|
||||
if use_msaa {
|
||||
resolve_pass := sdl.BeginGPURenderPass(
|
||||
cmd_buffer,
|
||||
&sdl.GPUColorTargetInfo {
|
||||
texture = render_texture,
|
||||
load_op = .LOAD,
|
||||
store_op = .RESOLVE,
|
||||
resolve_texture = swapchain_texture,
|
||||
},
|
||||
// Approach B finalization: when we rendered into source_texture, copy it to the swapchain.
|
||||
// Single CopyGPUTextureToTexture call per frame, only when backdrop content was present.
|
||||
if has_backdrop {
|
||||
copy_pass := sdl.BeginGPUCopyPass(cmd_buffer)
|
||||
sdl.CopyGPUTextureToTexture(
|
||||
copy_pass,
|
||||
sdl.GPUTextureLocation{texture = GLOB.pipeline_2d_backdrop.source_texture},
|
||||
sdl.GPUTextureLocation{texture = swapchain_texture},
|
||||
width,
|
||||
height,
|
||||
1,
|
||||
nil,
|
||||
false,
|
||||
)
|
||||
sdl.EndGPURenderPass(resolve_pass)
|
||||
sdl.EndGPUCopyPass(copy_pass)
|
||||
}
|
||||
|
||||
if !sdl.SubmitGPUCommandBuffer(cmd_buffer) {
|
||||
@@ -852,48 +901,6 @@ end :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window, clear_color: Color = DF
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// ----- MSAA --------------------------
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// Query the highest MSAA sample count supported by the GPU for the swapchain format.
|
||||
max_sample_count :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window) -> sdl.GPUSampleCount {
|
||||
format := sdl.GetGPUSwapchainTextureFormat(device, window)
|
||||
counts := [?]sdl.GPUSampleCount{._8, ._4, ._2}
|
||||
for count in counts {
|
||||
if sdl.GPUTextureSupportsSampleCount(device, format, count) do return count
|
||||
}
|
||||
return ._1
|
||||
}
|
||||
|
||||
@(private = "file")
|
||||
ensure_msaa_texture :: proc(device: ^sdl.GPUDevice, format: sdl.GPUTextureFormat, width, height: u32) {
|
||||
if GLOB.msaa_texture != nil && GLOB.msaa_width == width && GLOB.msaa_height == height {
|
||||
return
|
||||
}
|
||||
if GLOB.msaa_texture != nil {
|
||||
sdl.ReleaseGPUTexture(device, GLOB.msaa_texture)
|
||||
}
|
||||
GLOB.msaa_texture = sdl.CreateGPUTexture(
|
||||
device,
|
||||
sdl.GPUTextureCreateInfo {
|
||||
type = .D2,
|
||||
format = format,
|
||||
usage = {.COLOR_TARGET},
|
||||
width = width,
|
||||
height = height,
|
||||
layer_count_or_depth = 1,
|
||||
num_levels = 1,
|
||||
sample_count = GLOB.sample_count,
|
||||
},
|
||||
)
|
||||
if GLOB.msaa_texture == nil {
|
||||
log.panicf("Failed to create MSAA texture (%dx%d): %s", width, height, sdl.GetError())
|
||||
}
|
||||
GLOB.msaa_width = width
|
||||
GLOB.msaa_height = height
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// ----- Utility -----------------------
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,383 @@
|
||||
package examples
|
||||
|
||||
import "core:fmt"
|
||||
import "core:math"
|
||||
import "core:os"
|
||||
import sdl "vendor:sdl3"
|
||||
|
||||
import "../../draw"
|
||||
import cyber "../cybersteel"
|
||||
|
||||
// Backdrop example.
|
||||
//
|
||||
// Verifies the Stage D 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}
|
||||
|
||||
draw.gaussian_blur(
|
||||
base_layer,
|
||||
{60, 80, 320, 140},
|
||||
gaussian_sigma = 30,
|
||||
tint = draw.Color{170, 200, 240, 200}, // cool blue, strong mix
|
||||
radii = panel_radii,
|
||||
)
|
||||
draw.text(
|
||||
base_layer,
|
||||
"sigma = 20, cool tint",
|
||||
{72, 90},
|
||||
PLEX_SANS_REGULAR,
|
||||
FONT_SIZE,
|
||||
color = draw.Color{30, 35, 50, 255},
|
||||
)
|
||||
|
||||
// Panel B: lighter blur, warm amber tint. sigma=6.
|
||||
draw.gaussian_blur(
|
||||
base_layer,
|
||||
{420, 80, 320, 140},
|
||||
gaussian_sigma = 6,
|
||||
tint = draw.Color{255, 220, 160, 200}, // warm amber, strong mix
|
||||
radii = panel_radii,
|
||||
)
|
||||
draw.text(
|
||||
base_layer,
|
||||
"sigma = 6, warm tint",
|
||||
{432, 90},
|
||||
PLEX_SANS_REGULAR,
|
||||
FONT_SIZE,
|
||||
color = draw.Color{60, 40, 20, 255},
|
||||
)
|
||||
|
||||
// Pass-B verification: a rectangle drawn AFTER the backdrops in the same layer
|
||||
// Per the bracket scheduling model, this should render ON TOP of both panels above.
|
||||
// If you see this stripe behind the panels instead of in front, something is wrong with
|
||||
// the Pass B post-bracket path.
|
||||
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.
|
||||
draw.gaussian_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,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// 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.gaussian_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},
|
||||
)
|
||||
draw.text(
|
||||
zone3,
|
||||
"sigma=0 (mirror)",
|
||||
{WINDOW_W * 0.55 + 38, 318},
|
||||
PLEX_SANS_REGULAR,
|
||||
FONT_SIZE,
|
||||
color = draw.Color{20, 20, 20, 255},
|
||||
)
|
||||
|
||||
// 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.gaussian_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.gaussian_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},
|
||||
)
|
||||
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},
|
||||
)
|
||||
|
||||
// Edge case 3: text drawn AFTER a backdrop in the same layer. Tests Pass B over a fresh
|
||||
// V-composite output. The text should appear sharply on top of the green panels above.
|
||||
draw.text(
|
||||
zone3,
|
||||
"Pass B 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 console label and to the title bar so you can correlate visual
|
||||
// behavior with kernel state (which is also logged via the [backdrop] debug print in
|
||||
// backdrop.odin's compute_blur_kernel callsite).
|
||||
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).
|
||||
panel := draw.Rectangle{250, 150, 300, 300}
|
||||
draw.gaussian_blur(
|
||||
base_layer,
|
||||
panel,
|
||||
gaussian_sigma = sigma,
|
||||
tint = draw.WHITE,
|
||||
radii = draw.Rectangle_Radii{20, 20, 20, 20},
|
||||
)
|
||||
|
||||
// Pass B test: a bright rectangle drawn AFTER the 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})
|
||||
}
|
||||
}
|
||||
+66
-43
@@ -5,65 +5,88 @@ import "core:log"
|
||||
import "core:mem"
|
||||
import "core:os"
|
||||
|
||||
EX_HELLOPE_SHAPES :: "hellope-shapes"
|
||||
EX_HELLOPE_TEXT :: "hellope-text"
|
||||
EX_HELLOPE_CLAY :: "hellope-clay"
|
||||
EX_HELLOPE_CUSTOM :: "hellope-custom"
|
||||
EX_TEXTURES :: "textures"
|
||||
EX_GAUSSIAN_BLUR :: "gaussian-blur"
|
||||
EX_GAUSSIAN_BLUR_DEBUG :: "gaussian-blur-debug"
|
||||
|
||||
AVAILABLE_EXAMPLES_MSG ::
|
||||
"Available examples: " +
|
||||
EX_HELLOPE_SHAPES +
|
||||
", " +
|
||||
EX_HELLOPE_TEXT +
|
||||
", " +
|
||||
EX_HELLOPE_CLAY +
|
||||
", " +
|
||||
EX_HELLOPE_CUSTOM +
|
||||
", " +
|
||||
EX_TEXTURES +
|
||||
", " +
|
||||
EX_GAUSSIAN_BLUR +
|
||||
", " +
|
||||
EX_GAUSSIAN_BLUR_DEBUG
|
||||
|
||||
main :: proc() {
|
||||
//----- General setup ----------------------------------
|
||||
{
|
||||
// Temp
|
||||
track_temp: mem.Tracking_Allocator
|
||||
mem.tracking_allocator_init(&track_temp, context.temp_allocator)
|
||||
context.temp_allocator = mem.tracking_allocator(&track_temp)
|
||||
// Temp
|
||||
track_temp: mem.Tracking_Allocator
|
||||
mem.tracking_allocator_init(&track_temp, context.temp_allocator)
|
||||
context.temp_allocator = mem.tracking_allocator(&track_temp)
|
||||
|
||||
// Default
|
||||
track: mem.Tracking_Allocator
|
||||
mem.tracking_allocator_init(&track, context.allocator)
|
||||
context.allocator = mem.tracking_allocator(&track)
|
||||
// Log a warning about any memory that was not freed by the end of the program.
|
||||
// This could be fine for some global state or it could be a memory leak.
|
||||
defer {
|
||||
// Temp allocator
|
||||
if len(track_temp.bad_free_array) > 0 {
|
||||
fmt.eprintf("=== %v incorrect frees - temp allocator: ===\n", len(track_temp.bad_free_array))
|
||||
for entry in track_temp.bad_free_array {
|
||||
fmt.eprintf("- %p @ %v\n", entry.memory, entry.location)
|
||||
}
|
||||
mem.tracking_allocator_destroy(&track_temp)
|
||||
// Default
|
||||
track: mem.Tracking_Allocator
|
||||
mem.tracking_allocator_init(&track, context.allocator)
|
||||
context.allocator = mem.tracking_allocator(&track)
|
||||
// Log a warning about any memory that was not freed by the end of the program.
|
||||
// This could be fine for some global state or it could be a memory leak.
|
||||
defer {
|
||||
// Temp allocator
|
||||
if len(track_temp.bad_free_array) > 0 {
|
||||
fmt.eprintf("=== %v incorrect frees - temp allocator: ===\n", len(track_temp.bad_free_array))
|
||||
for entry in track_temp.bad_free_array {
|
||||
fmt.eprintf("- %p @ %v\n", entry.memory, entry.location)
|
||||
}
|
||||
// Default allocator
|
||||
if len(track.allocation_map) > 0 {
|
||||
fmt.eprintf("=== %v allocations not freed - main allocator: ===\n", len(track.allocation_map))
|
||||
for _, entry in track.allocation_map {
|
||||
fmt.eprintf("- %v bytes @ %v\n", entry.size, entry.location)
|
||||
}
|
||||
}
|
||||
if len(track.bad_free_array) > 0 {
|
||||
fmt.eprintf("=== %v incorrect frees - main allocator: ===\n", len(track.bad_free_array))
|
||||
for entry in track.bad_free_array {
|
||||
fmt.eprintf("- %p @ %v\n", entry.memory, entry.location)
|
||||
}
|
||||
}
|
||||
mem.tracking_allocator_destroy(&track)
|
||||
mem.tracking_allocator_destroy(&track_temp)
|
||||
}
|
||||
// Logger
|
||||
context.logger = log.create_console_logger()
|
||||
defer log.destroy_console_logger(context.logger)
|
||||
// Default allocator
|
||||
if len(track.allocation_map) > 0 {
|
||||
fmt.eprintf("=== %v allocations not freed - main allocator: ===\n", len(track.allocation_map))
|
||||
for _, entry in track.allocation_map {
|
||||
fmt.eprintf("- %v bytes @ %v\n", entry.size, entry.location)
|
||||
}
|
||||
}
|
||||
if len(track.bad_free_array) > 0 {
|
||||
fmt.eprintf("=== %v incorrect frees - main allocator: ===\n", len(track.bad_free_array))
|
||||
for entry in track.bad_free_array {
|
||||
fmt.eprintf("- %p @ %v\n", entry.memory, entry.location)
|
||||
}
|
||||
}
|
||||
mem.tracking_allocator_destroy(&track)
|
||||
}
|
||||
context.logger = log.create_console_logger()
|
||||
defer log.destroy_console_logger(context.logger)
|
||||
|
||||
args := os.args
|
||||
if len(args) < 2 {
|
||||
fmt.eprintln("Usage: examples <example_name>")
|
||||
fmt.eprintln("Available examples: hellope-shapes, hellope-text, hellope-clay, hellope-custom, textures")
|
||||
fmt.eprintln(AVAILABLE_EXAMPLES_MSG)
|
||||
os.exit(1)
|
||||
}
|
||||
|
||||
switch args[1] {
|
||||
case "hellope-clay": hellope_clay()
|
||||
case "hellope-custom": hellope_custom()
|
||||
case "hellope-shapes": hellope_shapes()
|
||||
case "hellope-text": hellope_text()
|
||||
case "textures": textures()
|
||||
case EX_HELLOPE_CLAY: hellope_clay()
|
||||
case EX_HELLOPE_CUSTOM: hellope_custom()
|
||||
case EX_HELLOPE_SHAPES: hellope_shapes()
|
||||
case EX_HELLOPE_TEXT: hellope_text()
|
||||
case EX_TEXTURES: textures()
|
||||
case EX_GAUSSIAN_BLUR: gaussian_blur()
|
||||
case EX_GAUSSIAN_BLUR_DEBUG: gaussian_blur_debug()
|
||||
case:
|
||||
fmt.eprintf("Unknown example: %v\n", args[1])
|
||||
fmt.eprintln("Available examples: hellope-shapes, hellope-text, hellope-clay, hellope-custom, textures")
|
||||
fmt.eprintln(AVAILABLE_EXAMPLES_MSG)
|
||||
os.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,8 +48,7 @@ hellope_shapes :: proc() {
|
||||
draw.rectangle(
|
||||
base_layer,
|
||||
{20, 160, 460, 60},
|
||||
{255, 0, 0, 255},
|
||||
gradient = draw.Linear_Gradient{end_color = {0, 0, 255, 255}, angle = 0},
|
||||
draw.Linear_Gradient{start_color = {255, 0, 0, 255}, end_color = {0, 0, 255, 255}, angle = 0},
|
||||
)
|
||||
|
||||
// ----- Rotation demos -----
|
||||
@@ -79,18 +78,18 @@ hellope_shapes :: proc() {
|
||||
)
|
||||
|
||||
// Ellipse rotating around its center (tilted ellipse)
|
||||
draw.ellipse(base_layer, {410, 340}, 50, 30, {255, 200, 50, 255}, rotation = spin_angle)
|
||||
draw.ellipse(base_layer, {410, 340}, 50, 30, draw.Color{255, 200, 50, 255}, rotation = spin_angle)
|
||||
|
||||
// Circle orbiting a point (moon orbiting planet)
|
||||
// Convention B: center = pivot point (planet), origin = offset from moon center to pivot.
|
||||
// Moon's visual center at rotation=0: planet_pos - origin = (100, 450) - (0, 40) = (100, 410).
|
||||
planet_pos := draw.Vec2{100, 450}
|
||||
draw.circle(base_layer, planet_pos, 8, {200, 200, 200, 255}) // planet (stationary)
|
||||
draw.circle(base_layer, planet_pos, 8, draw.Color{200, 200, 200, 255}) // planet (stationary)
|
||||
draw.circle(
|
||||
base_layer,
|
||||
planet_pos,
|
||||
5,
|
||||
{100, 150, 255, 255},
|
||||
draw.Color{100, 150, 255, 255},
|
||||
origin = draw.Vec2{0, 40},
|
||||
rotation = spin_angle,
|
||||
) // moon orbiting
|
||||
@@ -101,7 +100,7 @@ hellope_shapes :: proc() {
|
||||
draw.Vec2{250, 450},
|
||||
0,
|
||||
30,
|
||||
{100, 100, 220, 255},
|
||||
draw.Color{100, 100, 220, 255},
|
||||
start_angle = 0,
|
||||
end_angle = 270,
|
||||
rotation = spin_angle,
|
||||
@@ -127,7 +126,7 @@ hellope_shapes :: proc() {
|
||||
{460, 450},
|
||||
6,
|
||||
30,
|
||||
{180, 100, 220, 255},
|
||||
draw.Color{180, 100, 220, 255},
|
||||
outline_color = draw.WHITE,
|
||||
outline_width = 2,
|
||||
rotation = spin_angle,
|
||||
@@ -190,14 +189,7 @@ hellope_text :: proc() {
|
||||
)
|
||||
|
||||
// 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}, PLEX_SANS_REGULAR, FONT_SIZE, color = draw.WHITE)
|
||||
|
||||
// Measure text for manual layout
|
||||
size := draw.measure_text("Measured!", PLEX_SANS_REGULAR, FONT_SIZE)
|
||||
|
||||
+164
-28
@@ -9,7 +9,7 @@ import cyber "../cybersteel"
|
||||
|
||||
textures :: proc() {
|
||||
if !sdl.Init({.VIDEO}) do os.exit(1)
|
||||
window := sdl.CreateWindow("Textures", 800, 600, {.HIGH_PIXEL_DENSITY})
|
||||
window := sdl.CreateWindow("Textures", 800, 750, {.HIGH_PIXEL_DENSITY})
|
||||
gpu := sdl.CreateGPUDevice(draw.PLATFORM_SHADER_FORMAT, true, nil)
|
||||
if !sdl.ClaimWindowForGPUDevice(gpu, window) do os.exit(1)
|
||||
if !draw.init(gpu, window) do os.exit(1)
|
||||
@@ -88,10 +88,10 @@ textures :: proc() {
|
||||
}
|
||||
spin_angle += 1
|
||||
|
||||
base_layer := draw.begin({width = 800, height = 600})
|
||||
base_layer := draw.begin({width = 800, height = 750})
|
||||
|
||||
// Background
|
||||
draw.rectangle(base_layer, {0, 0, 800, 600}, draw.Color{30, 30, 30, 255})
|
||||
draw.rectangle(base_layer, {0, 0, 800, 750}, draw.Color{30, 30, 30, 255})
|
||||
|
||||
//----- Row 1: Sampler presets (y=30) ----------------------------------
|
||||
|
||||
@@ -103,11 +103,15 @@ textures :: proc() {
|
||||
COL4 :: f32(480)
|
||||
|
||||
// Nearest (sharp pixel edges)
|
||||
draw.rectangle_texture(
|
||||
draw.rectangle(
|
||||
base_layer,
|
||||
{COL1, ROW1_Y, ITEM_SIZE, ITEM_SIZE},
|
||||
checker_texture,
|
||||
sampler = .Nearest_Clamp,
|
||||
draw.Texture_Fill {
|
||||
id = checker_texture,
|
||||
tint = draw.WHITE,
|
||||
uv_rect = {0, 0, 1, 1},
|
||||
sampler = .Nearest_Clamp,
|
||||
},
|
||||
)
|
||||
draw.text(
|
||||
base_layer,
|
||||
@@ -119,11 +123,15 @@ textures :: proc() {
|
||||
)
|
||||
|
||||
// Linear (bilinear blur)
|
||||
draw.rectangle_texture(
|
||||
draw.rectangle(
|
||||
base_layer,
|
||||
{COL2, ROW1_Y, ITEM_SIZE, ITEM_SIZE},
|
||||
checker_texture,
|
||||
sampler = .Linear_Clamp,
|
||||
draw.Texture_Fill {
|
||||
id = checker_texture,
|
||||
tint = draw.WHITE,
|
||||
uv_rect = {0, 0, 1, 1},
|
||||
sampler = .Linear_Clamp,
|
||||
},
|
||||
)
|
||||
draw.text(
|
||||
base_layer,
|
||||
@@ -135,12 +143,15 @@ textures :: proc() {
|
||||
)
|
||||
|
||||
// Tiled (4x repeat)
|
||||
draw.rectangle_texture(
|
||||
draw.rectangle(
|
||||
base_layer,
|
||||
{COL3, ROW1_Y, ITEM_SIZE, ITEM_SIZE},
|
||||
checker_texture,
|
||||
sampler = .Nearest_Repeat,
|
||||
uv_rect = {0, 0, 4, 4},
|
||||
draw.Texture_Fill {
|
||||
id = checker_texture,
|
||||
tint = draw.WHITE,
|
||||
uv_rect = {0, 0, 4, 4},
|
||||
sampler = .Nearest_Repeat,
|
||||
},
|
||||
)
|
||||
draw.text(
|
||||
base_layer,
|
||||
@@ -157,11 +168,10 @@ textures :: proc() {
|
||||
|
||||
// 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_texture(
|
||||
draw.rectangle(
|
||||
base_layer,
|
||||
{COL1, ROW2_Y, ITEM_SIZE, ITEM_SIZE},
|
||||
qr_texture,
|
||||
sampler = .Nearest_Clamp,
|
||||
draw.Texture_Fill{id = qr_texture, tint = draw.WHITE, uv_rect = {0, 0, 1, 1}, sampler = .Nearest_Clamp},
|
||||
)
|
||||
draw.text(
|
||||
base_layer,
|
||||
@@ -173,11 +183,15 @@ textures :: proc() {
|
||||
)
|
||||
|
||||
// Rounded corners
|
||||
draw.rectangle_texture(
|
||||
draw.rectangle(
|
||||
base_layer,
|
||||
{COL2, ROW2_Y, ITEM_SIZE, ITEM_SIZE},
|
||||
checker_texture,
|
||||
sampler = .Nearest_Clamp,
|
||||
draw.Texture_Fill {
|
||||
id = checker_texture,
|
||||
tint = draw.WHITE,
|
||||
uv_rect = {0, 0, 1, 1},
|
||||
sampler = .Nearest_Clamp,
|
||||
},
|
||||
radii = draw.uniform_radii({COL2, ROW2_Y, ITEM_SIZE, ITEM_SIZE}, 0.3),
|
||||
)
|
||||
draw.text(
|
||||
@@ -191,11 +205,15 @@ textures :: proc() {
|
||||
|
||||
// Rotating
|
||||
rot_rect := draw.Rectangle{COL3, ROW2_Y, ITEM_SIZE, ITEM_SIZE}
|
||||
draw.rectangle_texture(
|
||||
draw.rectangle(
|
||||
base_layer,
|
||||
rot_rect,
|
||||
checker_texture,
|
||||
sampler = .Nearest_Clamp,
|
||||
draw.Texture_Fill {
|
||||
id = checker_texture,
|
||||
tint = draw.WHITE,
|
||||
uv_rect = {0, 0, 1, 1},
|
||||
sampler = .Nearest_Clamp,
|
||||
},
|
||||
origin = draw.center_of(rot_rect),
|
||||
rotation = spin_angle,
|
||||
)
|
||||
@@ -216,7 +234,11 @@ textures :: proc() {
|
||||
// Stretch
|
||||
uv_s, sampler_s, inner_s := draw.fit_params(.Stretch, {COL1, ROW3_Y, FIT_SIZE, FIT_SIZE}, stripe_texture)
|
||||
draw.rectangle(base_layer, {COL1, ROW3_Y, FIT_SIZE, FIT_SIZE}, draw.Color{60, 60, 60, 255}) // bg
|
||||
draw.rectangle_texture(base_layer, inner_s, stripe_texture, uv_rect = uv_s, sampler = sampler_s)
|
||||
draw.rectangle(
|
||||
base_layer,
|
||||
inner_s,
|
||||
draw.Texture_Fill{id = stripe_texture, tint = draw.WHITE, uv_rect = uv_s, sampler = sampler_s},
|
||||
)
|
||||
draw.text(
|
||||
base_layer,
|
||||
"Stretch",
|
||||
@@ -229,7 +251,11 @@ textures :: proc() {
|
||||
// Fill (center-crop)
|
||||
uv_f, sampler_f, inner_f := draw.fit_params(.Fill, {COL2, ROW3_Y, FIT_SIZE, FIT_SIZE}, stripe_texture)
|
||||
draw.rectangle(base_layer, {COL2, ROW3_Y, FIT_SIZE, FIT_SIZE}, draw.Color{60, 60, 60, 255})
|
||||
draw.rectangle_texture(base_layer, inner_f, stripe_texture, uv_rect = uv_f, sampler = sampler_f)
|
||||
draw.rectangle(
|
||||
base_layer,
|
||||
inner_f,
|
||||
draw.Texture_Fill{id = stripe_texture, tint = draw.WHITE, uv_rect = uv_f, sampler = sampler_f},
|
||||
)
|
||||
draw.text(
|
||||
base_layer,
|
||||
"Fill",
|
||||
@@ -242,7 +268,11 @@ textures :: proc() {
|
||||
// Fit (letterbox)
|
||||
uv_ft, sampler_ft, inner_ft := draw.fit_params(.Fit, {COL3, ROW3_Y, FIT_SIZE, FIT_SIZE}, stripe_texture)
|
||||
draw.rectangle(base_layer, {COL3, ROW3_Y, FIT_SIZE, FIT_SIZE}, draw.Color{60, 60, 60, 255}) // visible margin bg
|
||||
draw.rectangle_texture(base_layer, inner_ft, stripe_texture, uv_rect = uv_ft, sampler = sampler_ft)
|
||||
draw.rectangle(
|
||||
base_layer,
|
||||
inner_ft,
|
||||
draw.Texture_Fill{id = stripe_texture, tint = draw.WHITE, uv_rect = uv_ft, sampler = sampler_ft},
|
||||
)
|
||||
draw.text(
|
||||
base_layer,
|
||||
"Fit",
|
||||
@@ -253,11 +283,15 @@ textures :: proc() {
|
||||
)
|
||||
|
||||
// Per-corner radii
|
||||
draw.rectangle_texture(
|
||||
draw.rectangle(
|
||||
base_layer,
|
||||
{COL4, ROW3_Y, FIT_SIZE, FIT_SIZE},
|
||||
checker_texture,
|
||||
sampler = .Nearest_Clamp,
|
||||
draw.Texture_Fill {
|
||||
id = checker_texture,
|
||||
tint = draw.WHITE,
|
||||
uv_rect = {0, 0, 1, 1},
|
||||
sampler = .Nearest_Clamp,
|
||||
},
|
||||
radii = {20, 0, 20, 0},
|
||||
)
|
||||
draw.text(
|
||||
@@ -269,6 +303,108 @@ textures :: proc() {
|
||||
color = draw.WHITE,
|
||||
)
|
||||
|
||||
//----- Row 4: Textured shapes (y=520) ----------------------------------
|
||||
|
||||
ROW4_Y :: f32(520)
|
||||
SHAPE_SIZE :: f32(80)
|
||||
SHAPE_GAP :: f32(30)
|
||||
SHAPE_COL1 :: f32(30)
|
||||
SHAPE_COL2 :: SHAPE_COL1 + SHAPE_SIZE + SHAPE_GAP
|
||||
SHAPE_COL3 :: SHAPE_COL2 + SHAPE_SIZE + SHAPE_GAP
|
||||
SHAPE_COL4 :: SHAPE_COL3 + SHAPE_SIZE + SHAPE_GAP
|
||||
SHAPE_COL5 :: SHAPE_COL4 + SHAPE_SIZE + SHAPE_GAP
|
||||
|
||||
checker_fill := draw.Texture_Fill {
|
||||
id = checker_texture,
|
||||
tint = draw.WHITE,
|
||||
uv_rect = {0, 0, 1, 1},
|
||||
sampler = .Nearest_Clamp,
|
||||
}
|
||||
|
||||
// Textured circle
|
||||
draw.circle(
|
||||
base_layer,
|
||||
{SHAPE_COL1 + SHAPE_SIZE / 2, ROW4_Y + SHAPE_SIZE / 2},
|
||||
SHAPE_SIZE / 2,
|
||||
checker_fill,
|
||||
)
|
||||
draw.text(
|
||||
base_layer,
|
||||
"Circle",
|
||||
{SHAPE_COL1, ROW4_Y + SHAPE_SIZE + LABEL_OFFSET},
|
||||
PLEX_SANS_REGULAR,
|
||||
FONT_SIZE,
|
||||
color = draw.WHITE,
|
||||
)
|
||||
|
||||
// Textured ellipse
|
||||
draw.ellipse(
|
||||
base_layer,
|
||||
{SHAPE_COL2 + SHAPE_SIZE / 2, ROW4_Y + SHAPE_SIZE / 2},
|
||||
SHAPE_SIZE / 2,
|
||||
SHAPE_SIZE / 3,
|
||||
checker_fill,
|
||||
)
|
||||
draw.text(
|
||||
base_layer,
|
||||
"Ellipse",
|
||||
{SHAPE_COL2, ROW4_Y + SHAPE_SIZE + LABEL_OFFSET},
|
||||
PLEX_SANS_REGULAR,
|
||||
FONT_SIZE,
|
||||
color = draw.WHITE,
|
||||
)
|
||||
|
||||
// Textured polygon (hexagon)
|
||||
draw.polygon(
|
||||
base_layer,
|
||||
{SHAPE_COL3 + SHAPE_SIZE / 2, ROW4_Y + SHAPE_SIZE / 2},
|
||||
6,
|
||||
SHAPE_SIZE / 2,
|
||||
checker_fill,
|
||||
)
|
||||
draw.text(
|
||||
base_layer,
|
||||
"Polygon",
|
||||
{SHAPE_COL3, ROW4_Y + SHAPE_SIZE + LABEL_OFFSET},
|
||||
PLEX_SANS_REGULAR,
|
||||
FONT_SIZE,
|
||||
color = draw.WHITE,
|
||||
)
|
||||
|
||||
// Textured ring
|
||||
draw.ring(
|
||||
base_layer,
|
||||
{SHAPE_COL4 + SHAPE_SIZE / 2, ROW4_Y + SHAPE_SIZE / 2},
|
||||
SHAPE_SIZE / 4,
|
||||
SHAPE_SIZE / 2,
|
||||
checker_fill,
|
||||
)
|
||||
draw.text(
|
||||
base_layer,
|
||||
"Ring",
|
||||
{SHAPE_COL4, ROW4_Y + SHAPE_SIZE + LABEL_OFFSET},
|
||||
PLEX_SANS_REGULAR,
|
||||
FONT_SIZE,
|
||||
color = draw.WHITE,
|
||||
)
|
||||
|
||||
// Textured line (capsule)
|
||||
draw.line(
|
||||
base_layer,
|
||||
{SHAPE_COL5, ROW4_Y + SHAPE_SIZE / 2},
|
||||
{SHAPE_COL5 + SHAPE_SIZE, ROW4_Y + SHAPE_SIZE / 2},
|
||||
checker_fill,
|
||||
thickness = 20,
|
||||
)
|
||||
draw.text(
|
||||
base_layer,
|
||||
"Line",
|
||||
{SHAPE_COL5, ROW4_Y + SHAPE_SIZE + LABEL_OFFSET},
|
||||
PLEX_SANS_REGULAR,
|
||||
FONT_SIZE,
|
||||
color = draw.WHITE,
|
||||
)
|
||||
|
||||
draw.end(gpu, window)
|
||||
}
|
||||
}
|
||||
|
||||
+133
-28
@@ -29,7 +29,7 @@ TextBatch :: struct {
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// The SDF path evaluates one of four signed distance functions per primitive, dispatched
|
||||
// by Shape_Kind encoded in the low byte of Primitive.flags:
|
||||
// by Shape_Kind encoded in the low byte of Base_2D_Primitive.flags:
|
||||
//
|
||||
// RRect — rounded rectangle with per-corner radii (sdRoundedBox). Also covers circles
|
||||
// (uniform radii = half-size), capsule-style line segments (rotated, max rounding),
|
||||
@@ -47,10 +47,10 @@ Shape_Kind :: enum u8 {
|
||||
}
|
||||
|
||||
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
|
||||
Textured, // bit 0: sample texture using uv_rect (mutually exclusive with Gradient via Brush union)
|
||||
Gradient, // bit 1: 2-color gradient using 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
|
||||
Outline, // bit 3: outer outline band using 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.
|
||||
@@ -97,7 +97,7 @@ Shape_Params :: struct #raw_union {
|
||||
#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.
|
||||
// Packed into 16 bytes. Independent from uv_rect — texture and outline can coexist.
|
||||
// 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.
|
||||
@@ -107,38 +107,33 @@ Gradient_Outline :: struct {
|
||||
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.
|
||||
// GPU layout: 96 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 {
|
||||
//
|
||||
// Named Base_2D_Primitive (not just Primitive) to disambiguate from Backdrop_Primitive in
|
||||
// pipeline_2d_backdrop.odin. The two pipelines have unrelated GPU layouts and unrelated
|
||||
// fragment-shader contracts; pairing each with its own primitive type keeps cross-references
|
||||
// unambiguous when grepping the codebase.
|
||||
Base_2D_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
|
||||
uv_rect: [4]f32, // 64: texture UV coordinates (u_min, v_min, u_max, v_max). Read when .Textured.
|
||||
effects: Gradient_Outline, // 80: gradient and/or outline parameters. Read when .Gradient and/or .Outline.
|
||||
}
|
||||
#assert(size_of(Base_2D_Primitive) == 96)
|
||||
|
||||
#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 shape kind and flags into the Base_2D_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)
|
||||
}
|
||||
@@ -159,11 +154,13 @@ Pipeline_2D_Base :: struct {
|
||||
sampler: ^sdl.GPUSampler,
|
||||
}
|
||||
|
||||
// MSAA is not supported by levlib (see init's doc comment in draw.odin); the PSO is hard-wired
|
||||
// to single-sample. SDF text and shapes provide analytical AA via smoothstep; tessellated user
|
||||
// geometry is not anti-aliased.
|
||||
@(private)
|
||||
create_pipeline_2d_base :: proc(
|
||||
device: ^sdl.GPUDevice,
|
||||
window: ^sdl.Window,
|
||||
sample_count: sdl.GPUSampleCount,
|
||||
) -> (
|
||||
pipeline: Pipeline_2D_Base,
|
||||
ok: bool,
|
||||
@@ -237,7 +234,7 @@ create_pipeline_2d_base :: proc(
|
||||
vertex_shader = vert_shader,
|
||||
fragment_shader = frag_shader,
|
||||
primitive_type = .TRIANGLELIST,
|
||||
multisample_state = sdl.GPUMultisampleState{sample_count = sample_count},
|
||||
multisample_state = sdl.GPUMultisampleState{sample_count = ._1},
|
||||
target_info = sdl.GPUGraphicsPipelineTargetInfo {
|
||||
color_target_descriptions = &sdl.GPUColorTargetDescription {
|
||||
format = sdl.GetGPUSwapchainTextureFormat(device, window),
|
||||
@@ -302,7 +299,7 @@ create_pipeline_2d_base :: proc(
|
||||
prim_buf_ok: bool
|
||||
pipeline.primitive_buffer, prim_buf_ok = create_buffer(
|
||||
device,
|
||||
size_of(Primitive) * BUFFER_INIT_SIZE,
|
||||
size_of(Base_2D_Primitive) * BUFFER_INIT_SIZE,
|
||||
sdl.GPUBufferUsageFlags{.GRAPHICS_STORAGE_READ},
|
||||
)
|
||||
if !prim_buf_ok do return pipeline, false
|
||||
@@ -505,7 +502,7 @@ upload :: proc(device: ^sdl.GPUDevice, pass: ^sdl.GPUCopyPass) {
|
||||
// Upload SDF primitives
|
||||
prim_count := u32(len(GLOB.tmp_primitives))
|
||||
if prim_count > 0 {
|
||||
prim_size := prim_count * size_of(Primitive)
|
||||
prim_size := prim_count * size_of(Base_2D_Primitive)
|
||||
|
||||
grow_buffer_if_needed(
|
||||
device,
|
||||
@@ -560,6 +557,101 @@ draw_layer :: proc(
|
||||
return
|
||||
}
|
||||
|
||||
bracket_start_abs := find_first_backdrop_in_layer(layer)
|
||||
layer_end_abs := int(layer.sub_batch_start + layer.sub_batch_len)
|
||||
|
||||
if bracket_start_abs < 0 {
|
||||
// Fast path: no backdrop in this layer; render the whole sub-batch range in one pass.
|
||||
render_layer_sub_batch_range(
|
||||
cmd_buffer,
|
||||
render_texture,
|
||||
swapchain_width,
|
||||
swapchain_height,
|
||||
clear_color,
|
||||
layer,
|
||||
int(layer.sub_batch_start),
|
||||
layer_end_abs,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Bracketed layer: Pass A → backdrop bracket → Pass B.
|
||||
// See README.md § "Backdrop pipeline" for the full ordering semantics.
|
||||
render_layer_sub_batch_range(
|
||||
cmd_buffer,
|
||||
render_texture,
|
||||
swapchain_width,
|
||||
swapchain_height,
|
||||
clear_color,
|
||||
layer,
|
||||
int(layer.sub_batch_start),
|
||||
bracket_start_abs,
|
||||
)
|
||||
|
||||
run_backdrop_bracket(cmd_buffer, layer, swapchain_width, swapchain_height)
|
||||
|
||||
// Pass B: render the [bracket_start_abs, layer_end_abs) range. .Backdrop sub-batches in
|
||||
// this range are dispatched by the bracket above and ignored here (the .Backdrop case in
|
||||
// the inner switch is a no-op). LOAD is implied because Pass A or the bracket's V-
|
||||
// composite has already touched render_texture.
|
||||
render_layer_sub_batch_range(
|
||||
cmd_buffer,
|
||||
render_texture,
|
||||
swapchain_width,
|
||||
swapchain_height,
|
||||
clear_color,
|
||||
layer,
|
||||
bracket_start_abs,
|
||||
layer_end_abs,
|
||||
)
|
||||
}
|
||||
|
||||
// Render a sub-range of a layer's sub-batches in a single render pass. Iterates the layer's
|
||||
// scissors and walks each scissor's sub-batches, dispatching by kind. The `range_start_abs`
|
||||
// and `range_end_abs` parameters are absolute indices into GLOB.tmp_sub_batches; only sub-
|
||||
// batches within `[range_start_abs, range_end_abs)` are drawn.
|
||||
//
|
||||
// .Backdrop sub-batches in the range are always silently skipped — they are dispatched by
|
||||
// run_backdrop_bracket, not here. The empty .Backdrop case in the inner switch enforces this.
|
||||
//
|
||||
// Render-pass setup mirrors the original draw_layer: clear-or-load based on GLOB.cleared,
|
||||
// pipeline + storage + index buffer bound up front, then per-batch state tracking. After this
|
||||
// proc returns, GLOB.cleared is guaranteed true.
|
||||
//
|
||||
// If the range is empty after filtering (no eligible sub-batches at all), this proc still
|
||||
// honors the no-clear-yet contract by issuing a clear-only pass when needed; otherwise it
|
||||
// returns without opening a render pass.
|
||||
@(private)
|
||||
render_layer_sub_batch_range :: proc(
|
||||
cmd_buffer: ^sdl.GPUCommandBuffer,
|
||||
render_texture: ^sdl.GPUTexture,
|
||||
swapchain_width: u32,
|
||||
swapchain_height: u32,
|
||||
clear_color: [4]f32,
|
||||
layer: ^Layer,
|
||||
range_start_abs: int,
|
||||
range_end_abs: int,
|
||||
) {
|
||||
if range_start_abs >= range_end_abs {
|
||||
// Empty range. If we still owe a clear, do a clear-only pass; otherwise nothing to do.
|
||||
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 {
|
||||
@@ -611,9 +703,17 @@ draw_layer :: proc(
|
||||
text_vertex_gpu_base := u32(len(GLOB.tmp_shape_verts))
|
||||
|
||||
for &scissor in GLOB.scissors[layer.scissor_start:][:layer.scissor_len] {
|
||||
// Intersect this scissor's sub-batch span with the requested range.
|
||||
scissor_start := int(scissor.sub_batch_start)
|
||||
scissor_end := scissor_start + int(scissor.sub_batch_len)
|
||||
effective_start := max(scissor_start, range_start_abs)
|
||||
effective_end := min(scissor_end, range_end_abs)
|
||||
if effective_start >= effective_end do continue
|
||||
|
||||
sdl.SetGPUScissor(render_pass, scissor.bounds)
|
||||
|
||||
for &batch in GLOB.tmp_sub_batches[scissor.sub_batch_start:][:scissor.sub_batch_len] {
|
||||
for abs_idx in effective_start ..< effective_end {
|
||||
batch := &GLOB.tmp_sub_batches[abs_idx]
|
||||
switch batch.kind {
|
||||
case .Tessellated:
|
||||
if current_mode != .Tessellated {
|
||||
@@ -702,6 +802,11 @@ draw_layer :: proc(
|
||||
current_sampler = batch_sampler
|
||||
}
|
||||
sdl.DrawGPUPrimitives(render_pass, 6, batch.count, 0, batch.offset)
|
||||
|
||||
case .Backdrop:
|
||||
// Always a no-op here. Backdrop sub-batches are dispatched by run_backdrop_bracket;
|
||||
// when this proc encounters one (only possible in Pass B, since Pass A and the no-
|
||||
// backdrop fast path both stop their range before any .Backdrop index), we skip it.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
#pragma clang diagnostic ignored "-Wmissing-prototypes"
|
||||
|
||||
#include <metal_stdlib>
|
||||
#include <simd/simd.h>
|
||||
|
||||
using namespace metal;
|
||||
|
||||
struct Uniforms
|
||||
{
|
||||
float2 inv_working_size;
|
||||
uint pair_count;
|
||||
uint mode;
|
||||
float2 direction;
|
||||
float inv_downsample_factor;
|
||||
float _pad0;
|
||||
float4 kernel0[32];
|
||||
};
|
||||
|
||||
struct main0_out
|
||||
{
|
||||
float4 out_color [[color(0)]];
|
||||
};
|
||||
|
||||
struct main0_in
|
||||
{
|
||||
float2 p_local [[user(locn0)]];
|
||||
float4 f_color [[user(locn1)]];
|
||||
float2 f_half_size [[user(locn2), flat]];
|
||||
float4 f_radii [[user(locn3), flat]];
|
||||
float f_half_feather [[user(locn4), flat]];
|
||||
};
|
||||
|
||||
static inline __attribute__((always_inline))
|
||||
float3 blur_sample(thread const float2& uv, constant Uniforms& _108, texture2d<float> blur_input_tex, sampler blur_input_texSmplr)
|
||||
{
|
||||
float3 color = blur_input_tex.sample(blur_input_texSmplr, uv).xyz * _108.kernel0[0].x;
|
||||
float2 axis_step = _108.direction * _108.inv_working_size;
|
||||
for (uint i = 1u; i < _108.pair_count; i++)
|
||||
{
|
||||
float w = _108.kernel0[i].x;
|
||||
float off = _108.kernel0[i].y;
|
||||
float2 step_uv = axis_step * off;
|
||||
color += (blur_input_tex.sample(blur_input_texSmplr, (uv - step_uv)).xyz * w);
|
||||
color += (blur_input_tex.sample(blur_input_texSmplr, (uv + step_uv)).xyz * w);
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
static inline __attribute__((always_inline))
|
||||
float sdRoundedBox(thread const float2& p, thread const float2& b, thread const float4& r)
|
||||
{
|
||||
float2 _36;
|
||||
if (p.x > 0.0)
|
||||
{
|
||||
_36 = r.xy;
|
||||
}
|
||||
else
|
||||
{
|
||||
_36 = r.zw;
|
||||
}
|
||||
float2 rxy = _36;
|
||||
float _50;
|
||||
if (p.y > 0.0)
|
||||
{
|
||||
_50 = rxy.x;
|
||||
}
|
||||
else
|
||||
{
|
||||
_50 = rxy.y;
|
||||
}
|
||||
float rr = _50;
|
||||
float2 q = abs(p) - b;
|
||||
if (rr == 0.0)
|
||||
{
|
||||
return fast::max(q.x, q.y);
|
||||
}
|
||||
q += float2(rr);
|
||||
return (fast::min(fast::max(q.x, q.y), 0.0) + length(fast::max(q, float2(0.0)))) - rr;
|
||||
}
|
||||
|
||||
static inline __attribute__((always_inline))
|
||||
float sdf_alpha(thread const float& d, thread const float& h)
|
||||
{
|
||||
return 1.0 - smoothstep(-h, h, d);
|
||||
}
|
||||
|
||||
fragment main0_out main0(main0_in in [[stage_in]], constant Uniforms& _108 [[buffer(0)]], texture2d<float> blur_input_tex [[texture(0)]], sampler blur_input_texSmplr [[sampler(0)]], float4 gl_FragCoord [[position]])
|
||||
{
|
||||
main0_out out = {};
|
||||
if (_108.mode == 0u)
|
||||
{
|
||||
float2 uv = gl_FragCoord.xy * _108.inv_working_size;
|
||||
float2 param = uv;
|
||||
float3 color = blur_sample(param, _108, blur_input_tex, blur_input_texSmplr);
|
||||
out.out_color = float4(color, 1.0);
|
||||
return out;
|
||||
}
|
||||
float2 param_1 = in.p_local;
|
||||
float2 param_2 = in.f_half_size;
|
||||
float4 param_3 = in.f_radii;
|
||||
float d = sdRoundedBox(param_1, param_2, param_3);
|
||||
if (d > in.f_half_feather)
|
||||
{
|
||||
discard_fragment();
|
||||
}
|
||||
float grad_magnitude = fast::max(fwidth(d), 9.9999999747524270787835121154785e-07);
|
||||
float d_n = d / grad_magnitude;
|
||||
float h_n = in.f_half_feather / grad_magnitude;
|
||||
float2 uv_1 = (gl_FragCoord.xy * _108.inv_downsample_factor) * _108.inv_working_size;
|
||||
float3 color_1 = blur_input_tex.sample(blur_input_texSmplr, uv_1).xyz;
|
||||
float3 tinted = mix(color_1, color_1 * in.f_color.xyz, float3(in.f_color.w));
|
||||
float param_4 = d_n;
|
||||
float param_5 = h_n;
|
||||
float coverage = sdf_alpha(param_4, param_5);
|
||||
out.out_color = float4(tinted * coverage, coverage);
|
||||
return out;
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,123 @@
|
||||
#pragma clang diagnostic ignored "-Wmissing-prototypes"
|
||||
#pragma clang diagnostic ignored "-Wmissing-braces"
|
||||
|
||||
#include <metal_stdlib>
|
||||
#include <simd/simd.h>
|
||||
|
||||
using namespace metal;
|
||||
|
||||
template<typename T, size_t Num>
|
||||
struct spvUnsafeArray
|
||||
{
|
||||
T elements[Num ? Num : 1];
|
||||
|
||||
thread T& operator [] (size_t pos) thread
|
||||
{
|
||||
return elements[pos];
|
||||
}
|
||||
constexpr const thread T& operator [] (size_t pos) const thread
|
||||
{
|
||||
return elements[pos];
|
||||
}
|
||||
|
||||
device T& operator [] (size_t pos) device
|
||||
{
|
||||
return elements[pos];
|
||||
}
|
||||
constexpr const device T& operator [] (size_t pos) const device
|
||||
{
|
||||
return elements[pos];
|
||||
}
|
||||
|
||||
constexpr const constant T& operator [] (size_t pos) const constant
|
||||
{
|
||||
return elements[pos];
|
||||
}
|
||||
|
||||
threadgroup T& operator [] (size_t pos) threadgroup
|
||||
{
|
||||
return elements[pos];
|
||||
}
|
||||
constexpr const threadgroup T& operator [] (size_t pos) const threadgroup
|
||||
{
|
||||
return elements[pos];
|
||||
}
|
||||
};
|
||||
|
||||
struct Uniforms
|
||||
{
|
||||
float4x4 projection;
|
||||
float dpi_scale;
|
||||
uint mode;
|
||||
float2 _pad0;
|
||||
};
|
||||
|
||||
struct Backdrop_Primitive
|
||||
{
|
||||
float4 bounds;
|
||||
float4 radii;
|
||||
float2 half_size;
|
||||
float half_feather;
|
||||
uint color;
|
||||
};
|
||||
|
||||
struct Backdrop_Primitive_1
|
||||
{
|
||||
float4 bounds;
|
||||
float4 radii;
|
||||
float2 half_size;
|
||||
float half_feather;
|
||||
uint color;
|
||||
};
|
||||
|
||||
struct Backdrop_Primitives
|
||||
{
|
||||
Backdrop_Primitive_1 primitives[1];
|
||||
};
|
||||
|
||||
constant spvUnsafeArray<float2, 6> _97 = spvUnsafeArray<float2, 6>({ float2(0.0), float2(1.0, 0.0), float2(0.0, 1.0), float2(0.0, 1.0), float2(1.0, 0.0), float2(1.0) });
|
||||
|
||||
struct main0_out
|
||||
{
|
||||
float2 p_local [[user(locn0)]];
|
||||
float4 f_color [[user(locn1)]];
|
||||
float2 f_half_size [[user(locn2)]];
|
||||
float4 f_radii [[user(locn3)]];
|
||||
float f_half_feather [[user(locn4)]];
|
||||
float4 gl_Position [[position]];
|
||||
};
|
||||
|
||||
vertex main0_out main0(constant Uniforms& _13 [[buffer(0)]], const device Backdrop_Primitives& _69 [[buffer(1)]], uint gl_VertexIndex [[vertex_id]], uint gl_InstanceIndex [[instance_id]])
|
||||
{
|
||||
main0_out out = {};
|
||||
if (_13.mode == 0u)
|
||||
{
|
||||
float2 ndc = float2((int(gl_VertexIndex) == 1) ? 3.0 : (-1.0), (int(gl_VertexIndex) == 2) ? 3.0 : (-1.0));
|
||||
out.gl_Position = float4(ndc, 0.0, 1.0);
|
||||
out.p_local = float2(0.0);
|
||||
out.f_color = float4(0.0);
|
||||
out.f_half_size = float2(0.0);
|
||||
out.f_radii = float4(0.0);
|
||||
out.f_half_feather = 0.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
Backdrop_Primitive p;
|
||||
p.bounds = _69.primitives[int(gl_InstanceIndex)].bounds;
|
||||
p.radii = _69.primitives[int(gl_InstanceIndex)].radii;
|
||||
p.half_size = _69.primitives[int(gl_InstanceIndex)].half_size;
|
||||
p.half_feather = _69.primitives[int(gl_InstanceIndex)].half_feather;
|
||||
p.color = _69.primitives[int(gl_InstanceIndex)].color;
|
||||
float2 corner = _97[int(gl_VertexIndex)];
|
||||
float2 world_pos = mix(p.bounds.xy, p.bounds.zw, corner);
|
||||
float2 center = (p.bounds.xy + p.bounds.zw) * 0.5;
|
||||
out.p_local = (world_pos - center) * _13.dpi_scale;
|
||||
out.f_color = unpack_unorm4x8_to_float(p.color);
|
||||
out.f_half_size = p.half_size;
|
||||
out.f_radii = p.radii;
|
||||
out.f_half_feather = p.half_feather;
|
||||
out.gl_Position = _13.projection * float4(world_pos * _13.dpi_scale, 0.0, 1.0);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,47 @@
|
||||
#include <metal_stdlib>
|
||||
#include <simd/simd.h>
|
||||
|
||||
using namespace metal;
|
||||
|
||||
struct Uniforms
|
||||
{
|
||||
float2 inv_source_size;
|
||||
uint downsample_factor;
|
||||
uint _pad0;
|
||||
};
|
||||
|
||||
struct main0_out
|
||||
{
|
||||
float4 out_color [[color(0)]];
|
||||
};
|
||||
|
||||
fragment main0_out main0(constant Uniforms& _18 [[buffer(0)]], texture2d<float> source_tex [[texture(0)]], sampler source_texSmplr [[sampler(0)]], float4 gl_FragCoord [[position]])
|
||||
{
|
||||
main0_out out = {};
|
||||
float2 src_block_center = gl_FragCoord.xy * float(_18.downsample_factor);
|
||||
if (_18.downsample_factor == 1u)
|
||||
{
|
||||
float2 uv = src_block_center * _18.inv_source_size;
|
||||
out.out_color = source_tex.sample(source_texSmplr, uv);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_18.downsample_factor == 2u)
|
||||
{
|
||||
float2 uv_1 = src_block_center * _18.inv_source_size;
|
||||
out.out_color = source_tex.sample(source_texSmplr, uv_1);
|
||||
}
|
||||
else
|
||||
{
|
||||
float off = float(_18.downsample_factor) * 0.25;
|
||||
float2 uv_tl = (src_block_center + float2(-off, -off)) * _18.inv_source_size;
|
||||
float2 uv_tr = (src_block_center + float2(off, -off)) * _18.inv_source_size;
|
||||
float2 uv_bl = (src_block_center + float2(-off, off)) * _18.inv_source_size;
|
||||
float2 uv_br = (src_block_center + float2(off)) * _18.inv_source_size;
|
||||
float4 c = ((source_tex.sample(source_texSmplr, uv_tl) + source_tex.sample(source_texSmplr, uv_tr)) + source_tex.sample(source_texSmplr, uv_bl)) + source_tex.sample(source_texSmplr, uv_br);
|
||||
out.out_color = c * 0.25;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,18 @@
|
||||
#include <metal_stdlib>
|
||||
#include <simd/simd.h>
|
||||
|
||||
using namespace metal;
|
||||
|
||||
struct main0_out
|
||||
{
|
||||
float4 gl_Position [[position]];
|
||||
};
|
||||
|
||||
vertex main0_out main0(uint gl_VertexIndex [[vertex_id]])
|
||||
{
|
||||
main0_out out = {};
|
||||
float2 ndc = float2((int(gl_VertexIndex) == 1) ? 3.0 : (-1.0), (int(gl_VertexIndex) == 2) ? 3.0 : (-1.0));
|
||||
out.gl_Position = float4(ndc, 0.0, 1.0);
|
||||
return out;
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -24,8 +24,8 @@ struct main0_in
|
||||
float4 f_params [[user(locn2)]];
|
||||
float4 f_params2 [[user(locn3)]];
|
||||
uint f_flags [[user(locn4)]];
|
||||
uint f_rotation_sc [[user(locn5)]];
|
||||
uint4 f_uv_or_effects [[user(locn6)]];
|
||||
float4 f_uv_rect [[user(locn6), flat]];
|
||||
uint4 f_effects [[user(locn7)]];
|
||||
};
|
||||
|
||||
static inline __attribute__((always_inline))
|
||||
@@ -109,11 +109,6 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
|
||||
float h = 0.5;
|
||||
float2 half_size = in.f_params.xy;
|
||||
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)
|
||||
{
|
||||
float4 corner_radii = float4(in.f_params.zw, in.f_params2.xy);
|
||||
@@ -163,16 +158,16 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
|
||||
{
|
||||
float d_start = dot(p_local, n_start);
|
||||
float d_end = dot(p_local, n_end);
|
||||
float _372;
|
||||
float _338;
|
||||
if (arc_bits == 1u)
|
||||
{
|
||||
_372 = fast::max(d_start, d_end);
|
||||
_338 = fast::max(d_start, d_end);
|
||||
}
|
||||
else
|
||||
{
|
||||
_372 = fast::min(d_start, d_end);
|
||||
_338 = fast::min(d_start, d_end);
|
||||
}
|
||||
float d_wedge = _372;
|
||||
float d_wedge = _338;
|
||||
d = fast::max(d, d_wedge);
|
||||
}
|
||||
half_size = float2(outer);
|
||||
@@ -187,7 +182,7 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
|
||||
if ((flags & 2u) != 0u)
|
||||
{
|
||||
float4 gradient_start = in.f_color;
|
||||
float4 gradient_end = unpack_unorm4x8_to_float(in.f_uv_or_effects.x);
|
||||
float4 gradient_end = unpack_unorm4x8_to_float(in.f_effects.x);
|
||||
if ((flags & 4u) != 0u)
|
||||
{
|
||||
float t_1 = length(p_local / half_size);
|
||||
@@ -198,7 +193,7 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
|
||||
}
|
||||
else
|
||||
{
|
||||
float2 direction = float2(as_type<half2>(in.f_uv_or_effects.z));
|
||||
float2 direction = float2(as_type<half2>(in.f_effects.z));
|
||||
float t_2 = (dot(p_local / half_size, direction) * 0.5) + 0.5;
|
||||
float4 param_11 = gradient_start;
|
||||
float4 param_12 = gradient_end;
|
||||
@@ -210,7 +205,7 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
|
||||
{
|
||||
if ((flags & 1u) != 0u)
|
||||
{
|
||||
float4 uv_rect = as_type<float4>(in.f_uv_or_effects);
|
||||
float4 uv_rect = in.f_uv_rect;
|
||||
float2 local_uv = ((p_local / half_size) * 0.5) + float2(0.5);
|
||||
float2 uv = mix(uv_rect.xy, uv_rect.zw, local_uv);
|
||||
shape_color = in.f_color * tex.sample(texSmplr, uv);
|
||||
@@ -222,8 +217,8 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
|
||||
}
|
||||
if ((flags & 8u) != 0u)
|
||||
{
|
||||
float4 ol_color = unpack_unorm4x8_to_float(in.f_uv_or_effects.y);
|
||||
float ol_width = float2(as_type<half2>(in.f_uv_or_effects.w)).x / grad_magnitude;
|
||||
float4 ol_color = unpack_unorm4x8_to_float(in.f_effects.y);
|
||||
float ol_width = float2(as_type<half2>(in.f_effects.w)).x / grad_magnitude;
|
||||
float param_14 = d;
|
||||
float param_15 = h;
|
||||
float fill_cov = sdf_alpha(param_14, param_15);
|
||||
|
||||
Binary file not shown.
@@ -10,7 +10,7 @@ struct Uniforms
|
||||
uint mode;
|
||||
};
|
||||
|
||||
struct Primitive
|
||||
struct Base_2D_Primitive
|
||||
{
|
||||
float4 bounds;
|
||||
uint color;
|
||||
@@ -19,10 +19,11 @@ struct Primitive
|
||||
float _pad;
|
||||
float4 params;
|
||||
float4 params2;
|
||||
uint4 uv_or_effects;
|
||||
float4 uv_rect;
|
||||
uint4 effects;
|
||||
};
|
||||
|
||||
struct Primitive_1
|
||||
struct Base_2D_Primitive_1
|
||||
{
|
||||
float4 bounds;
|
||||
uint color;
|
||||
@@ -31,12 +32,13 @@ struct Primitive_1
|
||||
float _pad;
|
||||
float4 params;
|
||||
float4 params2;
|
||||
uint4 uv_or_effects;
|
||||
float4 uv_rect;
|
||||
uint4 effects;
|
||||
};
|
||||
|
||||
struct Primitives
|
||||
struct Base_2D_Primitives
|
||||
{
|
||||
Primitive_1 primitives[1];
|
||||
Base_2D_Primitive_1 primitives[1];
|
||||
};
|
||||
|
||||
struct main0_out
|
||||
@@ -46,8 +48,8 @@ struct main0_out
|
||||
float4 f_params [[user(locn2)]];
|
||||
float4 f_params2 [[user(locn3)]];
|
||||
uint f_flags [[user(locn4)]];
|
||||
uint f_rotation_sc [[user(locn5)]];
|
||||
uint4 f_uv_or_effects [[user(locn6)]];
|
||||
float4 f_uv_rect [[user(locn6)]];
|
||||
uint4 f_effects [[user(locn7)]];
|
||||
float4 gl_Position [[position]];
|
||||
};
|
||||
|
||||
@@ -58,7 +60,7 @@ struct main0_in
|
||||
float4 v_color [[attribute(2)]];
|
||||
};
|
||||
|
||||
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]])
|
||||
vertex main0_out main0(main0_in in [[stage_in]], constant Uniforms& _12 [[buffer(0)]], const device Base_2D_Primitives& _75 [[buffer(1)]], uint gl_InstanceIndex [[instance_id]])
|
||||
{
|
||||
main0_out out = {};
|
||||
if (_12.mode == 0u)
|
||||
@@ -68,13 +70,13 @@ vertex main0_out main0(main0_in in [[stage_in]], constant Uniforms& _12 [[buffer
|
||||
out.f_params = float4(0.0);
|
||||
out.f_params2 = float4(0.0);
|
||||
out.f_flags = 0u;
|
||||
out.f_rotation_sc = 0u;
|
||||
out.f_uv_or_effects = uint4(0u);
|
||||
out.f_uv_rect = float4(0.0);
|
||||
out.f_effects = uint4(0u);
|
||||
out.gl_Position = _12.projection * float4(in.v_position * _12.dpi_scale, 0.0, 1.0);
|
||||
}
|
||||
else
|
||||
{
|
||||
Primitive p;
|
||||
Base_2D_Primitive p;
|
||||
p.bounds = _75.primitives[int(gl_InstanceIndex)].bounds;
|
||||
p.color = _75.primitives[int(gl_InstanceIndex)].color;
|
||||
p.flags = _75.primitives[int(gl_InstanceIndex)].flags;
|
||||
@@ -82,17 +84,25 @@ vertex main0_out main0(main0_in in [[stage_in]], constant Uniforms& _12 [[buffer
|
||||
p._pad = _75.primitives[int(gl_InstanceIndex)]._pad;
|
||||
p.params = _75.primitives[int(gl_InstanceIndex)].params;
|
||||
p.params2 = _75.primitives[int(gl_InstanceIndex)].params2;
|
||||
p.uv_or_effects = _75.primitives[int(gl_InstanceIndex)].uv_or_effects;
|
||||
p.uv_rect = _75.primitives[int(gl_InstanceIndex)].uv_rect;
|
||||
p.effects = _75.primitives[int(gl_InstanceIndex)].effects;
|
||||
float2 corner = in.v_position;
|
||||
float2 world_pos = mix(p.bounds.xy, p.bounds.zw, corner);
|
||||
float2 center = (p.bounds.xy + p.bounds.zw) * 0.5;
|
||||
float2 local = (world_pos - center) * _12.dpi_scale;
|
||||
uint flags = (p.flags >> 8u) & 255u;
|
||||
if ((flags & 16u) != 0u)
|
||||
{
|
||||
float2 sc = float2(as_type<half2>(p.rotation_sc));
|
||||
local = float2((sc.y * local.x) + (sc.x * local.y), ((-sc.x) * local.x) + (sc.y * local.y));
|
||||
}
|
||||
out.f_color = unpack_unorm4x8_to_float(p.color);
|
||||
out.f_local_or_uv = (world_pos - center) * _12.dpi_scale;
|
||||
out.f_local_or_uv = local;
|
||||
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.f_uv_rect = p.uv_rect;
|
||||
out.f_effects = p.effects;
|
||||
out.gl_Position = _12.projection * float4(world_pos * _12.dpi_scale, 0.0, 1.0);
|
||||
}
|
||||
return out;
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,156 @@
|
||||
#version 450 core
|
||||
|
||||
// Unified backdrop blur fragment shader.
|
||||
// Handles both H-blur (mode 0, blurs the ¼-resolution downsample texture into
|
||||
// the ¼-resolution h_blur texture) and V-blur+composite (mode 1, blurs h_blur
|
||||
// vertically, masks via RRect SDF, applies tint, composites outline, and writes
|
||||
// to the main render target with premultiplied alpha).
|
||||
//
|
||||
// Following RAD's pattern, V-mode replaces a separate composite pass: the SDF
|
||||
// discard limits V-blur work to the masked region, and the per-primitive tint
|
||||
// is folded in. Output blends with the main render target 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.
|
||||
//
|
||||
// Splitting V-blur out of the composite pass (an earlier version combined them) was needed
|
||||
// to avoid a horizontal-vs-vertical asymmetry artifact: when the 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. Running V-blur as
|
||||
// its own working→working pass (matching H's structure exactly) restores symmetry.
|
||||
|
||||
const uint MAX_KERNEL_PAIRS = 32;
|
||||
|
||||
// --- Inputs from vertex shader ---
|
||||
layout(location = 0) in vec2 p_local;
|
||||
layout(location = 1) in mediump vec4 f_color;
|
||||
layout(location = 2) flat in vec2 f_half_size;
|
||||
layout(location = 3) flat in vec4 f_radii;
|
||||
layout(location = 4) flat in float f_half_feather;
|
||||
|
||||
// --- Output ---
|
||||
layout(location = 0) out vec4 out_color;
|
||||
|
||||
// --- Sampler ---
|
||||
// Mode 0: bound to downsample_texture. Mode 1: bound to h_blur_texture.
|
||||
layout(set = 2, binding = 0) uniform sampler2D blur_input_tex;
|
||||
|
||||
// --- Uniforms (set 3) ---
|
||||
// Per-bracket-substage. `mode` matches the vertex shader's mode (0 = H, 1 = V).
|
||||
// `direction` selects the kernel axis for blur offsets.
|
||||
// `kernel` holds the per-sigma weight/offset pairs computed CPU-side using the
|
||||
// linear-sampling pair adjustment (RAD/Rákos).
|
||||
layout(set = 3, binding = 0) uniform Uniforms {
|
||||
vec2 inv_working_size; // 1.0 / working-resolution texture dimensions
|
||||
uint pair_count; // number of (weight, offset) pairs; pair[0] is the center
|
||||
uint mode; // 0 = H-blur, 1 = V-composite
|
||||
vec2 direction; // (1,0) for H, (0,1) for V — multiplied into the kernel offset
|
||||
float inv_downsample_factor; // 1.0 / downsample_factor (mode 1 only; mode 0 ignores)
|
||||
float _pad0;
|
||||
vec4 kernel[MAX_KERNEL_PAIRS]; // .x = weight (paired-sum for idx>0), .y = offset (texels)
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// ----- SDF helper --------------------
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
float sdRoundedBox(vec2 p, vec2 b, vec4 r) {
|
||||
vec2 rxy = (p.x > 0.0) ? r.xy : r.zw;
|
||||
float rr = (p.y > 0.0) ? rxy.x : rxy.y;
|
||||
vec2 q = abs(p) - b;
|
||||
if (rr == 0.0) {
|
||||
return max(q.x, q.y);
|
||||
}
|
||||
q += rr;
|
||||
return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - rr;
|
||||
}
|
||||
|
||||
float sdf_alpha(float d, float h) {
|
||||
return 1.0 - smoothstep(-h, h, d);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// ----- Blur sample loop --------------
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
vec3 blur_sample(vec2 uv) {
|
||||
vec3 color = kernel[0].x * texture(blur_input_tex, uv).rgb;
|
||||
|
||||
// Per-pair offset in texel space, projected onto the active axis.
|
||||
vec2 axis_step = direction * inv_working_size;
|
||||
|
||||
for (uint i = 1u; i < pair_count; i += 1u) {
|
||||
float w = kernel[i].x;
|
||||
float off = kernel[i].y;
|
||||
vec2 step_uv = off * axis_step;
|
||||
color += w * texture(blur_input_tex, uv - step_uv).rgb;
|
||||
color += w * texture(blur_input_tex, uv + step_uv).rgb;
|
||||
}
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// ----- Main --------------------------
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
void main() {
|
||||
if (mode == 0u) {
|
||||
// ---- Mode 0: 1D separable blur (used for both H-pass and V-pass).
|
||||
// gl_FragCoord is in working-res target pixel space; sample the previous working-res
|
||||
// texture along `direction` with the kernel.
|
||||
vec2 uv = gl_FragCoord.xy * inv_working_size;
|
||||
vec3 color = blur_sample(uv);
|
||||
out_color = vec4(color, 1.0);
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- Mode 1: composite per-primitive.
|
||||
// RRect SDF — early discard for fragments well outside the masked region.
|
||||
float d = sdRoundedBox(p_local, f_half_size, f_radii);
|
||||
if (d > f_half_feather) {
|
||||
discard;
|
||||
}
|
||||
|
||||
// fwidth-based normalization for AA (matches main pipeline approach).
|
||||
float grad_magnitude = max(fwidth(d), 1e-6);
|
||||
float d_n = d / grad_magnitude;
|
||||
float h_n = f_half_feather / grad_magnitude;
|
||||
|
||||
// Sample the fully-blurred working-res texture. gl_FragCoord is full-res; convert to
|
||||
// working-res UV via inv_downsample_factor. No kernel is applied — the H+V blur passes
|
||||
// already produced the final blurred image; this is just an upsample + tint.
|
||||
vec2 uv = (gl_FragCoord.xy * inv_downsample_factor) * inv_working_size;
|
||||
vec3 color = texture(blur_input_tex, uv).rgb;
|
||||
|
||||
// Tint composition (Option B semantics): 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 without re-introducing the bug
|
||||
// where mid-panel pixels became semi-transparent.
|
||||
mediump vec3 tinted = mix(color, color * f_color.rgb, f_color.a);
|
||||
mediump float coverage = sdf_alpha(d_n, h_n);
|
||||
out_color = vec4(tinted * coverage, coverage);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
#version 450 core
|
||||
|
||||
// Unified backdrop blur vertex shader.
|
||||
// Handles both H-blur (fullscreen triangle, mode 0) and V-blur+composite (instanced
|
||||
// unit-quad over Backdrop_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 layer-bracket to the work region (union
|
||||
// AABB of backdrop primitives + 3*max_sigma, clamped to swapchain bounds). Mode 1
|
||||
// renders into the main render target with the screen-space orthographic projection;
|
||||
// the per-primitive bounds drive the quad in screen space.
|
||||
//
|
||||
// Backdrop primitives have NO rotation — backdrop sampling is in screen space, so
|
||||
// a rotated mask over a stationary blur sample would look wrong.
|
||||
|
||||
// --- Outputs to fragment shader ---
|
||||
// p_local: shape-local position in physical pixels (origin at shape center).
|
||||
// Only meaningful in mode 1 (V-composite). Zero-init for mode 0.
|
||||
layout(location = 0) out vec2 p_local;
|
||||
// f_color: tint, unpacked from primitive.color. Only meaningful in mode 1.
|
||||
layout(location = 1) out mediump vec4 f_color;
|
||||
// f_half_size: RRect half extents in physical pixels (mode 1 only).
|
||||
layout(location = 2) flat out vec2 f_half_size;
|
||||
// f_radii: per-corner radii in physical pixels (mode 1 only).
|
||||
layout(location = 3) flat out vec4 f_radii;
|
||||
// f_half_feather: SDF anti-aliasing feather (mode 1 only).
|
||||
layout(location = 4) flat out float f_half_feather;
|
||||
|
||||
// --- Uniforms (set 1) ---
|
||||
// Backdrop pipeline's own uniform block — distinct from the main pipeline's
|
||||
// Vertex_Uniforms. `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;
|
||||
};
|
||||
|
||||
// --- Backdrop 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 Backdrop_Primitive declared in
|
||||
// levlib/draw/pipeline_2d_backdrop.odin; keep both in sync.
|
||||
//
|
||||
// Backdrop primitives are tint-only in v1: outline is intentionally absent.
|
||||
// Future specialized effects (e.g. liquid-glass-style edges) would be a
|
||||
// dedicated primitive type with its own pipeline rather than a flag bit here.
|
||||
struct Backdrop_Primitive {
|
||||
vec4 bounds; // 0-15: min_xy, max_xy (world-space)
|
||||
vec4 radii; // 16-31: per-corner radii (physical px)
|
||||
vec2 half_size; // 32-39: RRect half extents (physical px)
|
||||
float half_feather; // 40-43: SDF anti-aliasing feather (physical px)
|
||||
uint color; // 44-47: tint, packed RGBA u8x4
|
||||
};
|
||||
|
||||
layout(std430, set = 0, binding = 0) readonly buffer Backdrop_Primitives {
|
||||
Backdrop_Primitive primitives[];
|
||||
};
|
||||
|
||||
void main() {
|
||||
if (mode == 0u) {
|
||||
// ---- Mode 0: H-blur fullscreen triangle ----
|
||||
// gl_VertexIndex 0 -> ( -1, -1)
|
||||
// gl_VertexIndex 1 -> ( 3, -1)
|
||||
// gl_VertexIndex 2 -> ( -1, 3)
|
||||
vec2 ndc = vec2(
|
||||
(gl_VertexIndex == 1) ? 3.0 : -1.0,
|
||||
(gl_VertexIndex == 2) ? 3.0 : -1.0);
|
||||
gl_Position = vec4(ndc, 0.0, 1.0);
|
||||
|
||||
// Mode 0 doesn't read the per-primitive varyings; zero-init for safety.
|
||||
p_local = vec2(0.0);
|
||||
f_color = vec4(0.0);
|
||||
f_half_size = vec2(0.0);
|
||||
f_radii = vec4(0.0);
|
||||
f_half_feather = 0.0;
|
||||
} else {
|
||||
// ---- Mode 1: V-composite instanced unit-quad over Backdrop_Primitive ----
|
||||
Backdrop_Primitive p = primitives[gl_InstanceIndex];
|
||||
|
||||
// Unit-quad corners for TRIANGLELIST (2 triangles, 6 vertices):
|
||||
// index 0 -> (0,0) index 3 -> (0,1)
|
||||
// index 1 -> (1,0) index 4 -> (1,0)
|
||||
// index 2 -> (0,1) index 5 -> (1,1)
|
||||
vec2 quad_corners[6] = vec2[6](
|
||||
vec2(0.0, 0.0), vec2(1.0, 0.0), vec2(0.0, 1.0),
|
||||
vec2(0.0, 1.0), vec2(1.0, 0.0), vec2(1.0, 1.0));
|
||||
vec2 corner = quad_corners[gl_VertexIndex];
|
||||
|
||||
vec2 world_pos = mix(p.bounds.xy, p.bounds.zw, corner);
|
||||
vec2 center = 0.5 * (p.bounds.xy + p.bounds.zw);
|
||||
|
||||
// Shape-local position in physical pixels (no rotation for backdrops).
|
||||
p_local = (world_pos - center) * dpi_scale;
|
||||
|
||||
f_color = unpackUnorm4x8(p.color);
|
||||
f_half_size = p.half_size;
|
||||
f_radii = p.radii;
|
||||
f_half_feather = p.half_feather;
|
||||
|
||||
gl_Position = projection * vec4(world_pos * dpi_scale, 0.0, 1.0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
#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, 4, 8, or 16. 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 (factor)×(factor) source block. We use 4 bilinear taps,
|
||||
// each at the shared corner of a (factor/2)×(factor/2) sub-block. Each tap reads
|
||||
// 4 source pixels uniformly; combined, the 4 taps sample 16 source pixels arranged
|
||||
// uniformly across the block. This is an approximation of a true (factor)² box
|
||||
// filter — exact at factor=4 (16 pixels = full coverage), undersampled at factor=8
|
||||
// (16 pixels of 64) and factor=16 (16 of 256). Flutter uses a richer 13-tap COD-
|
||||
// style downsample shader at high factors; we accept the simpler 4-tap pattern
|
||||
// for now since the high-factor cases come with large kernels that mask any
|
||||
// residual aliasing.
|
||||
//
|
||||
// The viewport+scissor are set by the CPU to limit output to the layer's work region in
|
||||
// working-texture coords (work_region_phys / factor), clamped to the texture bounds.
|
||||
|
||||
layout(set = 3, binding = 0) uniform Uniforms {
|
||||
vec2 inv_source_size; // 1.0 / source_texture pixel dimensions
|
||||
uint downsample_factor; // 1, 2, 4, 8, or 16
|
||||
uint _pad0;
|
||||
};
|
||||
|
||||
layout(set = 2, binding = 0) uniform sampler2D source_tex;
|
||||
|
||||
layout(location = 0) out vec4 out_color;
|
||||
|
||||
void main() {
|
||||
// Output pixel index (i): gl_FragCoord.xy - 0.5. Source-pixel block top-left for this
|
||||
// output: i * factor. Center of the block: i*factor + factor/2 = gl_FragCoord.xy * factor.
|
||||
vec2 src_block_center = gl_FragCoord.xy * float(downsample_factor);
|
||||
|
||||
if (downsample_factor == 1u) {
|
||||
// Identity copy. UV at src_block_center hits the source pixel center directly.
|
||||
vec2 uv = src_block_center * inv_source_size;
|
||||
out_color = texture(source_tex, uv);
|
||||
} else if (downsample_factor == 2u) {
|
||||
// Single tap at the shared corner of the 2×2 source block; one bilinear sample reads
|
||||
// all 4 source pixels with equal 0.25 weights — uniform 2×2 box filter for free.
|
||||
vec2 uv = src_block_center * inv_source_size;
|
||||
out_color = texture(source_tex, uv);
|
||||
} else {
|
||||
// Four taps at offsets ±(factor/4) from the block center. Each tap lands on a corner
|
||||
// shared by 4 source pixels of a (factor/2)×(factor/2) sub-block (equivalent at the
|
||||
// bilinear level), giving a 4-tap = 16-source-pixel uniform sample of the block.
|
||||
float off = float(downsample_factor) * 0.25;
|
||||
vec2 uv_tl = (src_block_center + vec2(-off, -off)) * inv_source_size;
|
||||
vec2 uv_tr = (src_block_center + vec2( off, -off)) * inv_source_size;
|
||||
vec2 uv_bl = (src_block_center + vec2(-off, off)) * inv_source_size;
|
||||
vec2 uv_br = (src_block_center + vec2( off, off)) * inv_source_size;
|
||||
vec4 c = texture(source_tex, uv_tl)
|
||||
+ texture(source_tex, uv_tr)
|
||||
+ texture(source_tex, uv_bl)
|
||||
+ texture(source_tex, uv_br);
|
||||
out_color = c * 0.25;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#version 450 core
|
||||
|
||||
// Fullscreen-triangle vertex shader for the backdrop downsample and H-blur sub-passes.
|
||||
// Emits a single triangle covering NDC [-1,1]^2; the rasterizer clips edges outside.
|
||||
// No vertex buffer; uses gl_VertexIndex to pick corners.
|
||||
//
|
||||
// The CPU sets the viewport (and matching scissor) per layer-bracket to limit work to
|
||||
// the union AABB of the layer's backdrop primitives, expanded by 3*max_sigma and
|
||||
// clamped to swapchain bounds. The fragment shader uses gl_FragCoord (absolute pixel
|
||||
// space in the bound target) plus an inv-size uniform to compute its own UVs — see
|
||||
// each fragment shader for the per-pass sampling math.
|
||||
|
||||
void main() {
|
||||
// gl_VertexIndex 0 -> ( -1, -1)
|
||||
// gl_VertexIndex 1 -> ( 3, -1)
|
||||
// gl_VertexIndex 2 -> ( -1, 3)
|
||||
vec2 ndc = vec2(
|
||||
(gl_VertexIndex == 1) ? 3.0 : -1.0,
|
||||
(gl_VertexIndex == 2) ? 3.0 : -1.0);
|
||||
gl_Position = vec4(ndc, 0.0, 1.0);
|
||||
}
|
||||
@@ -6,8 +6,8 @@ layout(location = 1) in vec2 f_local_or_uv;
|
||||
layout(location = 2) in vec4 f_params;
|
||||
layout(location = 3) in vec4 f_params2;
|
||||
layout(location = 4) flat in uint f_flags;
|
||||
layout(location = 5) flat in uint f_rotation_sc;
|
||||
layout(location = 6) flat in uvec4 f_uv_or_effects;
|
||||
layout(location = 6) flat in vec4 f_uv_rect;
|
||||
layout(location = 7) flat in uvec4 f_effects;
|
||||
|
||||
// --- Output ---
|
||||
layout(location = 0) out vec4 out_color;
|
||||
@@ -83,16 +83,7 @@ void main() {
|
||||
float h = 0.5; // half-feather width; overwritten per shape kind
|
||||
vec2 half_size = f_params.xy; // used by RRect and as reference size for gradients
|
||||
|
||||
vec2 p_local = f_local_or_uv;
|
||||
|
||||
// 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);
|
||||
}
|
||||
vec2 p_local = f_local_or_uv; // arrives rotated; vertex shader handled .Rotated
|
||||
|
||||
if (kind == 1u) {
|
||||
// RRect — half_feather in params2.z
|
||||
@@ -151,7 +142,7 @@ void main() {
|
||||
if ((flags & 2u) != 0u) {
|
||||
// Gradient active (bit 1)
|
||||
mediump vec4 gradient_start = f_color;
|
||||
mediump vec4 gradient_end = unpackUnorm4x8(f_uv_or_effects.x);
|
||||
mediump vec4 gradient_end = unpackUnorm4x8(f_effects.x);
|
||||
|
||||
if ((flags & 4u) != 0u) {
|
||||
// Radial gradient (bit 2): t from distance to center
|
||||
@@ -159,13 +150,13 @@ void main() {
|
||||
shape_color = gradient_2color(gradient_start, gradient_end, t);
|
||||
} else {
|
||||
// Linear gradient: direction pre-computed on CPU as (cos, sin) f16 pair
|
||||
vec2 direction = unpackHalf2x16(f_uv_or_effects.z);
|
||||
vec2 direction = unpackHalf2x16(f_effects.z);
|
||||
mediump float t = dot(p_local / half_size, direction) * 0.5 + 0.5;
|
||||
shape_color = gradient_2color(gradient_start, gradient_end, t);
|
||||
}
|
||||
} else if ((flags & 1u) != 0u) {
|
||||
// Textured (bit 0) — RRect only in practice
|
||||
vec4 uv_rect = uintBitsToFloat(f_uv_or_effects);
|
||||
// Textured (bit 0)
|
||||
vec4 uv_rect = f_uv_rect;
|
||||
vec2 local_uv = p_local / half_size * 0.5 + 0.5;
|
||||
vec2 uv = mix(uv_rect.xy, uv_rect.zw, local_uv);
|
||||
shape_color = f_color * texture(tex, uv);
|
||||
@@ -180,9 +171,9 @@ void main() {
|
||||
// AA at d=ol_width. The outline band's coverage is total_cov - fill_cov.
|
||||
// Output is premultiplied: blend state is ONE, ONE_MINUS_SRC_ALPHA.
|
||||
if ((flags & 8u) != 0u) {
|
||||
mediump vec4 ol_color = unpackUnorm4x8(f_uv_or_effects.y);
|
||||
// Outline width in f_uv_or_effects.w (low f16 half)
|
||||
float ol_width = unpackHalf2x16(f_uv_or_effects.w).x / grad_magnitude;
|
||||
mediump vec4 ol_color = unpackUnorm4x8(f_effects.y);
|
||||
// Outline width in f_effects.w (low f16 half)
|
||||
float ol_width = unpackHalf2x16(f_effects.w).x / grad_magnitude;
|
||||
|
||||
float fill_cov = sdf_alpha(d, h);
|
||||
float total_cov = sdf_alpha(d - ol_width, h);
|
||||
|
||||
@@ -11,8 +11,9 @@ layout(location = 1) out vec2 f_local_or_uv;
|
||||
layout(location = 2) out vec4 f_params;
|
||||
layout(location = 3) out vec4 f_params2;
|
||||
layout(location = 4) flat out uint f_flags;
|
||||
layout(location = 5) flat out uint f_rotation_sc;
|
||||
layout(location = 6) flat out uvec4 f_uv_or_effects;
|
||||
|
||||
layout(location = 6) flat out vec4 f_uv_rect;
|
||||
layout(location = 7) flat out uvec4 f_effects;
|
||||
|
||||
// ---------- Uniforms (single block — avoids spirv-cross reordering on Metal) ----------
|
||||
layout(set = 1, binding = 0) uniform Uniforms {
|
||||
@@ -22,7 +23,10 @@ layout(set = 1, binding = 0) uniform Uniforms {
|
||||
};
|
||||
|
||||
// ---------- SDF primitive storage buffer ----------
|
||||
struct Primitive {
|
||||
// Mirrors the CPU-side Base_2D_Primitive in pipeline_2d_base.odin. Named with the
|
||||
// pipeline prefix so a project-wide grep on the type name matches both the GLSL
|
||||
// declaration and the Odin declaration.
|
||||
struct Base_2D_Primitive {
|
||||
vec4 bounds; // 0-15
|
||||
uint color; // 16-19
|
||||
uint flags; // 20-23
|
||||
@@ -30,11 +34,12 @@ struct Primitive {
|
||||
float _pad; // 28-31
|
||||
vec4 params; // 32-47
|
||||
vec4 params2; // 48-63
|
||||
uvec4 uv_or_effects; // 64-79
|
||||
vec4 uv_rect; // 64-79: texture UV coordinates (read when .Textured)
|
||||
uvec4 effects; // 80-95: gradient/outline parameters (read when .Gradient/.Outline)
|
||||
};
|
||||
|
||||
layout(std430, set = 0, binding = 0) readonly buffer Primitives {
|
||||
Primitive primitives[];
|
||||
layout(std430, set = 0, binding = 0) readonly buffer Base_2D_Primitives {
|
||||
Base_2D_Primitive primitives[];
|
||||
};
|
||||
|
||||
// ---------- Entry point ----------
|
||||
@@ -46,25 +51,39 @@ void main() {
|
||||
f_params = vec4(0.0);
|
||||
f_params2 = vec4(0.0);
|
||||
f_flags = 0u;
|
||||
f_rotation_sc = 0u;
|
||||
f_uv_or_effects = uvec4(0);
|
||||
f_uv_rect = vec4(0.0);
|
||||
f_effects = uvec4(0);
|
||||
|
||||
gl_Position = projection * vec4(v_position * dpi_scale, 0.0, 1.0);
|
||||
} else {
|
||||
// ---- Mode 1: SDF instanced quads ----
|
||||
Primitive p = primitives[gl_InstanceIndex];
|
||||
Base_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 = (world_pos - center) * dpi_scale; // shape-centered physical pixels
|
||||
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_rotation_sc = p.rotation_sc;
|
||||
f_uv_or_effects = p.uv_or_effects;
|
||||
f_uv_rect = p.uv_rect;
|
||||
f_effects = p.effects;
|
||||
|
||||
gl_Position = projection * vec4(world_pos * dpi_scale, 0.0, 1.0);
|
||||
}
|
||||
|
||||
+84
-96
@@ -52,12 +52,14 @@ emit_rectangle :: proc(x, y, width, height: f32, color: Color, vertices: []Verte
|
||||
vertices[offset + 5] = solid_vertex({x, y + height}, color)
|
||||
}
|
||||
|
||||
// Internal
|
||||
prepare_sdf_primitive_textured :: proc(
|
||||
// Internal — submit an SDF primitive with optional texture binding.
|
||||
// Replaces the old prepare_sdf_primitive and prepare_sdf_primitive_textured.
|
||||
@(private)
|
||||
prepare_sdf_primitive_ex :: proc(
|
||||
layer: ^Layer,
|
||||
prim: Primitive,
|
||||
texture_id: Texture_Id,
|
||||
sampler: Sampler_Preset,
|
||||
prim: Base_2D_Primitive,
|
||||
texture_id: Texture_Id = INVALID_TEXTURE,
|
||||
sampler: Sampler_Preset = DFT_SAMPLER,
|
||||
) {
|
||||
offset := u32(len(GLOB.tmp_primitives))
|
||||
append(&GLOB.tmp_primitives, prim)
|
||||
@@ -65,6 +67,23 @@ prepare_sdf_primitive_textured :: proc(
|
||||
append_or_extend_sub_batch(scissor, layer, .SDF, offset, 1, texture_id, sampler)
|
||||
}
|
||||
|
||||
// Internal — resolve Texture_Fill zero-initialized fields to their defaults.
|
||||
// Odin structs zero-initialize; Color{} and Rectangle{} are all-zero which is not a
|
||||
// useful tint or UV rect. This proc substitutes sensible defaults for zero values.
|
||||
@(private)
|
||||
resolve_texture_defaults :: #force_inline proc(
|
||||
tf: Texture_Fill,
|
||||
) -> (
|
||||
tint: Color,
|
||||
uv: Rectangle,
|
||||
sampler: Sampler_Preset,
|
||||
) {
|
||||
tint = tf.tint == Color{} ? DFT_TINT : tf.tint
|
||||
uv = tf.uv_rect == Rectangle{} ? DFT_UV_RECT : tf.uv_rect
|
||||
sampler = tf.sampler
|
||||
return
|
||||
}
|
||||
|
||||
//Internal
|
||||
//
|
||||
// Compute the visual center of a center-parametrized shape after applying
|
||||
@@ -89,7 +108,7 @@ rotated_aabb_half_extents :: proc(half_width, half_height, cos_angle, 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 sin/cos into the Base_2D_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))
|
||||
}
|
||||
@@ -97,7 +116,7 @@ pack_rotation_sc :: #force_inline proc(sin_angle, cos_angle: f32) -> u32 {
|
||||
|
||||
// Internal
|
||||
//
|
||||
// Build an RRect Primitive with bounds, params, and rotation computed from rectangle geometry.
|
||||
// Build an RRect Base_2D_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,
|
||||
@@ -105,7 +124,7 @@ build_rrect_primitive :: proc(
|
||||
origin: Vec2,
|
||||
rotation: f32,
|
||||
feather_px: f32,
|
||||
) -> Primitive {
|
||||
) -> Base_2D_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)
|
||||
@@ -141,7 +160,7 @@ build_rrect_primitive :: proc(
|
||||
bounds_half_height = expanded.y
|
||||
}
|
||||
|
||||
prim := Primitive {
|
||||
prim := Base_2D_Primitive {
|
||||
bounds = {
|
||||
center_x - bounds_half_width - padding,
|
||||
center_y - bounds_half_height - padding,
|
||||
@@ -165,7 +184,7 @@ build_rrect_primitive :: proc(
|
||||
|
||||
// Internal
|
||||
//
|
||||
// Build an RRect Primitive for a circle (fully-rounded square RRect).
|
||||
// Build an RRect Base_2D_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,
|
||||
@@ -173,7 +192,7 @@ build_circle_primitive :: proc(
|
||||
origin: Vec2,
|
||||
rotation: f32,
|
||||
feather_px: f32,
|
||||
) -> Primitive {
|
||||
) -> Base_2D_Primitive {
|
||||
half_feather := feather_px * 0.5
|
||||
padding := half_feather / GLOB.dpi_scaling
|
||||
dpi_scale := GLOB.dpi_scaling
|
||||
@@ -184,7 +203,7 @@ build_circle_primitive :: proc(
|
||||
actual_center = compute_pivot_center(center, origin, sin_a, cos_a)
|
||||
}
|
||||
|
||||
prim := Primitive {
|
||||
prim := Base_2D_Primitive {
|
||||
bounds = {
|
||||
actual_center.x - radius - padding,
|
||||
actual_center.y - radius - padding,
|
||||
@@ -203,7 +222,7 @@ build_circle_primitive :: proc(
|
||||
|
||||
// Internal
|
||||
//
|
||||
// Build an Ellipse Primitive with bounds, params, and rotation computed from ellipse geometry.
|
||||
// Build an Ellipse Base_2D_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,
|
||||
@@ -211,7 +230,7 @@ build_ellipse_primitive :: proc(
|
||||
origin: Vec2,
|
||||
rotation: f32,
|
||||
feather_px: f32,
|
||||
) -> Primitive {
|
||||
) -> Base_2D_Primitive {
|
||||
half_feather := feather_px * 0.5
|
||||
padding := half_feather / GLOB.dpi_scaling
|
||||
dpi_scale := GLOB.dpi_scaling
|
||||
@@ -235,7 +254,7 @@ build_ellipse_primitive :: proc(
|
||||
bound_vertical = expanded.y
|
||||
}
|
||||
|
||||
prim := Primitive {
|
||||
prim := Base_2D_Primitive {
|
||||
bounds = {
|
||||
actual_center.x - bound_horizontal - padding,
|
||||
actual_center.y - bound_vertical - padding,
|
||||
@@ -253,7 +272,7 @@ build_ellipse_primitive :: proc(
|
||||
|
||||
// Internal
|
||||
//
|
||||
// Build an NGon Primitive with bounds, params, and rotation computed from polygon geometry.
|
||||
// Build an NGon Base_2D_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,
|
||||
@@ -262,7 +281,7 @@ build_polygon_primitive :: proc(
|
||||
origin: Vec2,
|
||||
rotation: f32,
|
||||
feather_px: f32,
|
||||
) -> Primitive {
|
||||
) -> Base_2D_Primitive {
|
||||
half_feather := feather_px * 0.5
|
||||
padding := half_feather / GLOB.dpi_scaling
|
||||
dpi_scale := GLOB.dpi_scaling
|
||||
@@ -276,7 +295,7 @@ build_polygon_primitive :: proc(
|
||||
rotation_radians := math.to_radians(rotation)
|
||||
sin_rot, cos_rot := math.sincos(rotation_radians)
|
||||
|
||||
prim := Primitive {
|
||||
prim := Base_2D_Primitive {
|
||||
bounds = {
|
||||
actual_center.x - radius - padding,
|
||||
actual_center.y - radius - padding,
|
||||
@@ -295,7 +314,7 @@ build_polygon_primitive :: proc(
|
||||
|
||||
// Internal
|
||||
//
|
||||
// Build a Ring_Arc Primitive with bounds and params computed from ring/arc geometry.
|
||||
// Build a Ring_Arc Base_2D_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.
|
||||
@@ -309,7 +328,7 @@ build_ring_arc_primitive :: proc(
|
||||
rotation: f32,
|
||||
feather_px: f32,
|
||||
) -> (
|
||||
Primitive,
|
||||
Base_2D_Primitive,
|
||||
Shape_Flags,
|
||||
) {
|
||||
half_feather := feather_px * 0.5
|
||||
@@ -347,7 +366,7 @@ build_ring_arc_primitive :: proc(
|
||||
arc_flags = arc_span <= math.PI ? {.Arc_Narrow} : {.Arc_Wide}
|
||||
}
|
||||
|
||||
prim := Primitive {
|
||||
prim := Base_2D_Primitive {
|
||||
bounds = {
|
||||
actual_center.x - outer_radius - padding,
|
||||
actual_center.y - outer_radius - padding,
|
||||
@@ -365,39 +384,53 @@ build_ring_arc_primitive :: proc(
|
||||
return prim, arc_flags
|
||||
}
|
||||
|
||||
// Apply gradient and outline effects to a primitive. Sets flags, uv.effects, and expands bounds.
|
||||
// Apply brush fill and outline to a primitive, then submit it.
|
||||
// Dispatches to the correct sub-batch based on the Brush variant.
|
||||
// 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,
|
||||
apply_brush_and_outline :: proc(
|
||||
layer: ^Layer,
|
||||
prim: ^Base_2D_Primitive,
|
||||
kind: Shape_Kind,
|
||||
gradient: Gradient,
|
||||
brush: Brush,
|
||||
outline_color: Color,
|
||||
outline_width: f32,
|
||||
extra_flags: Shape_Flags = {},
|
||||
) {
|
||||
flags: Shape_Flags = extra_flags
|
||||
gradient_dir_sc: u32 = 0
|
||||
|
||||
switch g in gradient {
|
||||
// Fill — determined by the Brush variant.
|
||||
texture_id := INVALID_TEXTURE
|
||||
sampler := DFT_SAMPLER
|
||||
|
||||
switch b in brush {
|
||||
case Color: prim.color = b
|
||||
case Linear_Gradient:
|
||||
flags += {.Gradient}
|
||||
prim.uv.effects.gradient_color = g.end_color
|
||||
rad := math.to_radians(g.angle)
|
||||
prim.color = b.start_color
|
||||
prim.effects.gradient_color = b.end_color
|
||||
rad := math.to_radians(b.angle)
|
||||
sin_a, cos_a := math.sincos(rad)
|
||||
gradient_dir_sc = pack_f16_pair(f16(cos_a), f16(sin_a))
|
||||
prim.effects.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:
|
||||
prim.color = b.inner_color
|
||||
prim.effects.gradient_color = b.outer_color
|
||||
case Texture_Fill:
|
||||
flags += {.Textured}
|
||||
tint, uv, sam := resolve_texture_defaults(b)
|
||||
prim.color = tint
|
||||
prim.uv_rect = {uv.x, uv.y, uv.width, uv.height}
|
||||
texture_id = b.id
|
||||
sampler = sam
|
||||
}
|
||||
|
||||
outline_packed: u32 = 0
|
||||
// Outline — orthogonal to all Brush variants.
|
||||
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)
|
||||
prim.effects.outline_color = outline_color
|
||||
prim.effects.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
|
||||
@@ -410,9 +443,8 @@ apply_shape_effects :: proc(
|
||||
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)
|
||||
prepare_sdf_primitive_ex(layer, prim^, texture_id, sampler)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
@@ -430,8 +462,7 @@ apply_shape_effects :: proc(
|
||||
rectangle :: proc(
|
||||
layer: ^Layer,
|
||||
rect: Rectangle,
|
||||
color: Color,
|
||||
gradient: Gradient = nil,
|
||||
brush: Brush,
|
||||
outline_color: Color = {},
|
||||
outline_width: f32 = 0,
|
||||
radii: Rectangle_Radii = {},
|
||||
@@ -440,36 +471,7 @@ rectangle :: proc(
|
||||
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)
|
||||
apply_brush_and_outline(layer, &prim, .RRect, brush, outline_color, outline_width)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
@@ -488,8 +490,7 @@ circle :: proc(
|
||||
layer: ^Layer,
|
||||
center: Vec2,
|
||||
radius: f32,
|
||||
color: Color,
|
||||
gradient: Gradient = nil,
|
||||
brush: Brush,
|
||||
outline_color: Color = {},
|
||||
outline_width: f32 = 0,
|
||||
origin: Vec2 = {},
|
||||
@@ -497,9 +498,7 @@ circle :: proc(
|
||||
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)
|
||||
apply_brush_and_outline(layer, &prim, .RRect, brush, outline_color, outline_width)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
@@ -512,8 +511,7 @@ ellipse :: proc(
|
||||
layer: ^Layer,
|
||||
center: Vec2,
|
||||
radius_horizontal, radius_vertical: f32,
|
||||
color: Color,
|
||||
gradient: Gradient = nil,
|
||||
brush: Brush,
|
||||
outline_color: Color = {},
|
||||
outline_width: f32 = 0,
|
||||
origin: Vec2 = {},
|
||||
@@ -521,9 +519,7 @@ ellipse :: proc(
|
||||
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)
|
||||
apply_brush_and_outline(layer, &prim, .Ellipse, brush, outline_color, outline_width)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
@@ -538,8 +534,7 @@ polygon :: proc(
|
||||
center: Vec2,
|
||||
sides: int,
|
||||
radius: f32,
|
||||
color: Color,
|
||||
gradient: Gradient = nil,
|
||||
brush: Brush,
|
||||
outline_color: Color = {},
|
||||
outline_width: f32 = 0,
|
||||
origin: Vec2 = {},
|
||||
@@ -549,9 +544,7 @@ polygon :: proc(
|
||||
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)
|
||||
apply_brush_and_outline(layer, &prim, .NGon, brush, outline_color, outline_width)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
@@ -566,8 +559,7 @@ ring :: proc(
|
||||
layer: ^Layer,
|
||||
center: Vec2,
|
||||
inner_radius, outer_radius: f32,
|
||||
color: Color,
|
||||
gradient: Gradient = nil,
|
||||
brush: Brush,
|
||||
outline_color: Color = {},
|
||||
outline_width: f32 = 0,
|
||||
start_angle: f32 = 0,
|
||||
@@ -586,9 +578,7 @@ ring :: proc(
|
||||
rotation,
|
||||
feather_px,
|
||||
)
|
||||
prim.color = color
|
||||
apply_shape_effects(&prim, .Ring_Arc, gradient, outline_color, outline_width, arc_flags)
|
||||
prepare_sdf_primitive(layer, prim)
|
||||
apply_brush_and_outline(layer, &prim, .Ring_Arc, brush, outline_color, outline_width, arc_flags)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
@@ -600,7 +590,7 @@ ring :: proc(
|
||||
line :: proc(
|
||||
layer: ^Layer,
|
||||
start_position, end_position: Vec2,
|
||||
color: Color,
|
||||
brush: Brush,
|
||||
thickness: f32 = DFT_STROKE_THICKNESS,
|
||||
outline_color: Color = {},
|
||||
outline_width: f32 = 0,
|
||||
@@ -627,14 +617,13 @@ line :: proc(
|
||||
// Expand bounds for rotation
|
||||
bounds_half := rotated_aabb_half_extents(half_length + cap_radius, half_thickness, cos_angle, sin_angle)
|
||||
|
||||
prim := Primitive {
|
||||
prim := Base_2D_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 {
|
||||
@@ -647,15 +636,14 @@ line :: proc(
|
||||
},
|
||||
half_feather = half_feather,
|
||||
}
|
||||
apply_shape_effects(&prim, .RRect, nil, outline_color, outline_width)
|
||||
prepare_sdf_primitive(layer, prim)
|
||||
apply_brush_and_outline(layer, &prim, .RRect, brush, outline_color, outline_width)
|
||||
}
|
||||
|
||||
// Draw a line strip via decomposed SDF line segments.
|
||||
line_strip :: proc(
|
||||
layer: ^Layer,
|
||||
points: []Vec2,
|
||||
color: Color,
|
||||
brush: Brush,
|
||||
thickness: f32 = DFT_STROKE_THICKNESS,
|
||||
outline_color: Color = {},
|
||||
outline_width: f32 = 0,
|
||||
@@ -663,7 +651,7 @@ line_strip :: proc(
|
||||
) {
|
||||
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)
|
||||
line(layer, points[i], points[i + 1], brush, thickness, outline_color, outline_width, feather_px)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -14,8 +14,8 @@ Texture_Kind :: enum u8 {
|
||||
}
|
||||
|
||||
Sampler_Preset :: enum u8 {
|
||||
Nearest_Clamp,
|
||||
Linear_Clamp,
|
||||
Nearest_Clamp,
|
||||
Nearest_Repeat,
|
||||
Linear_Repeat,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user