From 16989cbb716e73961b73cdb80db361894106a881 Mon Sep 17 00:00:00 2001 From: Zachary Levy Date: Tue, 28 Apr 2026 22:12:25 -0700 Subject: [PATCH] Added backdrop effects pipeline (blur) --- .zed/tasks.json | 10 + draw/README.md | 110 +- draw/backdrop.odin | 1121 +++++++++++++++++ draw/draw.odin | 253 ++-- draw/examples/backdrop.odin | 383 ++++++ draw/examples/examples.odin | 109 +- draw/examples/hellope.odin | 22 +- draw/examples/textures.odin | 192 ++- draw/pipeline_2d_base.odin | 161 ++- .../generated/backdrop_blur.frag.metal | 118 ++ draw/shaders/generated/backdrop_blur.frag.spv | Bin 0 -> 7056 bytes .../generated/backdrop_blur.vert.metal | 123 ++ draw/shaders/generated/backdrop_blur.vert.spv | Bin 0 -> 4588 bytes .../generated/backdrop_downsample.frag.metal | 47 + .../generated/backdrop_downsample.frag.spv | Bin 0 -> 3012 bytes .../generated/backdrop_fullscreen.vert.metal | 18 + .../generated/backdrop_fullscreen.vert.spv | Bin 0 -> 1096 bytes draw/shaders/generated/base_2d.frag.metal | 27 +- draw/shaders/generated/base_2d.frag.spv | Bin 14212 -> 13528 bytes draw/shaders/generated/base_2d.vert.metal | 42 +- draw/shaders/generated/base_2d.vert.spv | Bin 5060 -> 6088 bytes draw/shaders/source/backdrop_blur.frag | 156 +++ draw/shaders/source/backdrop_blur.vert | 109 ++ draw/shaders/source/backdrop_downsample.frag | 70 + draw/shaders/source/backdrop_fullscreen.vert | 21 + draw/shaders/source/base_2d.frag | 29 +- draw/shaders/source/base_2d.vert | 43 +- draw/shapes.odin | 180 ++- draw/textures.odin | 2 +- 29 files changed, 2931 insertions(+), 415 deletions(-) create mode 100644 draw/backdrop.odin create mode 100644 draw/examples/backdrop.odin create mode 100644 draw/shaders/generated/backdrop_blur.frag.metal create mode 100644 draw/shaders/generated/backdrop_blur.frag.spv create mode 100644 draw/shaders/generated/backdrop_blur.vert.metal create mode 100644 draw/shaders/generated/backdrop_blur.vert.spv create mode 100644 draw/shaders/generated/backdrop_downsample.frag.metal create mode 100644 draw/shaders/generated/backdrop_downsample.frag.spv create mode 100644 draw/shaders/generated/backdrop_fullscreen.vert.metal create mode 100644 draw/shaders/generated/backdrop_fullscreen.vert.spv create mode 100644 draw/shaders/source/backdrop_blur.frag create mode 100644 draw/shaders/source/backdrop_blur.vert create mode 100644 draw/shaders/source/backdrop_downsample.frag create mode 100644 draw/shaders/source/backdrop_fullscreen.vert diff --git a/.zed/tasks.json b/.zed/tasks.json index 8b14508..a9be01a 100644 --- a/.zed/tasks.json +++ b/.zed/tasks.json @@ -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", diff --git a/draw/README.md b/draw/README.md index c9e8e75..879b520 100644 --- a/draw/README.md +++ b/draw/README.md @@ -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: diff --git a/draw/backdrop.odin b/draw/backdrop.odin new file mode 100644 index 0000000..78776d7 --- /dev/null +++ b/draw/backdrop.odin @@ -0,0 +1,1121 @@ +package draw + +import "core:log" +import "core:math" +import "core:mem" +import sdl "vendor:sdl3" + +// Adaptive downsample design (Flutter-style). +// +// The bracket picks a downsample factor per-sigma-group, not as a global constant. The choice +// is driven by Flutter's `CalculateScale` formula in +// impeller/entity/contents/filters/gaussian_blur_filter_contents.cc (originally from Skia's +// GrBlurUtils): downsample so that the sigma in working-resolution pixels stays in the +// 2..4 range. This keeps the kernel reach wide enough to hide high-frequency artifacts from +// the bilinear upsample at the composite, while keeping the kernel's discrete tap count +// small (≤3σ reach → ≈12 paired taps). +// +// The full table, in physical pixels (sigma_logical * dpi_scaling): +// +// sigma_phys ≤ 4 → factor = 1 (no downsample; source is sampled directly) +// sigma_phys ≤ 8 → factor = 2 +// sigma_phys > 8 → factor = 4 (capped) +// +// Capped at factor=4: master's preference for visual quality over bandwidth at the high end. +// Larger factors (8 and 16) would lose more high-frequency detail than the kernel can mask +// even with the H+V split, and the bandwidth saving is small (the work region also shrinks +// quadratically, so most of the savings are already captured at factor=4). +// +// Working textures are sized at full swapchain resolution to support factor=1. Larger factors +// just write to a smaller sub-rect via viewport-limited rendering. Memory cost: ½-res → full- +// res working textures means 4× more bytes per working texture (2 textures, RGBA8: roughly +// 16 MB at 1080p, 64 MB at 4K). On modern GPUs this is well within budget; on Mali Valhall +// SBCs it's negligible against unified-memory headroom. +// +// The shaders read the factor as a uniform. The downsample shader has three paths (factor=1 +// identity, factor=2 single bilinear tap, factor>=4 four bilinear taps with offsets scaling +// by factor/4). The V-composite mode of backdrop_blur.frag uses inv_downsample_factor to +// scale full-res frag coords down to working-res UV. + +// Maximum number of (weight, offset) pairs in a single blur kernel. Each pair represents +// the linear-sampling pair adjustment (one bilinear fetch covering two adjacent texels); +// pair[0] is the center weight with offset 0. With 32 pairs we cover up to 63 input texels +// (1 center + 31 paired symmetric taps × 2 texels each), enough for sigma values well past +// the 4..24 typical UI range. Must match MAX_KERNEL_PAIRS in shaders/source/backdrop_blur.frag. +MAX_BACKDROP_KERNEL_PAIRS :: 32 + +// Backdrop_Primitive is the GPU-side per-primitive storage layout. Mirrors the GLSL std430 +// struct in shaders/source/backdrop_blur.vert. Field order is chosen so std430 alignment +// rules pack the struct to a clean 48-byte natural layout (no implicit padding): vec4 +// members come first (16-byte aligned at any offset), then vec2, then scalars. The total is +// a multiple of 16 so the std430 array stride matches size_of(...) exactly. +// +// Backdrop primitives are RRect-only: rectangles, rounded rectangles, and circles +// (via uniform_radii) are all expressible. Rotation is intentionally omitted — backdrop +// sampling is in screen space, so a rotated mask over a stationary blur sample would look +// visually wrong. iOS, CSS backdrop-filter, and Flutter BackdropFilter all enforce this +// implicitly; we enforce it explicitly by leaving no rotation field. +// +// Outline is also intentionally omitted. A specialized edge effect (e.g. liquid-glass-style +// refraction outlines) would be implemented as a dedicated primitive type with its own +// pipeline rather than tacked onto this one as a flag bit. +Backdrop_Primitive :: struct { + bounds: [4]f32, // 0: 16 — world-space quad (min_xy, max_xy) + radii: [4]f32, // 16: 16 — per-corner radii in physical pixels (BR, TR, BL, TL) + half_size: [2]f32, // 32: 8 — RRect half extents (physical px) + half_feather: f32, // 40: 4 — feather_px * 0.5 (SDF anti-aliasing) + color: Color, // 44: 4 — tint, packed RGBA u8x4 +} +#assert(size_of(Backdrop_Primitive) == 48) + +// --------------------------------------------------------------------------------------------------------------------- +// ----- Uniform blocks ---------------- +// --------------------------------------------------------------------------------------------------------------------- + +// Vertex uniforms for the unified blur PSO (mode 0 = H-blur, mode 1 = V-composite). +// Matches the GLSL Uniforms block in shaders/source/backdrop_blur.vert. The downsample +// PSO has no vertex uniforms. +Backdrop_Vert_Uniforms :: struct { + projection: matrix[4, 4]f32, // 0: 64 — screen-space ortho (mode 1 only; mode 0 ignores) + dpi_scale: f32, // 64: 4 + mode: u32, // 68: 4 — 0 = H-blur fullscreen tri; 1 = V-composite instanced quads + _pad0: [2]f32, // 72: 8 — std140 vec4 alignment pad +} + +// Fragment uniforms for the downsample PSO. Matches Uniforms block in +// shaders/source/backdrop_downsample.frag. +Backdrop_Downsample_Frag_Uniforms :: struct { + inv_source_size: [2]f32, // 0: 8 — 1.0 / source_texture pixel dimensions (full-res) + downsample_factor: u32, // 8: 4 — 2 or 4 (selects 1-tap vs 4-tap path in shader) + _pad0: u32, // 12: 4 +} + +// Fragment uniforms for the unified blur PSO (mode 0 + mode 1). Matches the GLSL Uniforms +// block in shaders/source/backdrop_blur.frag. The kernel array holds the linear-sampling +// pair coefficients computed CPU-side via `compute_blur_kernel`. +Backdrop_Frag_Uniforms :: struct { + inv_working_size: [2]f32, // 0: 8 — 1.0 / working-resolution texture dimensions + pair_count: u32, // 8: 4 — number of (weight, offset) pairs; pair[0] is center + mode: u32, // 12: 4 — 0 = H-blur, 1 = V-composite (must match vert mode) + direction: [2]f32, // 16: 8 — (1,0) for H-blur, (0,1) for V-composite + inv_downsample_factor: f32, // 24: 4 — 1.0 / downsample_factor (mode 1 only; mode 0 ignores) + _pad0: f32, // 28: 4 + kernel: [MAX_BACKDROP_KERNEL_PAIRS][4]f32, // 32: 512 — .x = weight, .y = offset (texels) +} + +// --------------------------------------------------------------------------------------------------------------------- +// ----- Pipeline --------------- +// --------------------------------------------------------------------------------------------------------------------- + +Pipeline_2D_Backdrop :: struct { + // Two graphics pipelines. The downsample PSO is a single-bilinear-sample fullscreen pass; + // the blur PSO is mode-branched (H-blur fullscreen + V-composite instanced) and shares + // one shader program for both modes via a uniform `mode` selector. + downsample_pipeline: ^sdl.GPUGraphicsPipeline, + blur_pipeline: ^sdl.GPUGraphicsPipeline, + + // Per-instance Backdrop_Primitive storage buffer. Grows on demand via grow_buffer_if_needed. + // All backdrop primitives across all layers in a frame share this single buffer; sub-batches + // reference into it by offset. + primitive_buffer: Buffer, + + // Working textures, allocated once at swapchain resolution and recreated only on resize. + // `source_texture` is full-resolution; the other two are ¼-res. All single-sample. + // source_texture — when any backdrop draw exists this frame, the entire frame renders + // here instead of the swapchain (Approach B). Copied to the swapchain + // at frame end. Acts as the bracket's snapshot input by virtue of + // already containing the pre-bracket frame. + // downsample_texture — written by the downsample PSO. Read by the blur PSO in mode 0. + // h_blur_texture — written by the blur PSO in mode 0. Read by the blur PSO in mode 1. + source_texture: ^sdl.GPUTexture, + downsample_texture: ^sdl.GPUTexture, + h_blur_texture: ^sdl.GPUTexture, + + // Cached pixel dimensions for resize-detection in `ensure_backdrop_textures`. + cached_width: u32, + cached_height: u32, + + // Linear-clamp sampler used for all backdrop sampling. Linear filtering is required by the + // linear-sampling pair trick (one bilinear fetch covers two adjacent texels). Clamp avoids + // edge-bleed at the work-region boundary. + sampler: ^sdl.GPUSampler, +} + +@(private) +create_pipeline_2d_backdrop :: proc( + device: ^sdl.GPUDevice, + window: ^sdl.Window, +) -> ( + pipeline: Pipeline_2D_Backdrop, + ok: bool, +) { + // On failure, clean up any partially-created resources. + defer if !ok { + if pipeline.sampler != nil do sdl.ReleaseGPUSampler(device, pipeline.sampler) + if pipeline.primitive_buffer.gpu != nil do destroy_buffer(device, &pipeline.primitive_buffer) + if pipeline.blur_pipeline != nil do sdl.ReleaseGPUGraphicsPipeline(device, pipeline.blur_pipeline) + if pipeline.downsample_pipeline != nil do sdl.ReleaseGPUGraphicsPipeline(device, pipeline.downsample_pipeline) + } + + active_shader_formats := sdl.GetGPUShaderFormats(device) + if PLATFORM_SHADER_FORMAT_FLAG not_in active_shader_formats { + log.errorf( + "backdrop: no embedded shader matches active GPU formats; build supports %v but device reports %v", + PLATFORM_SHADER_FORMAT, + active_shader_formats, + ) + return pipeline, false + } + + swapchain_format := sdl.GetGPUSwapchainTextureFormat(device, window) + + //----- Shader modules ---------------------------------- + + fullscreen_vert := sdl.CreateGPUShader( + device, + sdl.GPUShaderCreateInfo { + code_size = len(BACKDROP_FULLSCREEN_VERT_RAW), + code = raw_data(BACKDROP_FULLSCREEN_VERT_RAW), + entrypoint = SHADER_ENTRY, + format = {PLATFORM_SHADER_FORMAT_FLAG}, + stage = .VERTEX, + }, + ) + if fullscreen_vert == nil { + log.errorf("Could not create backdrop fullscreen vertex shader: %s", sdl.GetError()) + return pipeline, false + } + defer sdl.ReleaseGPUShader(device, fullscreen_vert) + + downsample_frag := sdl.CreateGPUShader( + device, + sdl.GPUShaderCreateInfo { + code_size = len(BACKDROP_DOWNSAMPLE_FRAG_RAW), + code = raw_data(BACKDROP_DOWNSAMPLE_FRAG_RAW), + entrypoint = SHADER_ENTRY, + format = {PLATFORM_SHADER_FORMAT_FLAG}, + stage = .FRAGMENT, + num_samplers = 1, + num_uniform_buffers = 1, + }, + ) + if downsample_frag == nil { + log.errorf("Could not create backdrop downsample fragment shader: %s", sdl.GetError()) + return pipeline, false + } + defer sdl.ReleaseGPUShader(device, downsample_frag) + + blur_vert := sdl.CreateGPUShader( + device, + sdl.GPUShaderCreateInfo { + code_size = len(BACKDROP_BLUR_VERT_RAW), + code = raw_data(BACKDROP_BLUR_VERT_RAW), + entrypoint = SHADER_ENTRY, + format = {PLATFORM_SHADER_FORMAT_FLAG}, + stage = .VERTEX, + num_uniform_buffers = 1, + num_storage_buffers = 1, + }, + ) + if blur_vert == nil { + log.errorf("Could not create backdrop blur vertex shader: %s", sdl.GetError()) + return pipeline, false + } + defer sdl.ReleaseGPUShader(device, blur_vert) + + blur_frag := sdl.CreateGPUShader( + device, + sdl.GPUShaderCreateInfo { + code_size = len(BACKDROP_BLUR_FRAG_RAW), + code = raw_data(BACKDROP_BLUR_FRAG_RAW), + entrypoint = SHADER_ENTRY, + format = {PLATFORM_SHADER_FORMAT_FLAG}, + stage = .FRAGMENT, + num_samplers = 1, + num_uniform_buffers = 1, + }, + ) + if blur_frag == nil { + log.errorf("Could not create backdrop blur fragment shader: %s", sdl.GetError()) + return pipeline, false + } + defer sdl.ReleaseGPUShader(device, blur_frag) + + //----- Downsample PSO ---------------------------------- + // Single bilinear sample, blend disabled. No vertex buffer (gl_VertexIndex 0..2 emits the + // fullscreen triangle). Single-sample target (the ¼-res working textures are never MSAA). + downsample_target := sdl.GPUColorTargetDescription { + format = swapchain_format, + blend_state = sdl.GPUColorTargetBlendState{enable_blend = false}, + } + pipeline.downsample_pipeline = sdl.CreateGPUGraphicsPipeline( + device, + sdl.GPUGraphicsPipelineCreateInfo { + vertex_shader = fullscreen_vert, + fragment_shader = downsample_frag, + primitive_type = .TRIANGLELIST, + multisample_state = sdl.GPUMultisampleState{sample_count = ._1}, + target_info = sdl.GPUGraphicsPipelineTargetInfo { + color_target_descriptions = &downsample_target, + num_color_targets = 1, + }, + }, + ) + if pipeline.downsample_pipeline == nil { + log.errorf("Failed to create backdrop downsample graphics pipeline: %s", sdl.GetError()) + return pipeline, false + } + + //----- Blur PSO (H-blur + V-composite, mode-branched) -------------- + // Premultiplied-over blend matching the main pipeline. No vertex buffer (mode 0 uses + // gl_VertexIndex 0..2 fullscreen tri; mode 1 uses gl_VertexIndex 0..5 unit-quad + + // gl_InstanceIndex into the storage buffer). + // + // Single-sample throughout: levlib does not support MSAA (see init's doc comment in + // draw.odin). The whole frame renders to single-sample targets, so sample_count = ._1 + // matches both mode 0 (writes h_blur_texture) and mode 1 (writes source_texture). + blur_target := sdl.GPUColorTargetDescription { + format = swapchain_format, + blend_state = sdl.GPUColorTargetBlendState { + enable_blend = true, + enable_color_write_mask = true, + src_color_blendfactor = .ONE, + dst_color_blendfactor = .ONE_MINUS_SRC_ALPHA, + color_blend_op = .ADD, + src_alpha_blendfactor = .ONE, + dst_alpha_blendfactor = .ONE_MINUS_SRC_ALPHA, + alpha_blend_op = .ADD, + color_write_mask = sdl.GPUColorComponentFlags{.R, .G, .B, .A}, + }, + } + pipeline.blur_pipeline = sdl.CreateGPUGraphicsPipeline( + device, + sdl.GPUGraphicsPipelineCreateInfo { + vertex_shader = blur_vert, + fragment_shader = blur_frag, + primitive_type = .TRIANGLELIST, + multisample_state = sdl.GPUMultisampleState{sample_count = ._1}, + target_info = sdl.GPUGraphicsPipelineTargetInfo { + color_target_descriptions = &blur_target, + num_color_targets = 1, + }, + }, + ) + if pipeline.blur_pipeline == nil { + log.errorf("Failed to create backdrop blur graphics pipeline: %s", sdl.GetError()) + return pipeline, false + } + + //----- Storage buffer for Backdrop_Primitive instances ------------- + pipeline.primitive_buffer = create_buffer( + device, + size_of(Backdrop_Primitive) * BUFFER_INIT_SIZE, + sdl.GPUBufferUsageFlags{.GRAPHICS_STORAGE_READ}, + ) or_return + + //----- Sampler ---------------------------------- + pipeline.sampler = sdl.CreateGPUSampler( + device, + sdl.GPUSamplerCreateInfo { + min_filter = .LINEAR, + mag_filter = .LINEAR, + mipmap_mode = .LINEAR, + address_mode_u = .CLAMP_TO_EDGE, + address_mode_v = .CLAMP_TO_EDGE, + address_mode_w = .CLAMP_TO_EDGE, + }, + ) + if pipeline.sampler == nil { + log.errorf("Could not create backdrop GPU sampler: %s", sdl.GetError()) + return pipeline, false + } + + log.debug("Done creating backdrop pipeline") + return pipeline, true +} + +@(private) +destroy_pipeline_2d_backdrop :: proc(device: ^sdl.GPUDevice, pipeline: ^Pipeline_2D_Backdrop) { + if pipeline.h_blur_texture != nil do sdl.ReleaseGPUTexture(device, pipeline.h_blur_texture) + if pipeline.downsample_texture != nil do sdl.ReleaseGPUTexture(device, pipeline.downsample_texture) + if pipeline.source_texture != nil do sdl.ReleaseGPUTexture(device, pipeline.source_texture) + if pipeline.sampler != nil do sdl.ReleaseGPUSampler(device, pipeline.sampler) + destroy_buffer(device, &pipeline.primitive_buffer) + if pipeline.blur_pipeline != nil do sdl.ReleaseGPUGraphicsPipeline(device, pipeline.blur_pipeline) + if pipeline.downsample_pipeline != nil do sdl.ReleaseGPUGraphicsPipeline(device, pipeline.downsample_pipeline) +} + +// --------------------------------------------------------------------------------------------------------------------- +// ----- Working texture management ---- +// --------------------------------------------------------------------------------------------------------------------- + +// Allocate (or reallocate, on resize) the three working textures that the backdrop bracket +// uses. `source_texture` is full swapchain resolution; the other two are ¼-res. All single- +// sample, all share the swapchain format, all need {.COLOR_TARGET, .SAMPLER} usage so they +// can be written by render passes and read by subsequent passes. +// +// Recreates on dimension change only — same-size frames hit the early-out and skip GPU +// resource churn. +@(private) +ensure_backdrop_textures :: proc(device: ^sdl.GPUDevice, format: sdl.GPUTextureFormat, width, height: u32) { + pipeline := &GLOB.pipeline_2d_backdrop + if pipeline.source_texture != nil && pipeline.cached_width == width && pipeline.cached_height == height { + return + } + + // Free any prior allocations (handles resize and the very-first call where these are nil). + if pipeline.h_blur_texture != nil { + sdl.ReleaseGPUTexture(device, pipeline.h_blur_texture) + pipeline.h_blur_texture = nil + } + if pipeline.downsample_texture != nil { + sdl.ReleaseGPUTexture(device, pipeline.downsample_texture) + pipeline.downsample_texture = nil + } + if pipeline.source_texture != nil { + sdl.ReleaseGPUTexture(device, pipeline.source_texture) + pipeline.source_texture = nil + } + + // Working textures are sized at full swapchain resolution to support factor=1 (no downsample + // for small σ, where any 2:1 round-trip would visibly soften the output). Larger factors just + // write to a sub-rect via viewport-limited rendering. See the file-header comment. + working_width := width + working_height := height + + pipeline.source_texture = sdl.CreateGPUTexture( + device, + sdl.GPUTextureCreateInfo { + type = .D2, + format = format, + usage = {.COLOR_TARGET, .SAMPLER}, + width = width, + height = height, + layer_count_or_depth = 1, + num_levels = 1, + sample_count = ._1, + }, + ) + if pipeline.source_texture == nil { + log.panicf("Failed to create backdrop source texture (%dx%d): %s", width, height, sdl.GetError()) + } + + pipeline.downsample_texture = sdl.CreateGPUTexture( + device, + sdl.GPUTextureCreateInfo { + type = .D2, + format = format, + usage = {.COLOR_TARGET, .SAMPLER}, + width = working_width, + height = working_height, + layer_count_or_depth = 1, + num_levels = 1, + sample_count = ._1, + }, + ) + if pipeline.downsample_texture == nil { + log.panicf( + "Failed to create backdrop downsample texture (%dx%d): %s", + working_width, + working_height, + sdl.GetError(), + ) + } + + pipeline.h_blur_texture = sdl.CreateGPUTexture( + device, + sdl.GPUTextureCreateInfo { + type = .D2, + format = format, + usage = {.COLOR_TARGET, .SAMPLER}, + width = working_width, + height = working_height, + layer_count_or_depth = 1, + num_levels = 1, + sample_count = ._1, + }, + ) + if pipeline.h_blur_texture == nil { + log.panicf( + "Failed to create backdrop h_blur texture (%dx%d): %s", + working_width, + working_height, + sdl.GetError(), + ) + } + + pipeline.cached_width = width + pipeline.cached_height = height +} + +// --------------------------------------------------------------------------------------------------------------------- +// ----- Kernel computation ------------ +// --------------------------------------------------------------------------------------------------------------------- + +// Compute Gaussian blur kernel weights with the linear-sampling pair adjustment. +// Adapted from RAD Debugger's r_d3d11_g_blur_shader_src CPU-side coefficient generation +// and Daniel Rákos's "Efficient Gaussian blur with linear sampling" article. +// +// The trick: bilinear sampling lets us fetch (1-t)*pixel[i] + t*pixel[i+1] with a single +// texture lookup. So for any pair of adjacent discrete weights w0, w1 we can collapse them +// into one bilinear fetch with weight w = w0+w1 sampled at offset i + w1/w. This halves the +// fragment-shader sample count for a given kernel radius. +// +// Output: `kernel[0]` is the center weight (offset 0), and `kernel[1..pair_count-1]` each +// hold one paired tap (sampled symmetrically as ±offset in the shader). The shader iterates +// `i in [1, pair_count)` and does two texture fetches per pair — one at +offset, one at +// -offset — for a total of 1 + 2*(pair_count-1) bilinear fetches per fragment. +// +// `sigma` is the true Gaussian standard deviation in the kernel's working-space units (¼-res +// texels, after the caller has converted from logical pixels via dpi_scaling and the +// downsample factor). The kernel extent reaches ±3σ, capturing 99.7% of the Gaussian's +// mass; weights beyond that contribute imperceptibly. sigma <= 0 produces a degenerate +// kernel `{1, 0}` that acts as a sharp pass-through. After the loop, the discrete weights +// are normalized so they sum to 1.0 (truncating at ±3σ loses a tiny amount of mass; we +// renormalize to preserve overall image brightness). +// +// Earlier versions of this routine ported RAD Debugger's algorithm verbatim, which derives +// stdev from a tap-count parameter (`stdev = (blur_count-1)/2`). That made the parameter +// name misleading: the user thought they were passing σ but were actually passing +// half-kernel-width. This version takes σ directly and derives the tap count from it, +// matching what callers expect when they read "gaussian_sigma". +@(private) +compute_blur_kernel :: proc(sigma: f32, kernel: ^[MAX_BACKDROP_KERNEL_PAIRS][4]f32) -> (pair_count: u32) { + if sigma <= 0 { + kernel[0] = {1, 0, 0, 0} + return 1 + } + + // Per-side discrete tap count: ceil(3*sigma) + 1 (center + 3σ reach on each side). + // Cap at the storage budget. With MAX_BACKDROP_KERNEL_PAIRS=32 each pair collapses 2 + // discrete taps via linear-sampling, so max discrete taps per side = 1 + 31*2 = 63. + discrete_taps := u32(math.ceil(3 * sigma)) + 1 + max_taps := u32(MAX_BACKDROP_KERNEL_PAIRS - 1) * 2 + 1 + if discrete_taps > max_taps do discrete_taps = max_taps + if discrete_taps < 2 { + // Sigma was so small that 3σ < 1 texel; degenerate to a sharp sample. + kernel[0] = {1, 0, 0, 0} + return 1 + } + + // Compute discrete weights[i] = exp(-i² / (2σ²)). The inv_root prefactor cancels in the + // final normalization, so we skip it. + weights: [MAX_BACKDROP_KERNEL_PAIRS * 2]f32 = {} + two_sigma_sq := 2 * sigma * sigma + total: f32 = 0 + for i in 0 ..< discrete_taps { + x := f32(i) + weights[i] = math.exp(-x * x / two_sigma_sq) + // weights[0] is the center; weights[1..] are sampled on both sides, so they count twice. + total += weights[i] if i == 0 else 2 * weights[i] + } + // Normalize so the kernel sums to exactly 1.0 across the full ±3σ extent. + if total > 0 { + inv_total := 1.0 / total + for i in 0 ..< discrete_taps do weights[i] *= inv_total + } + + // Linear-sampling pair adjustment: weights[1] and weights[2] collapse to one bilinear + // fetch with weight w = w0+w1 at offset i + w1/w. `weights` is sized 2*MAX so that + // `weights[i+1]` access on odd i up to discrete_taps-1 is always in bounds. + kernel[0] = {weights[0], 0, 0, 0} + pair_count = 1 + for i := u32(1); i < discrete_taps; i += 2 { + w0 := weights[i] + w1 := weights[i + 1] + w := w0 + w1 + // Guard against a div-by-zero where both adjacent weights underflow to 0 (only happens + // at the tail of a very tight kernel; numerically-degenerate but legal). + offset := f32(i) + if w > 0 do offset = f32(i) + w1 / w + kernel[pair_count] = {w, offset, 0, 0} + pair_count += 1 + } + return pair_count +} + +// --------------------------------------------------------------------------------------------------------------------- +// ----- Uniform push helpers ---------- +// --------------------------------------------------------------------------------------------------------------------- + +// Push the Backdrop_Vert_Uniforms block to the vertex stage at slot 0. +@(private) +push_backdrop_vert_globals :: proc(cmd_buffer: ^sdl.GPUCommandBuffer, width: f32, height: f32, mode: u32) { + uniforms := Backdrop_Vert_Uniforms { + projection = ortho_rh(left = 0.0, top = 0.0, right = width, bottom = height, near = -1.0, far = 1.0), + dpi_scale = GLOB.dpi_scaling, + mode = mode, + } + sdl.PushGPUVertexUniformData(cmd_buffer, 0, &uniforms, size_of(Backdrop_Vert_Uniforms)) +} + +// Push the Backdrop_Downsample_Frag_Uniforms block to the fragment stage at slot 0. +@(private) +push_backdrop_downsample_frag_globals :: proc( + cmd_buffer: ^sdl.GPUCommandBuffer, + source_width, source_height: u32, + downsample_factor: u32, +) { + uniforms := Backdrop_Downsample_Frag_Uniforms { + inv_source_size = {1.0 / f32(source_width), 1.0 / f32(source_height)}, + downsample_factor = downsample_factor, + } + sdl.PushGPUFragmentUniformData(cmd_buffer, 0, &uniforms, size_of(Backdrop_Downsample_Frag_Uniforms)) +} + +// Pick a downsample factor for a given sigma. See the file-header comment for the table and +// rationale. Returned values: {1, 2, 4}. +@(private) +compute_backdrop_downsample_factor :: proc(sigma_logical: f32) -> u32 { + sigma_phys := sigma_logical * GLOB.dpi_scaling + switch { + case sigma_phys <= 4: return 1 + case sigma_phys <= 8: return 2 + case: return 4 + } +} + +// Push the Backdrop_Frag_Uniforms block (kernel + pass mode/direction) to the fragment stage at slot 0. +@(private) +push_backdrop_blur_frag_globals :: proc( + cmd_buffer: ^sdl.GPUCommandBuffer, + uniforms: ^Backdrop_Frag_Uniforms, +) { + sdl.PushGPUFragmentUniformData(cmd_buffer, 0, uniforms, size_of(Backdrop_Frag_Uniforms)) +} + +// --------------------------------------------------------------------------------------------------------------------- +// ----- Storage-buffer upload --------- +// --------------------------------------------------------------------------------------------------------------------- + +// Upload all Backdrop_Primitive instances staged this frame to the backdrop pipeline's storage +// buffer. Mirrors the SDF primitive upload in pipeline_2d_base.odin's `upload`. Called from +// `end()` inside the same copy pass that uploads vertices/indices/SDF primitives. +@(private) +upload_backdrop_primitives :: proc(device: ^sdl.GPUDevice, pass: ^sdl.GPUCopyPass) { + prim_count := u32(len(GLOB.tmp_backdrop_primitives)) + if prim_count == 0 do return + + prim_size := prim_count * size_of(Backdrop_Primitive) + grow_buffer_if_needed( + device, + &GLOB.pipeline_2d_backdrop.primitive_buffer, + prim_size, + sdl.GPUBufferUsageFlags{.GRAPHICS_STORAGE_READ}, + ) + + prim_array := sdl.MapGPUTransferBuffer(device, GLOB.pipeline_2d_backdrop.primitive_buffer.transfer, false) + if prim_array == nil { + log.panicf("Failed to map backdrop primitive transfer buffer: %s", sdl.GetError()) + } + mem.copy(prim_array, raw_data(GLOB.tmp_backdrop_primitives), int(prim_size)) + sdl.UnmapGPUTransferBuffer(device, GLOB.pipeline_2d_backdrop.primitive_buffer.transfer) + + sdl.UploadToGPUBuffer( + pass, + sdl.GPUTransferBufferLocation{transfer_buffer = GLOB.pipeline_2d_backdrop.primitive_buffer.transfer}, + sdl.GPUBufferRegion{buffer = GLOB.pipeline_2d_backdrop.primitive_buffer.gpu, offset = 0, size = prim_size}, + false, + ) +} + +// --------------------------------------------------------------------------------------------------------------------- +// ----- Frame / layer scanners -------- +// --------------------------------------------------------------------------------------------------------------------- + +// Returns true if any sub-batch in any layer this frame is .Backdrop kind. Called once at the +// top of `end()` to decide whether to route the whole frame to source_texture (Approach B). +// O(total sub-batches) but with an early-exit on the first hit, so typical cost is tiny. +@(private) +frame_has_backdrop :: proc() -> bool { + for &batch in GLOB.tmp_sub_batches { + if batch.kind == .Backdrop do return true + } + return false +} + +// Returns the absolute index of the first .Backdrop sub-batch in the layer's sub-batch range, +// or -1 if the layer has no backdrops. The index is into GLOB.tmp_sub_batches (not relative to +// layer.sub_batch_start), to match how draw_layer's render-range helpers consume it. +@(private) +find_first_backdrop_in_layer :: proc(layer: ^Layer) -> int { + for i in 0 ..< layer.sub_batch_len { + abs_idx := layer.sub_batch_start + i + if GLOB.tmp_sub_batches[abs_idx].kind == .Backdrop do return int(abs_idx) + } + return -1 +} + +// --------------------------------------------------------------------------------------------------------------------- +// ----- Bracket scheduler ------------- +// --------------------------------------------------------------------------------------------------------------------- + +// Compute the union AABB of the backdrop primitives in a contiguous-same-sigma sub-batch run +// (one "sigma group"), expanded by 6 sigmas of blur reach (the kernel weight beyond 3σ is +// negligible; halo of 6σ covers both the H-blur reads from downsample and the V-blur reads +// from h_blur, since each pass extends its kernel another 3σ from its output position). +// Returns a viewport in physical pixels for the full-resolution render target; the caller +// divides by the chosen downsample factor for the working-resolution passes. +// +// Per-group (rather than per-layer) because the adaptive downsample picks a different factor +// per sigma, and the kernel reach is also per-sigma. A tighter region per group means less +// fragment work in the downsample and H-blur passes. +@(private) +compute_backdrop_group_work_region :: proc( + group_start, group_end: u32, + sigma_logical: f32, + swapchain_width, swapchain_height: u32, +) -> ( + region_x, region_y, region_w, region_h: u32, +) { + dpi := GLOB.dpi_scaling + has_any := false + min_x: f32 = 0 + min_y: f32 = 0 + max_x: f32 = 0 + max_y: f32 = 0 + + for i in group_start ..< group_end { + batch := GLOB.tmp_sub_batches[i] + if batch.kind != .Backdrop do continue + for p in batch.offset ..< batch.offset + batch.count { + prim := GLOB.tmp_backdrop_primitives[p] + // prim.bounds is in logical pixels (world space). + if !has_any { + min_x = prim.bounds[0] + min_y = prim.bounds[1] + max_x = prim.bounds[2] + max_y = prim.bounds[3] + has_any = true + } else { + if prim.bounds[0] < min_x do min_x = prim.bounds[0] + if prim.bounds[1] < min_y do min_y = prim.bounds[1] + if prim.bounds[2] > max_x do max_x = prim.bounds[2] + if prim.bounds[3] > max_y do max_y = prim.bounds[3] + } + } + } + + if !has_any do return 0, 0, 0, 0 + + // Halo = 6σ. The bracket runs two sequential blur passes (H then V). H reads downsample + // at ±3σ from its output; V reads h_blur at ±3σ from its output. So for V outputs at + // primitive_AABB to be valid, h_blur must be valid at primitive_AABB ±3σ, which requires + // the downsample valid at primitive_AABB ±6σ. + halo_logical := 6.0 * sigma_logical + min_x -= halo_logical + min_y -= halo_logical + max_x += halo_logical + max_y += halo_logical + + // Convert to physical pixels and clamp to swapchain bounds. + phys_min_x := math.max(min_x * dpi, 0) + phys_min_y := math.max(min_y * dpi, 0) + phys_max_x := math.min(max_x * dpi, f32(swapchain_width)) + phys_max_y := math.min(max_y * dpi, f32(swapchain_height)) + + if phys_max_x <= phys_min_x || phys_max_y <= phys_min_y do return 0, 0, 0, 0 + + region_x = u32(phys_min_x) + region_y = u32(phys_min_y) + region_w = u32(phys_max_x - phys_min_x) + region_h = u32(phys_max_y - phys_min_y) + return +} + +// Run the backdrop bracket for one layer. Assumes: +// - source_texture currently holds the pre-bracket frame contents (Pass A has already +// rendered everything that should appear behind the backdrop). +// - The caller has invoked ensure_backdrop_textures with current swapchain dimensions. +// - At least one .Backdrop sub-batch exists in the layer (caller checked). +// +// Per-sigma-group execution. The bracket walks the layer's sub-batches in submission order, +// grouping contiguous-same-sigma .Backdrop sub-batches. For each group: +// 1. Pick a downsample factor using compute_backdrop_downsample_factor. +// 2. Compute that group's work region (primitives' AABB + 6σ halo, clamped). +// 3. Downsample: source_texture → downsample_texture, viewport-limited to +// work_region/factor. Writes into a sub-rect of the working texture. +// 4. H-blur (mode 0, direction=H): downsample_texture → h_blur_texture, same viewport. +// 5. V-blur (mode 0, direction=V): h_blur_texture → downsample_texture (ping-pong reuse; +// downsample_texture's data is no longer needed). Same viewport. +// 6. Composite (mode 1): downsample_texture (now holds H+V blur) → source_texture, full- +// target viewport, per-primitive SDF discard handles masking and applies the tint. Each +// sub-batch in the group is one instanced draw. +// +// V-blur was historically combined with the composite into a single shader invocation, but +// that produced a horizontal-vs-vertical asymmetry artifact (horizontal source features +// looked sharper than vertical ones inside the panel). Splitting V-blur into its own +// working→working pass restores symmetry by making H and V blurs structurally identical. +// +// On exit, source_texture contains the pre-bracket contents plus all backdrop primitives +// composited on top. The caller then runs Pass B (post-bracket non-backdrop sub-batches) on +// source_texture with LOAD. +@(private) +run_backdrop_bracket :: proc( + cmd_buffer: ^sdl.GPUCommandBuffer, + layer: ^Layer, + swapchain_width, swapchain_height: u32, +) { + pipeline := &GLOB.pipeline_2d_backdrop + + full_viewport := sdl.GPUViewport { + x = 0, + y = 0, + w = f32(swapchain_width), + h = f32(swapchain_height), + min_depth = 0, + max_depth = 1, + } + full_scissor := sdl.Rect { + x = 0, + y = 0, + w = i32(swapchain_width), + h = i32(swapchain_height), + } + + // Working textures are at full swapchain resolution. Each per-group factor=N pass writes + // only to a sub-rect of dimensions (work_region_phys / N), via viewport-limited rendering. + + layer_end := layer.sub_batch_start + layer.sub_batch_len + i := layer.sub_batch_start + for i < layer_end { + batch := GLOB.tmp_sub_batches[i] + if batch.kind != .Backdrop { + i += 1 + continue + } + + // Find the contiguous run of .Backdrop sub-batches with this sigma. + sigma := batch.gaussian_sigma + group_start := i + group_end := i + 1 + for group_end < layer_end { + next := GLOB.tmp_sub_batches[group_end] + if next.kind != .Backdrop || next.gaussian_sigma != sigma do break + group_end += 1 + } + + // Pick downsample factor for this group. + downsample_factor := compute_backdrop_downsample_factor(sigma) + + // Compute this group's work region (primitive AABB + 6σ halo, in physical pixels). + region_x, region_y, region_w, region_h := compute_backdrop_group_work_region( + group_start, + group_end, + sigma, + swapchain_width, + swapchain_height, + ) + if region_w == 0 || region_h == 0 { + i = group_end + continue + } + + // Convert work region to working-resolution coords (divide by factor, ceil-round-up). + working_x := region_x / downsample_factor + working_y := region_y / downsample_factor + working_w := (region_w + downsample_factor - 1) / downsample_factor + working_h := (region_h + downsample_factor - 1) / downsample_factor + + // Working textures are sized at min factor (2). At factor=4 we have only half the texture + // area available in each axis. Clamp to the texture extent for either case. + wt_w := pipeline.cached_width / downsample_factor + wt_h := pipeline.cached_height / downsample_factor + if working_x + working_w > wt_w do working_w = wt_w - working_x + if working_y + working_h > wt_h do working_h = wt_h - working_y + if working_w == 0 || working_h == 0 { + i = group_end + continue + } + + working_viewport := sdl.GPUViewport { + x = f32(working_x), + y = f32(working_y), + w = f32(working_w), + h = f32(working_h), + min_depth = 0, + max_depth = 1, + } + working_scissor := sdl.Rect { + x = i32(working_x), + y = i32(working_y), + w = i32(working_w), + h = i32(working_h), + } + + // inv_working_size is always relative to the actual texture extent (full swapchain res). + // At factor>1 we're only using a sub-rect, but the texture coords are still divided by the + // full texture's dimensions because that's what gl_FragCoord operates on. + inv_working_size := [2]f32{1.0 / f32(pipeline.cached_width), 1.0 / f32(pipeline.cached_height)} + + // Convert the user's logical-pixel sigma into the kernel's working space. + // sigma_working_texels = sigma_logical * dpi_scaling / downsample_factor. + effective_sigma := sigma * GLOB.dpi_scaling / f32(downsample_factor) + frag_uniforms := Backdrop_Frag_Uniforms { + inv_working_size = inv_working_size, + inv_downsample_factor = 1.0 / f32(downsample_factor), + } + frag_uniforms.pair_count = compute_blur_kernel(effective_sigma, &frag_uniforms.kernel) + + //----- Downsample (source_texture → downsample_texture, viewport-limited) ---------- + { + pass := sdl.BeginGPURenderPass( + cmd_buffer, + &sdl.GPUColorTargetInfo { + texture = pipeline.downsample_texture, + load_op = .DONT_CARE, + store_op = .STORE, + cycle = true, + }, + 1, + nil, + ) + sdl.BindGPUGraphicsPipeline(pass, pipeline.downsample_pipeline) + sdl.SetGPUViewport(pass, working_viewport) + sdl.SetGPUScissor(pass, working_scissor) + push_backdrop_downsample_frag_globals( + cmd_buffer, + pipeline.cached_width, + pipeline.cached_height, + downsample_factor, + ) + sdl.BindGPUFragmentSamplers( + pass, + 0, + &sdl.GPUTextureSamplerBinding{texture = pipeline.source_texture, sampler = pipeline.sampler}, + 1, + ) + sdl.DrawGPUPrimitives(pass, 3, 1, 0, 0) + sdl.EndGPURenderPass(pass) + } + + //----- H-blur (mode 0, direction=H): downsample_texture → h_blur_texture -------- + { + frag_uniforms.mode = 0 + frag_uniforms.direction = {1, 0} + + pass := sdl.BeginGPURenderPass( + cmd_buffer, + &sdl.GPUColorTargetInfo { + texture = pipeline.h_blur_texture, + load_op = .DONT_CARE, + store_op = .STORE, + cycle = true, + }, + 1, + nil, + ) + sdl.BindGPUGraphicsPipeline(pass, pipeline.blur_pipeline) + sdl.SetGPUViewport(pass, working_viewport) + sdl.SetGPUScissor(pass, working_scissor) + // Mode 0's vertex shader is a fullscreen triangle that ignores `projection`; pass + // the standard ortho anyway so the same uniform block works for both modes. + push_backdrop_vert_globals(cmd_buffer, f32(swapchain_width), f32(swapchain_height), 0) + push_backdrop_blur_frag_globals(cmd_buffer, &frag_uniforms) + // The blur PSO is declared with num_storage_buffers = 1 (mode 1 reads it). SDL3 GPU + // validation requires the binding to be present for *any* draw on this PSO, even + // though mode 0's shader path doesn't actually read it. Bind it here too. + sdl.BindGPUVertexStorageBuffers(pass, 0, ([^]^sdl.GPUBuffer)(&pipeline.primitive_buffer.gpu), 1) + sdl.BindGPUFragmentSamplers( + pass, + 0, + &sdl.GPUTextureSamplerBinding{texture = pipeline.downsample_texture, sampler = pipeline.sampler}, + 1, + ) + sdl.DrawGPUPrimitives(pass, 3, 1, 0, 0) + sdl.EndGPURenderPass(pass) + } + + //----- V-blur (mode 0, direction=V): h_blur_texture → downsample_texture -------- + // Ping-pong reuse: downsample_texture's data is no longer needed once H-blur has + // produced its output, so we reuse it as the V-blur target. Saves allocating a third + // working texture. + { + frag_uniforms.mode = 0 + frag_uniforms.direction = {0, 1} + + pass := sdl.BeginGPURenderPass( + cmd_buffer, + &sdl.GPUColorTargetInfo { + texture = pipeline.downsample_texture, + load_op = .DONT_CARE, + store_op = .STORE, + cycle = true, + }, + 1, + nil, + ) + sdl.BindGPUGraphicsPipeline(pass, pipeline.blur_pipeline) + sdl.SetGPUViewport(pass, working_viewport) + sdl.SetGPUScissor(pass, working_scissor) + push_backdrop_vert_globals(cmd_buffer, f32(swapchain_width), f32(swapchain_height), 0) + push_backdrop_blur_frag_globals(cmd_buffer, &frag_uniforms) + sdl.BindGPUVertexStorageBuffers(pass, 0, ([^]^sdl.GPUBuffer)(&pipeline.primitive_buffer.gpu), 1) + sdl.BindGPUFragmentSamplers( + pass, + 0, + &sdl.GPUTextureSamplerBinding{texture = pipeline.h_blur_texture, sampler = pipeline.sampler}, + 1, + ) + sdl.DrawGPUPrimitives(pass, 3, 1, 0, 0) + sdl.EndGPURenderPass(pass) + } + + //----- Composite (mode 1): downsample_texture (now holds H+V blur) → source_texture -- + // No kernel applied here — the working texture is already fully blurred. The shader just + // upsamples (via bilinear filtering on the read), applies the SDF mask, and applies the + // tint. One render pass for the whole sigma group; each sub-batch issues its own draw + // call because non-contiguous-but-same-sigma sub-batches couldn't coalesce upstream. + { + frag_uniforms.mode = 1 + // direction is unused in mode 1 but keep it set so reading the uniform doesn't see + // undefined data on platforms that care about that. + frag_uniforms.direction = {0, 0} + + pass := sdl.BeginGPURenderPass( + cmd_buffer, + &sdl.GPUColorTargetInfo{texture = pipeline.source_texture, load_op = .LOAD, store_op = .STORE}, + 1, + nil, + ) + sdl.BindGPUGraphicsPipeline(pass, pipeline.blur_pipeline) + sdl.SetGPUViewport(pass, full_viewport) + sdl.SetGPUScissor(pass, full_scissor) + push_backdrop_vert_globals(cmd_buffer, f32(swapchain_width), f32(swapchain_height), 1) + push_backdrop_blur_frag_globals(cmd_buffer, &frag_uniforms) + sdl.BindGPUVertexStorageBuffers(pass, 0, ([^]^sdl.GPUBuffer)(&pipeline.primitive_buffer.gpu), 1) + sdl.BindGPUFragmentSamplers( + pass, + 0, + &sdl.GPUTextureSamplerBinding{texture = pipeline.downsample_texture, sampler = pipeline.sampler}, + 1, + ) + for j in group_start ..< group_end { + grp := GLOB.tmp_sub_batches[j] + sdl.DrawGPUPrimitives(pass, 6, grp.count, 0, grp.offset) + } + sdl.EndGPURenderPass(pass) + } + + i = group_end + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// ----- Primitive builders ------------ +// --------------------------------------------------------------------------------------------------------------------- + +// Internal +// +// Build a Backdrop_Primitive with bounds, radii, and feather computed from rectangle +// geometry. The caller sets `color` (tint) on the returned primitive before submitting. +// +// No rotation, no outline — backdrop primitives are intentionally limited to axis-aligned +// RRects in v1. Rotation breaks screen-space blur sampling visually; outline would be a +// specialized edge effect that belongs in its own primitive type. +@(private) +build_backdrop_primitive :: proc( + rect: Rectangle, + radii: Rectangle_Radii, + feather_px: f32, +) -> Backdrop_Primitive { + max_radius := min(rect.width, rect.height) * 0.5 + clamped_top_left := clamp(radii.top_left, 0, max_radius) + clamped_top_right := clamp(radii.top_right, 0, max_radius) + clamped_bottom_right := clamp(radii.bottom_right, 0, max_radius) + clamped_bottom_left := clamp(radii.bottom_left, 0, max_radius) + + half_feather := feather_px * 0.5 + padding := half_feather / GLOB.dpi_scaling + dpi_scale := GLOB.dpi_scaling + + half_width := rect.width * 0.5 + half_height := rect.height * 0.5 + center_x := rect.x + half_width + center_y := rect.y + half_height + + return Backdrop_Primitive { + bounds = { + center_x - half_width - padding, + center_y - half_height - padding, + center_x + half_width + padding, + center_y + half_height + padding, + }, + // Radii ordering matches the shader's sdRoundedBox swizzle: + // (p.x > 0) ? r.xy : r.zw picks right-vs-left half + // then (p.y > 0) ? rxy.x : rxy.y picks bottom-vs-top within that half + // So slot 0 = bottom-right, slot 1 = top-right, slot 2 = bottom-left, slot 3 = top-left. + radii = { + clamped_bottom_right * dpi_scale, + clamped_top_right * dpi_scale, + clamped_bottom_left * dpi_scale, + clamped_top_left * dpi_scale, + }, + half_size = {half_width * dpi_scale, half_height * dpi_scale}, + half_feather = half_feather, + } +} + +// Internal — append a Backdrop_Primitive to the staging array and emit a .Backdrop sub-batch +// carrying the requested gaussian_sigma. Sub-batch coalescing in append_or_extend_sub_batch +// will merge contiguous backdrops that share a sigma into a single instanced draw. +@(private) +prepare_backdrop_primitive :: proc(layer: ^Layer, prim: Backdrop_Primitive, gaussian_sigma: f32) { + offset := u32(len(GLOB.tmp_backdrop_primitives)) + append(&GLOB.tmp_backdrop_primitives, prim) + scissor := &GLOB.scissors[layer.scissor_start + layer.scissor_len - 1] + append_or_extend_sub_batch( + scissor, + layer, + .Backdrop, + offset = offset, + count = 1, + gaussian_sigma = gaussian_sigma, + ) +} + +// --------------------------------------------------------------------------------------------------------------------- +// ----- Public API -------------------- +// --------------------------------------------------------------------------------------------------------------------- + +// Draw a rectangle whose interior samples a Gaussian-blurred snapshot of the framebuffer +// behind it. RRect-only — covers rectangles, rounded rectangles, and circles via +// uniform_radii. +// +// `gaussian_sigma` is the Gaussian standard deviation in logical pixels. Typical UI range is +// 4..24. sigma <= 0 produces a sharp framebuffer mirror (no blur). +// +// `tint` controls the color of the frosted glass: +// - tint.rgb is the tint color. +// - tint.a is the tint *mix strength*, NOT panel opacity. The panel is always fully +// opaque inside its mask (matching real frosted glass and iOS UIBlurEffect / CSS +// backdrop-filter). At alpha=0 the user sees the pure blur unchanged; at alpha=255 +// the blur is fully multiplied by tint.rgb. Intermediate values lerp between the two. +// - For a translucent panel layered over content, draw a separate translucent rect on +// top instead — the backdrop's job is to deliver the blur, not to blend with what's +// beneath it. +// +// Backdrop primitives have no rotation: backdrop sampling is in screen space, so a rotated +// mask over a stationary blur sample would look visually wrong. iOS UIVisualEffectView, +// CSS backdrop-filter, and Flutter BackdropFilter all enforce this implicitly; we enforce +// it explicitly by leaving no rotation parameter. +// +// Within a single layer, primitives sharing the same `gaussian_sigma` share one H+V blur +// pass pair via sub-batch coalescing. Primitives with different sigmas in the same layer +// trigger separate blur passes (cost scales with the number of unique sigmas). +// +// Submission ordering is asymmetric: a non-backdrop draw submitted between two backdrops in +// the same layer renders *on top of* both backdrops, not between them. Use `draw.new_layer` +// to interleave. See README.md § "Backdrop pipeline" for the full bracket scheduling model. +gaussian_blur :: proc( + layer: ^Layer, + rect: Rectangle, + gaussian_sigma: f32, + tint: Color = DFT_TINT, + radii: Rectangle_Radii = {}, + feather_px: f32 = DFT_FEATHER_PX, +) { + prim := build_backdrop_primitive(rect, radii, feather_px) + prim.color = tint + prepare_backdrop_primitive(layer, prim, gaussian_sigma) +} diff --git a/draw/draw.odin b/draw/draw.odin index fab8c54..770a59c 100644 --- a/draw/draw.odin +++ b/draw/draw.odin @@ -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 ----------------------- // --------------------------------------------------------------------------------------------------------------------- diff --git a/draw/examples/backdrop.odin b/draw/examples/backdrop.odin new file mode 100644 index 0000000..6e033aa --- /dev/null +++ b/draw/examples/backdrop.odin @@ -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}) + } +} diff --git a/draw/examples/examples.odin b/draw/examples/examples.odin index c75d49a..c865437 100644 --- a/draw/examples/examples.odin +++ b/draw/examples/examples.odin @@ -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 ") - 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) } } diff --git a/draw/examples/hellope.odin b/draw/examples/hellope.odin index c27a5e7..f497a9c 100644 --- a/draw/examples/hellope.odin +++ b/draw/examples/hellope.odin @@ -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) diff --git a/draw/examples/textures.odin b/draw/examples/textures.odin index acf2793..9a3c6d9 100644 --- a/draw/examples/textures.odin +++ b/draw/examples/textures.odin @@ -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) } } diff --git a/draw/pipeline_2d_base.odin b/draw/pipeline_2d_base.odin index 2d5923b..6ae876f 100644 --- a/draw/pipeline_2d_base.odin +++ b/draw/pipeline_2d_base.odin @@ -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. } } } diff --git a/draw/shaders/generated/backdrop_blur.frag.metal b/draw/shaders/generated/backdrop_blur.frag.metal new file mode 100644 index 0000000..dea97aa --- /dev/null +++ b/draw/shaders/generated/backdrop_blur.frag.metal @@ -0,0 +1,118 @@ +#pragma clang diagnostic ignored "-Wmissing-prototypes" + +#include +#include + +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 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 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; +} + diff --git a/draw/shaders/generated/backdrop_blur.frag.spv b/draw/shaders/generated/backdrop_blur.frag.spv new file mode 100644 index 0000000000000000000000000000000000000000..42e8d7cc7935298660abf5674485668a27ca0c74 GIT binary patch literal 7056 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 literal 0 HcmV?d00001 diff --git a/draw/shaders/generated/backdrop_blur.vert.metal b/draw/shaders/generated/backdrop_blur.vert.metal new file mode 100644 index 0000000..f633ac5 --- /dev/null +++ b/draw/shaders/generated/backdrop_blur.vert.metal @@ -0,0 +1,123 @@ +#pragma clang diagnostic ignored "-Wmissing-prototypes" +#pragma clang diagnostic ignored "-Wmissing-braces" + +#include +#include + +using namespace metal; + +template +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 _97 = spvUnsafeArray({ 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; +} + diff --git a/draw/shaders/generated/backdrop_blur.vert.spv b/draw/shaders/generated/backdrop_blur.vert.spv new file mode 100644 index 0000000000000000000000000000000000000000..8e2eb1f1fcb54521400887059bc87a2e29833742 GIT binary patch literal 4588 zcmZ{m33pRf5XWCznu2U1s{*B^u52o@%Jxc;7DXu{hzo|aNg$9WAxQxdwTK%oC@vpD zzl7)brCiSO9Dl#}-Yv1}<@`G{|CzaW?%aD{=;s2e7N%O!I3 z_^IRDTJ7St9Xp>fVoB7K1RAq6TAb+f_&Zgo)HK$DO<*V34fcZP!G3TQ^zg46`70Cj zM)UZ0KercVA@=E7rCe`LwNR-dmvHKhX8lrWvR$dOQsAwl*r?=NlZ9$2iBIQnjj4K3 zirAF1@_eIEbR)ggkyfAT55v`plkmA@4;QNWGo@y`bp2SZSh{XZE_#v}`0-LRbzvrT z$UF4aTa~21T-2AuTVKLgYVA{%8(BT6&l>TKR4a`ml~%h@n=CP3>a$jT)79#%&zkM6 z!`|8BM!s5S>xpf@%lXNAwcf<;O?qF>Uo2G1`IbwY<5`sR%|fwK!OkV;lEsuuh4#f# z)4BVigTx&!OkOTF>y7+)vogiDu2Q|d)aRI}PuGf^QsCTcI&<0=N?=kQ&bE)+S=X06mr-h$^h0iROF^+sjvYxQQe zm~Ye#GNx`!sn+%^M_JQ1<$lNTsXKbX7=MG!hah@z4mX7D2yymNF7Du*SMEp$*OPH$ z$Whkxoecs+o({ukR_Xw>Qx-XX~^U{pOmZ44gS~t{*scInU0ud^7#P z;aa{Khdy^j;5n$gTJVco~E%wLE938XP{?%#6{J=7nWr?*KZx# zz9+e`U(d)9*4>(#v-Y;co;l=v+tJN+E$tnd?Y^B~+qY!Q7VeS!F5sNP80YX!r7?ZU zTzfKqhzqgK=NmINBgp@bN*x}5c8@ry_ z4&&3`k3NFbc8()R?}wc0Yx_3jv_1EupbHp3hScX=+Q*ThzHlC=;KKf1%ly_I{I8?? zF3dlh`Q4L#<(t5G@5x(8_bq4q+ep`w59jj^x@T<91DDTi`M{k=*G{*hduZm#@{4+=^OL^W7eG^KW0&XH{w5kf%qqV1WneMz8*qR%?B_#3{%2xVAXjFb-+|w#ay8QNC-M4LfdjzW zQ$798tblT zzYpFSb-x3?eRbb?;Cj2lM22=uv6 z>whWZ5J}y>#g{=0LfzWNg`8K=oyR`7S94B)7fB_SpcDrljz>D zP-79@82NBlO6c=<oi7g-2ZlTKwM`!JA>=<=uL z-v^6^N6Dj8$-l-W71Mie`c=v9B8L)^MJ@=1*{fU7-=h=;v zcb+xK+aLzMfv|^<(dCWbi_|vW`}GNkL8#qzJ_Yi|`_0fc-uLnuh=K1V?DcbWdE@=A zXd8bA`30~y;SA@af0^;2zOS(DxjFXpYcRK;cQZcp^BZjQ^7G2&ExO?bw_APv4 sze6`x-&WvVzQ@|`{d-`2##qmG +#include + +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 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; +} + diff --git a/draw/shaders/generated/backdrop_downsample.frag.spv b/draw/shaders/generated/backdrop_downsample.frag.spv new file mode 100644 index 0000000000000000000000000000000000000000..6ab85047a6550843f104db29ad3dff65e8064ac7 GIT binary patch literal 3012 zcmZ9NYjaao6oyX;X{#uR+(Z;p@P?I&%1x09Mcb-XP(-|kG)-$I&8bOR5HA#G{OS+! zXZWYQ%sAuoBzxhRb7ofeyWVx#@7iadw6g8!SeA`sJF@ZYmu&d#&bGm1aHE5DZsF>} zRIk4@b@J2+Gj?W`L83XkvaxJCtG3oy?HG6nJBB?%o;(|XDs;`VKPalg7W1oQJV311 zZPpjtdGlty+3NILT_|!#2mMys_1SJ?Z*_W&we@zZzT9Z`b2l1gCu`MMUvDfqXcf(mvigW8ST{Ds z)xmuEM!(+7+k<+!yI7-YXIL|?DyudlToc-+U?9S{H{t<8``M%@B zn2&MiFXro;Z{I!qb)BHvdhveHC~FNnK)v@w-&hS_ZE~9N2U(4KcZ@sp2qu3n`D&Nj zhwpjRFrUNeEbJvejBU?$gWb<_vWLEN%h_WOYYnsKWb*eWUk&@f=GxD=>l2vydx<=m zc>8#^KGr?WYMpxYpJ(-4VitYRSMDHR#|x~^Zr|b){vk3a37BWU7gLg)bLsm&d_w0* zuo}$2$f_3iFzz~wW!cuW@f>7aeWy7~>wFJBYWxeP%IaReH@|1|!=EVmeispU@0o&c z{d~#y`-ptweji1DrQoa8@5A_c^r-n^%)4dz&e9zYVP7h%6 z>-e6{V~P8X8v7l@>?uFZi8#Mp^n4s#?s*7K{&#wKPyCkTygz@^qsHp<4|^JW3VRvz zti@_`?<1J|xtI6q8O*)BZ;|tC;(twgpToCKUcJ5_IrF_UQ<(Q9>R$qzbDet6^8{v& zy!GByxxdj?V^yPCWBymb<@sNQlaKjNf#tlH&VPp0$N7zv?7KgUiz{UAr$1l%+ z4o*Jie*CEbY+z}Cp$OWb9!`xuM+ zbp>2L-w)yB<9t5?TPv@2zh}A7{xP_y{Tja7L+dBWk6Etc%Z1iY!Pdx!*3ZE9H5OVw z2bZ;e0Vf|?zXV$=uU3C+d}wWg?Q1NwE`iHh zTX6EBbs21}yjuMolnbrjfQwpJ@zokyz6Hx^q2(snI%A=w4K8b0gOd*}9k8|XYFS~G zQ_Cdlci8ddkFj}4$a+y)o>e2?#0 zMV~uhbL6AX4`6#4i#|Vsi+z5=ch2bZGuRyY=yMlrFJqp|I;)&L)w_ks`M*Q|9=3(W YcYZ8!#ov-MaK+!&8veih*E);+2cSF3n*aa+ literal 0 HcmV?d00001 diff --git a/draw/shaders/generated/backdrop_fullscreen.vert.metal b/draw/shaders/generated/backdrop_fullscreen.vert.metal new file mode 100644 index 0000000..6ecbfd5 --- /dev/null +++ b/draw/shaders/generated/backdrop_fullscreen.vert.metal @@ -0,0 +1,18 @@ +#include +#include + +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; +} + diff --git a/draw/shaders/generated/backdrop_fullscreen.vert.spv b/draw/shaders/generated/backdrop_fullscreen.vert.spv new file mode 100644 index 0000000000000000000000000000000000000000..8a127a047d33855aeea856fa2236384d336e04f0 GIT binary patch literal 1096 zcmYk4T}xC^6oz**&8S(L`PGNnG5b~r?$9u{k_cfIRlzh~O5)v*w+gvl@-)<2Wr$cUnT-iS)*Nrvb24BDA$5npvsqE(` z)^x*tIN-hfxK0s**0^_bN5i5jj!Mw6$ak-Z7iG0yoaXXU&V9(e9~3`66vOJEJj_Qq zUuEv+U@*$Lr*HA>@&eyBiLJ-D9p~BeBip*G$Gj(DwU(-UfmtKZnTh9JOr2&?PRLhzj%jlnr(3PQ?pb%$Nh5c0}!6TP2Ee(-sV>@vCr_` r;ZcL(M%d#9^BeT2QiCU$TyyHCR`2McPR}s4&M;>h%m2q$e2x7FMaDzh literal 0 HcmV?d00001 diff --git a/draw/shaders/generated/base_2d.frag.metal b/draw/shaders/generated/base_2d.frag.metal index e3e07d6..c2052dd 100644 --- a/draw/shaders/generated/base_2d.frag.metal +++ b/draw/shaders/generated/base_2d.frag.metal @@ -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 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(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 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 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 tex [[textur } else { - float2 direction = float2(as_type(in.f_uv_or_effects.z)); + float2 direction = float2(as_type(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 tex [[textur { if ((flags & 1u) != 0u) { - float4 uv_rect = as_type(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 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(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(in.f_effects.w)).x / grad_magnitude; float param_14 = d; float param_15 = h; float fill_cov = sdf_alpha(param_14, param_15); diff --git a/draw/shaders/generated/base_2d.frag.spv b/draw/shaders/generated/base_2d.frag.spv index aee37602610d8ce54c57bd39a037d507e032a226..8e85e79a89f54122a7bc0f059a2337cb6d172ee3 100644 GIT binary patch literal 13528 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 Hb6o!d>sLWN literal 14212 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 8u) & 255u; + if ((flags & 16u) != 0u) + { + float2 sc = float2(as_type(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; diff --git a/draw/shaders/generated/base_2d.vert.spv b/draw/shaders/generated/base_2d.vert.spv index 5cc30c56d086f022483d25eaa8d0d64fd575f074..01d50772c781622aad51d8aff2580f701bda59ed 100644 GIT binary patch delta 2306 zcmZXU*>BWU7>3VuIxTb&rgMQ25epI^Rg9Q4kO~SREneA%$OVZhq_!YbN*6#hajc5F zDE?fiVg*qVajPRPh>D07MK6qhfmg=F3-4TR^m#hJLrR_I&G&xq`L=WBo1R^G?%Min z#r*0JGNCq13qOw4@5wC4g+(ERp3a@SItRMDhTu8=nkKaFo*tyZ5Ry{n=q1%*4eIJm zgWa7=*K{TuD@Ir6CL}6`m|jJ)VbefN&xLGg#oqyPp*pl-FO<1DJKCI^kfxR~?BX)j z;pWMyzh~Pw^dWTh4Q%foC=cF`zq@as_x8>meGE>&%5b+a-P?!CF_qy#{NBEC`Vu&cDSa!Dprm5k(OXR1plax*fGYbh6}nom$WXrsYeY!%p8`l+g= zA~UO0KRutx)FyLl8j~ApF3Z-2wxqFUZZcH!N3n_cdSbT$Z+9iWgLwPJslPq-FUJ~h z?Rx;98}+9{_5t&w|1|vHNqz#K37VO?0$U0ABu8o+Z?(P;XU`T+^nJL=zK=H9_tB!i z9`2(>-~0o~Z?)r@t4d>a$192+Yz7@^&?<&l2fqWj30uHQwnD!gmz%i!R(x~3m^nVo zoxmI)%p85=Rxr2^D7ubDjZeG5*<9bCZu#qkw%5 zs5gK=sqq4q&%E#tMLc{SDE`H(%HxtUV*FcrH&7K$OSObgP3?2Zt=;9ui~r6m?LnDPafbkeDzrT zb+~!z!)e1e;D>-`-zvnzA)!gSsF19s~;my zCi#ZKoX=2BrV$IU`f;*T6F;lZQBI{13$gmK#uxBbumw1klYa@+ooZ3)8{Z2*4UFFg zj9-lX3Y-SUH>JMu*Tc{7fAr^ueDUitzi{)j`2D~*|A~A9>cBD(cmLZ|kN-r*@y%23 zAg>C04ygNy+IMg@U%#Cjai`YsJ&wU1&Ic>Neu{7y&I1QD#(`#Ge*o8krNE2npHGez GX8#B9ksmey delta 1299 zcmZuxOHUJF6ur}SrnI)&TD2OC4-7&>to0EiAqXaDbw!EMZN=Iuh@H|0wV2peRKy2@ zpKqitU7!ooy(<#+pSaM4|G;x5Gl?1}Il1?q@7%}SZ!%vGuJ;7pu6k?@hLbNvzX5oIgBoO#8tC6bva!yOYn8^ zNR^swzML(#KYQYurmLTNr)IQ$#wIP8gy0MW29Rp^dk0+F{&vL|)-=DG_wQ97{M+1q zNvZ|^ZuQ;&Be4hi31~T43}AfCj(5WLA!e-`0G=T=5=eAYz)6`jU?*_WKjW(U7WkrV zZZ8MdT<2N&3NU7iPU1z_brRsZt^y~-ZN=0LqA0sx2aefiY{;0o=U`3M3`cfw(Izm8 z=FFG|$Q9JDP(-bV66$C;G*|({`~dEssu@57;s7(s05zO}FT!)DPtEEx3#JA*eQv@h zH*582gcGM+h}=LE^RY)Apz0Q|XmbttP_PPMuRL!lC`fYkZ0GsDZ=KxMS z4DgJZ+hNH&o4Ny^lRNQU_^du>cfJRo8uIMc4?7P8fa3u3c+dAaAP=087vM7)2Ug=< zu;iTeJb=$Dyacca2Yv{U=b&A7?C2jMralWe`p59eQ{OE^A|#xhEW&5PMt})E*e3va zCiK{`qhC_vkw|?R?2@hMg=I_yzq+RYEAp#j#Tcvt$g?8<0F0?W0{aZ$o(==lcfdXe z$TQ|`zW}&@=3uvz@)E(n%Y6loytCWa@R>+{42cHp8-P3?hg*S9jvqk^8|IGR0@SXk d>1ZUj3Xda^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); +} diff --git a/draw/shaders/source/backdrop_blur.vert b/draw/shaders/source/backdrop_blur.vert new file mode 100644 index 0000000..879a6a2 --- /dev/null +++ b/draw/shaders/source/backdrop_blur.vert @@ -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); + } +} diff --git a/draw/shaders/source/backdrop_downsample.frag b/draw/shaders/source/backdrop_downsample.frag new file mode 100644 index 0000000..7c4e1d3 --- /dev/null +++ b/draw/shaders/source/backdrop_downsample.frag @@ -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; + } +} diff --git a/draw/shaders/source/backdrop_fullscreen.vert b/draw/shaders/source/backdrop_fullscreen.vert new file mode 100644 index 0000000..d3800ea --- /dev/null +++ b/draw/shaders/source/backdrop_fullscreen.vert @@ -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); +} diff --git a/draw/shaders/source/base_2d.frag b/draw/shaders/source/base_2d.frag index 0ebb043..7f0ed6e 100644 --- a/draw/shaders/source/base_2d.frag +++ b/draw/shaders/source/base_2d.frag @@ -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); diff --git a/draw/shaders/source/base_2d.vert b/draw/shaders/source/base_2d.vert index 0f74941..7bd5c15 100644 --- a/draw/shaders/source/base_2d.vert +++ b/draw/shaders/source/base_2d.vert @@ -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); } diff --git a/draw/shapes.odin b/draw/shapes.odin index 75c3852..0c9b712 100644 --- a/draw/shapes.odin +++ b/draw/shapes.odin @@ -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) } } diff --git a/draw/textures.odin b/draw/textures.odin index b9e5b31..adb9f88 100644 --- a/draw/textures.odin +++ b/draw/textures.odin @@ -14,8 +14,8 @@ Texture_Kind :: enum u8 { } Sampler_Preset :: enum u8 { - Nearest_Clamp, Linear_Clamp, + Nearest_Clamp, Nearest_Repeat, Linear_Repeat, }