diff --git a/draw/draw.odin b/draw/draw.odin index d581b11..cab4a39 100644 --- a/draw/draw.odin +++ b/draw/draw.odin @@ -754,14 +754,18 @@ measure_text_clay :: proc "c" ( } // Called for each Clay `RenderCommandType.Custom` render command that -// `prepare_clay_batch` encounters. +// `prepare_clay_batch` encounters and which is NOT a levlib-managed variant +// (e.g. `Backdrop_Marker`). // // - `layer` is the layer the command belongs to (post-z-index promotion). // - `bounds` is already translated into the active layer's coordinate system // and pre-DPI, matching what the built-in shape procs expect. // - `render_data` is Clay's `CustomRenderData` for the element, exposing -// `backgroundColor`, `cornerRadius`, and the `customData` pointer the caller -// attached to `clay.CustomElementConfig.customData`. +// `backgroundColor` and `cornerRadius`. Its `customData` field has been +// unwrapped from the `Clay_Custom` envelope: it points at the user's own +// data (the value the user wrote into the `rawptr` variant), not at the +// `Clay_Custom` itself. If the union was zero-init (no variant set) or +// `customData` was originally nil, the callback receives nil. // // The callback must not call `new_layer` or `prepare_clay_batch`. Custom_Draw :: #type proc(layer: ^Layer, bounds: Rectangle, render_data: clay.CustomRenderData) @@ -771,33 +775,51 @@ ClayBatch :: struct { cmds: clay.ClayArray(clay.RenderCommand), } -// Magic-number-tagged struct that user app data points at via Clay's customData field. -// `prepare_clay_batch` recognizes these and routes them through a backdrop scope automatically. -// The user populates a `Backdrop_Marker`, points `clay.CustomElementConfig.customData` at it, -// and the integration walks the command stream, opening/closing scopes around contiguous -// backdrop runs. Magic-number sentinel chosen over a separate userData flag so the marker -// type stays self-describing in core dumps and in any non-Odin debugger view of the heap. +// Discriminated sum of everything `clay.CustomElementConfig.customData` is allowed to point +// at. levlib-defined variants (currently just `Backdrop_Marker`) are recognized by +// `prepare_clay_batch` and routed to the appropriate internal path; the `rawptr` variant is +// the escape hatch for user-defined custom drawing — `prepare_clay_batch` unwraps it before +// invoking `custom_draw` so the callback sees the user's pointer in `render_data.customData` +// exactly as if no wrapper were involved. +// +// Contract: `customData`, when non-nil, MUST point at storage holding a `Clay_Custom` +// value. The user owns that storage; its lifetime must span the Clay layout call and the +// matching `prepare_clay_batch` call. Pointing `customData` at a bare user struct violates +// the contract — the dispatcher will read its first bytes as a union tag and either route +// the draw incorrectly or panic on type assertion. There is no recovery path; this is a +// strict-discipline API by design. +// +// Construction notes (Odin implicit-conversion rules): +// - Backdrop variant: `bd: Clay_Custom = Backdrop_Marker{...}` works directly. +// Variant-to-union conversion is implicit. +// - User pointer: `up: Clay_Custom = rawptr(&my_struct)` — the explicit `rawptr(...)` is +// required because Odin does not chain `^T -> rawptr -> Clay_Custom` implicitly. A bare +// `up: Clay_Custom = &my_struct` is a compile error. +Clay_Custom :: union { + Backdrop_Marker, + rawptr, +} + +// Per-primitive parameters for a backdrop blur dispatched through the Clay integration. +// Embedded as a `Clay_Custom` variant; `prepare_clay_batch` walks the command stream, +// opens/closes a backdrop scope around contiguous backdrop runs, and feeds these to +// `backdrop_blur` via `dispatch_clay_backdrop`. The discriminant is the union tag — no +// in-band magic field needed (compiler-enforced). Backdrop_Marker :: struct { - magic: u32, sigma: f32, tint: Color, radii: Rectangle_Radii, feather_ppx: f32, } -// 'BDPT' in big-endian ASCII. Picked for greppability and to be obviously non-zero in -// uninitialized memory; user code that forgets to set the magic field gets routed through -// the regular custom_draw path and surfaces as "my custom draw never fired," not as a -// silent backdrop schedule. -BACKDROP_MARKER_MAGIC :: u32(0x42445054) - -// Returns true if this Clay render command represents a backdrop primitive. -// Identified by a magic-number sentinel in the first 4 bytes of customData. +// Returns true if this Clay render command represents a backdrop primitive — i.e. its +// `customData` points at a `Clay_Custom` whose active variant is `Backdrop_Marker`. is_clay_backdrop :: proc(cmd: ^clay.RenderCommand) -> bool { if cmd.commandType != .Custom do return false p := cmd.renderData.custom.customData if p == nil do return false - return (^Backdrop_Marker)(p).magic == BACKDROP_MARKER_MAGIC + _, ok := (^Clay_Custom)(p).(Backdrop_Marker) + return ok } // Dispatch a single non-backdrop Clay render command to the appropriate `draw` primitive. @@ -918,28 +940,46 @@ dispatch_clay_command :: proc( } rectangle(layer, bounds, BLANK, outline_color = color, outline_width = thickness, radii = radii) - case clay.RenderCommandType.Custom: if is_clay_backdrop(render_command) { - // The walker pre-filters backdrops into `dispatch_clay_backdrop` and never feeds - // them here; reaching this branch means either the walker logic is broken or the - // `customData` pointee mutated between the walker's `is_clay_backdrop` check and - // this re-check (heap corruption / lifetime bug in user-managed customData - // memory). Both are renderer-level bugs that warrant a hard failure rather than a - // silently-dropped panel. - log.panicf( - "backdrop marker reached dispatch_clay_command; either the prepare_clay_batch walker is misrouting commands or the customData pointee at %p was mutated mid-frame", - render_command.renderData.custom.customData, - ) - } else if custom_draw != nil { - custom_draw(layer, bounds, render_command.renderData.custom) - } else { - log.panicf("Received clay render command of type custom but no custom_draw proc provided.") + case clay.RenderCommandType.Custom: + // Copy the CustomRenderData by value so we can patch its `customData` field for the + // user callback without mutating Clay-owned memory. After unwrapping, the callback + // sees its own pointer in `render_data.customData`, identical to what it would see + // if `Clay_Custom` did not exist as an intermediary. + patched := render_command.renderData.custom + // Default to nil so a zero-init `Clay_Custom` (no variant set) and an originally-nil + // `customData` both surface to the callback as `customData = nil`. + patched.customData = nil + if custom_data_pointer := render_command.renderData.custom.customData; custom_data_pointer != nil { + switch custom_value in (^Clay_Custom)(custom_data_pointer)^ { + case Backdrop_Marker: // The walker pre-filters backdrops into `dispatch_clay_backdrop` and never feeds + // them here; reaching this branch means either the walker logic is broken or the + // `Clay_Custom` variant tag mutated between the walker's `is_clay_backdrop` check + // and this re-check (heap corruption / lifetime bug in user-managed customData + // memory). Both are renderer-level bugs that warrant a hard failure rather than a + // silently-dropped panel. + log.panicf( + "backdrop marker reached dispatch_clay_command; either the prepare_clay_batch walker is misrouting commands or the customData pointee at %p was mutated mid-frame", + render_command.renderData.custom.customData, + ) + case rawptr: patched.customData = custom_value } + } + if custom_draw != nil { + custom_draw(layer, bounds, patched) + } else if patched.customData != nil { + log.panicf( + "Received clay render command of type custom with non-nil user data but no custom_draw proc provided.", + ) + } } } // Dispatch a single backdrop Clay render command to `backdrop_blur` on the active layer. -// Caller guarantees a backdrop scope is open on `layer` so the underlying -// `append_or_extend_sub_batch` contract assertion is satisfied. +// Caller guarantees: +// - a backdrop scope is open on `layer` so the underlying `append_or_extend_sub_batch` +// contract assertion is satisfied; +// - the command's `customData` points at a `Clay_Custom` whose active variant is +// `Backdrop_Marker` (the walker has already verified this via `is_clay_backdrop`). //INTERNAL dispatch_clay_backdrop :: proc(layer: ^Layer, cmd: ^clay.RenderCommand) { bounds := Rectangle { @@ -948,7 +988,10 @@ dispatch_clay_backdrop :: proc(layer: ^Layer, cmd: ^clay.RenderCommand) { width = cmd.boundingBox.width, height = cmd.boundingBox.height, } - marker := (^Backdrop_Marker)(cmd.renderData.custom.customData) + // Type-asserting form (no `, ok`): panics loudly if the variant tag changed since + // `is_clay_backdrop`, which is the desired tripwire for a heap-corruption bug in + // user-managed customData. + marker := (^Clay_Custom)(cmd.renderData.custom.customData).(Backdrop_Marker) backdrop_blur( layer, bounds, diff --git a/draw/examples/hellope.odin b/draw/examples/hellope.odin index 59f51cc..694ab2c 100644 --- a/draw/examples/hellope.odin +++ b/draw/examples/hellope.odin @@ -287,6 +287,22 @@ hellope_custom :: proc() { value = 0.45, color = {200, 100, 50, 255}, } + + // `clay.CustomElementConfig.customData` is a rawptr; the Clay integration in `draw` + // requires it to point at a `Clay_Custom` value. The explicit `rawptr(...)` cast is + // necessary because Odin does not chain `^Gauge -> rawptr -> Clay_Custom` implicitly + // (variant-to-union and ^T-to-rawptr are each implicit on their own, but not stacked). + gauge_custom: draw.Clay_Custom = rawptr(&gauge) + gauge2_custom: draw.Clay_Custom = rawptr(&gauge2) + + // Backdrop variant: variant-to-union conversion is implicit, so no cast needed. + // `tint = draw.WHITE` is the no-op tint per the backdrop module's convention + // (matches `examples/backdrop.odin`'s "pure blur, no color" usage). + backdrop_custom: draw.Clay_Custom = draw.Backdrop_Marker { + sigma = 8, + tint = draw.WHITE, + } + spin_angle: f32 = 0 for { @@ -320,20 +336,37 @@ hellope_custom :: proc() { clay.Text("Custom Draw Demo", &text_config) } + // gauge1 is BEHIND the backdrop — the backdrop is declared as a floating CHILD + // of gauge1, pinned to gauge1's LeftTop and sized 300x30 so it covers exactly + // gauge1's footprint. Clay emits a floating child's render command after the + // parent's, so the stream order is gauge1 → backdrop → gauge2: gauge1's pixels + // land in `source_texture` before the bracket samples (visible as a blurred + // reflection inside the strip), and gauge2 is deferred-replayed by + // `prepare_clay_batch` after the bracket closes (renders crisp on top of the + // bracket output — unrelated to the strip since they don't overlap). if clay.UI()( { id = clay.ID("gauge"), layout = {sizing = {clay.SizingFixed(300), clay.SizingFixed(30)}}, - custom = {customData = &gauge}, + custom = {customData = &gauge_custom}, backgroundColor = {80, 80, 80, 255}, }, - ) {} + ) { + if clay.UI()( + { + id = clay.ID("backdrop"), + floating = {attachTo = .Parent, attachment = {parent = .LeftTop, element = .LeftTop}}, + layout = {sizing = {clay.SizingFixed(300), clay.SizingFixed(30)}}, + custom = {customData = &backdrop_custom}, + }, + ) {} + } if clay.UI()( { id = clay.ID("gauge2"), layout = {sizing = {clay.SizingFixed(300), clay.SizingFixed(30)}}, - custom = {customData = &gauge2}, + custom = {customData = &gauge2_custom}, backgroundColor = {80, 80, 80, 255}, }, ) {} @@ -353,6 +386,9 @@ hellope_custom :: proc() { } draw_custom :: proc(layer: ^draw.Layer, bounds: draw.Rectangle, render_data: clay.CustomRenderData) { + // `render_data.customData` has been unwrapped from the `Clay_Custom` envelope by + // `prepare_clay_batch` — it points at the Gauge directly, the same as it would have + // before the union refactor. gauge := cast(^Gauge)render_data.customData border_width: f32 = 2