Clay custom dispatch now uses a union instead of magic marker
This commit is contained in:
+80
-37
@@ -754,14 +754,18 @@ measure_text_clay :: proc "c" (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Called for each Clay `RenderCommandType.Custom` render command that
|
// 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).
|
// - `layer` is the layer the command belongs to (post-z-index promotion).
|
||||||
// - `bounds` is already translated into the active layer's coordinate system
|
// - `bounds` is already translated into the active layer's coordinate system
|
||||||
// and pre-DPI, matching what the built-in shape procs expect.
|
// and pre-DPI, matching what the built-in shape procs expect.
|
||||||
// - `render_data` is Clay's `CustomRenderData` for the element, exposing
|
// - `render_data` is Clay's `CustomRenderData` for the element, exposing
|
||||||
// `backgroundColor`, `cornerRadius`, and the `customData` pointer the caller
|
// `backgroundColor` and `cornerRadius`. Its `customData` field has been
|
||||||
// attached to `clay.CustomElementConfig.customData`.
|
// 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`.
|
// The callback must not call `new_layer` or `prepare_clay_batch`.
|
||||||
Custom_Draw :: #type proc(layer: ^Layer, bounds: Rectangle, render_data: clay.CustomRenderData)
|
Custom_Draw :: #type proc(layer: ^Layer, bounds: Rectangle, render_data: clay.CustomRenderData)
|
||||||
@@ -771,33 +775,51 @@ ClayBatch :: struct {
|
|||||||
cmds: clay.ClayArray(clay.RenderCommand),
|
cmds: clay.ClayArray(clay.RenderCommand),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Magic-number-tagged struct that user app data points at via Clay's customData field.
|
// Discriminated sum of everything `clay.CustomElementConfig.customData` is allowed to point
|
||||||
// `prepare_clay_batch` recognizes these and routes them through a backdrop scope automatically.
|
// at. levlib-defined variants (currently just `Backdrop_Marker`) are recognized by
|
||||||
// The user populates a `Backdrop_Marker`, points `clay.CustomElementConfig.customData` at it,
|
// `prepare_clay_batch` and routed to the appropriate internal path; the `rawptr` variant is
|
||||||
// and the integration walks the command stream, opening/closing scopes around contiguous
|
// the escape hatch for user-defined custom drawing — `prepare_clay_batch` unwraps it before
|
||||||
// backdrop runs. Magic-number sentinel chosen over a separate userData flag so the marker
|
// invoking `custom_draw` so the callback sees the user's pointer in `render_data.customData`
|
||||||
// type stays self-describing in core dumps and in any non-Odin debugger view of the heap.
|
// 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 {
|
Backdrop_Marker :: struct {
|
||||||
magic: u32,
|
|
||||||
sigma: f32,
|
sigma: f32,
|
||||||
tint: Color,
|
tint: Color,
|
||||||
radii: Rectangle_Radii,
|
radii: Rectangle_Radii,
|
||||||
feather_ppx: f32,
|
feather_ppx: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 'BDPT' in big-endian ASCII. Picked for greppability and to be obviously non-zero in
|
// Returns true if this Clay render command represents a backdrop primitive — i.e. its
|
||||||
// uninitialized memory; user code that forgets to set the magic field gets routed through
|
// `customData` points at a `Clay_Custom` whose active variant is `Backdrop_Marker`.
|
||||||
// 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.
|
|
||||||
is_clay_backdrop :: proc(cmd: ^clay.RenderCommand) -> bool {
|
is_clay_backdrop :: proc(cmd: ^clay.RenderCommand) -> bool {
|
||||||
if cmd.commandType != .Custom do return false
|
if cmd.commandType != .Custom do return false
|
||||||
p := cmd.renderData.custom.customData
|
p := cmd.renderData.custom.customData
|
||||||
if p == nil do return false
|
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.
|
// 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)
|
rectangle(layer, bounds, BLANK, outline_color = color, outline_width = thickness, radii = radii)
|
||||||
case clay.RenderCommandType.Custom: if is_clay_backdrop(render_command) {
|
case clay.RenderCommandType.Custom:
|
||||||
// The walker pre-filters backdrops into `dispatch_clay_backdrop` and never feeds
|
// Copy the CustomRenderData by value so we can patch its `customData` field for the
|
||||||
// them here; reaching this branch means either the walker logic is broken or the
|
// user callback without mutating Clay-owned memory. After unwrapping, the callback
|
||||||
// `customData` pointee mutated between the walker's `is_clay_backdrop` check and
|
// sees its own pointer in `render_data.customData`, identical to what it would see
|
||||||
// this re-check (heap corruption / lifetime bug in user-managed customData
|
// if `Clay_Custom` did not exist as an intermediary.
|
||||||
// memory). Both are renderer-level bugs that warrant a hard failure rather than a
|
patched := render_command.renderData.custom
|
||||||
// silently-dropped panel.
|
// Default to nil so a zero-init `Clay_Custom` (no variant set) and an originally-nil
|
||||||
log.panicf(
|
// `customData` both surface to the callback as `customData = nil`.
|
||||||
"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",
|
patched.customData = nil
|
||||||
render_command.renderData.custom.customData,
|
if custom_data_pointer := render_command.renderData.custom.customData; custom_data_pointer != nil {
|
||||||
)
|
switch custom_value in (^Clay_Custom)(custom_data_pointer)^ {
|
||||||
} else if custom_draw != nil {
|
case Backdrop_Marker: // The walker pre-filters backdrops into `dispatch_clay_backdrop` and never feeds
|
||||||
custom_draw(layer, bounds, render_command.renderData.custom)
|
// them here; reaching this branch means either the walker logic is broken or the
|
||||||
} else {
|
// `Clay_Custom` variant tag mutated between the walker's `is_clay_backdrop` check
|
||||||
log.panicf("Received clay render command of type custom but no custom_draw proc provided.")
|
// 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.
|
// 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
|
// Caller guarantees:
|
||||||
// `append_or_extend_sub_batch` contract assertion is satisfied.
|
// - 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
|
//INTERNAL
|
||||||
dispatch_clay_backdrop :: proc(layer: ^Layer, cmd: ^clay.RenderCommand) {
|
dispatch_clay_backdrop :: proc(layer: ^Layer, cmd: ^clay.RenderCommand) {
|
||||||
bounds := Rectangle {
|
bounds := Rectangle {
|
||||||
@@ -948,7 +988,10 @@ dispatch_clay_backdrop :: proc(layer: ^Layer, cmd: ^clay.RenderCommand) {
|
|||||||
width = cmd.boundingBox.width,
|
width = cmd.boundingBox.width,
|
||||||
height = cmd.boundingBox.height,
|
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(
|
backdrop_blur(
|
||||||
layer,
|
layer,
|
||||||
bounds,
|
bounds,
|
||||||
|
|||||||
@@ -287,6 +287,22 @@ hellope_custom :: proc() {
|
|||||||
value = 0.45,
|
value = 0.45,
|
||||||
color = {200, 100, 50, 255},
|
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
|
spin_angle: f32 = 0
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@@ -320,20 +336,37 @@ hellope_custom :: proc() {
|
|||||||
clay.Text("Custom Draw Demo", &text_config)
|
clay.Text("Custom Draw Demo", &text_config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// gauge1 is BEHIND the backdrop — the backdrop is declared as a floating CHILD
|
||||||
|
// 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()(
|
if clay.UI()(
|
||||||
{
|
{
|
||||||
id = clay.ID("gauge"),
|
id = clay.ID("gauge"),
|
||||||
layout = {sizing = {clay.SizingFixed(300), clay.SizingFixed(30)}},
|
layout = {sizing = {clay.SizingFixed(300), clay.SizingFixed(30)}},
|
||||||
custom = {customData = &gauge},
|
custom = {customData = &gauge_custom},
|
||||||
backgroundColor = {80, 80, 80, 255},
|
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()(
|
if clay.UI()(
|
||||||
{
|
{
|
||||||
id = clay.ID("gauge2"),
|
id = clay.ID("gauge2"),
|
||||||
layout = {sizing = {clay.SizingFixed(300), clay.SizingFixed(30)}},
|
layout = {sizing = {clay.SizingFixed(300), clay.SizingFixed(30)}},
|
||||||
custom = {customData = &gauge2},
|
custom = {customData = &gauge2_custom},
|
||||||
backgroundColor = {80, 80, 80, 255},
|
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) {
|
draw_custom :: proc(layer: ^draw.Layer, bounds: draw.Rectangle, render_data: clay.CustomRenderData) {
|
||||||
|
// `render_data.customData` has been unwrapped from the `Clay_Custom` envelope by
|
||||||
|
// `prepare_clay_batch` — it points at the Gauge directly, the same as it would have
|
||||||
|
// before the union refactor.
|
||||||
gauge := cast(^Gauge)render_data.customData
|
gauge := cast(^Gauge)render_data.customData
|
||||||
|
|
||||||
border_width: f32 = 2
|
border_width: f32 = 2
|
||||||
|
|||||||
Reference in New Issue
Block a user