feat(capi): Strand 2 custom-rendering API (RFC 0013)#51
Conversation
Adds the second rendering strand from governance RFC 0013: a frontend can query the visible node tree each frame and render it itself (3D cubes, VR/spatial, custom visualisations, accessibility) instead of consuming the flat int[] command buffer. Strands coexist — a frontend can use the command buffer for most rendering and drop into Strand 2 for specific features. New C API: dasher_set_visible_nodes_enabled (opt-in; off by default so Strand 1 frontends pay zero overhead), dasher_get_visible_nodes (depth-first visible node tree with pre-computed screen bounds, colours, labels, depth, game flag), dasher_get_viewport (crosshair + visible Y range + canvas size). dasher_node_info / dasher_viewport carry a caller-set struct_size field for ABI evolution. Capture happens inside CDasherViewSquare::NewRender/DisjointRender at each node actually drawn, so the captured set matches exactly what the command buffer draws for the same frame (Strand 1/Strand 2 parity). This is the proper fix for cube mode (issue #49) — frontends now render real 3D cubes from node data + their own graphics pipeline, replacing the withdrawn opcode-7 experiment (PR #50). Minor core additions: CDasherNode::IsSymbolNode() virtual (GetAlphSymbol() throws on the base class, so the C boundary gates on IsSymbolNode rather than using RTTI); CDasherView gains VisibleNode struct + SetVisibleNodeCapture/IsVisibleNodeCaptureEnabled/GetVisibleNodes virtuals. Tests (test_strand2_nodes.cpp): disabled-by-default + enable; depth-first preorder invariant; Strand 1/Strand 2 parity (every captured node bound matches an opcode-4 rect from the same frame); labels/symbols populated; viewport matches constants + canvas size; struct_size ABI guard rejects undersized caller structs; bounds are monotone. Signed-off-by: will wade <willwade@gmail.com>
Adds a Custom Rendering section to docs/C_API.md covering dasher_set_visible_nodes_enabled / dasher_get_visible_nodes / dasher_get_viewport, the dasher_node_info / dasher_viewport structs, the opt-in capture model, Strand 1/2 parity, and a render example. Signed-off-by: will wade <willwade@gmail.com>
…Rule 4) GetAlphSymbol() throws on the base CDasherNode class and the IsSymbolNode() fast-path guard is not a substitute for the C-boundary exception guard — a thrown exception crossing extern "C" aborts the process (review feedback on #51). Wrap the whole body of dasher_get_visible_nodes (and, for consistency, dasher_get_viewport and dasher_set_visible_nodes_enabled) in try { ... } catch (...) { return -1; } per CONTRIBUTING Rule 4. IsSymbolNode() is kept as the common no-throw fast path; the catch is the safety net. Test: add a long-driven-session regression that asserts dasher_get_visible_nodes always returns a sane value and never lets an exception escape — would SIGABRT without the guard. Signed-off-by: will wade <willwade@gmail.com>
|
Good catch — fixed. You're right that the Change: wrapped the entire body of Test added: All 8 strand2 tests pass locally (227 assertions). CI re-running on the push; leaving the PR open for the Dasher-Apple team review as requested (no merge). |
…herNode*) Root cause of the recurring crash (review feedback on #51): VisibleNode stored a raw CDasherNode* captured during Render() and dasher_get_visible_nodes dereferenced it after the frame returned. The model can free/mutate nodes between the capture and the query, so those pointers dangled — the IsSymbolNode/GetAlphSymbol try/catch only moved the crash one accessor along (getLabel/GetFlag on a freed node). Fix: make VisibleNode a fully self-contained snapshot. CaptureNode now resolves every value — symbol (via IsSymbolNode + GetAlphSymbol), has_children, is_game_node, fill/outline colours, label text (copied), depth, clipped screen bounds — while the node is guaranteed alive, during the render pass. The C API query just copies pre-resolved fields; no CDasherNode* is stored or dereferenced, so node lifetime between frame and query no longer matters. Verified under ASan + UBSan (all 9 strand2 tests clean, no heap-use-after-free). Added regression test: drive, then dasher_reset() (frees the node tree the snapshot came from), then query — must not crash. The boundary try/catch is retained as defense-in-depth. Signed-off-by: will wade <willwade@gmail.com>
|
You were right — that was the real root cause, and the try/catch only moved the crash one accessor down. Fixed properly this time. Root cause: Fix:
The C API query just copies these pre-resolved fields into Verification:
CI re-running; PR still open for the Dasher-Apple review (no merge). |
Implements Strand 2 from governance RFC 0013 (dasher-project/governance#20): a frontend can query the visible node tree each frame and render it itself — 3D cubes, VR/spatial layouts, custom visualisations, accessibility views — instead of consuming the flat
int[]command buffer. Strands coexist.This is the proper fix for cube mode (issue #49): frontends render real 3D cubes from node data + their own graphics pipeline, replacing the withdrawn opcode-7 experiment (#50, closed unmerged).
New C API (
dasher.h)dasher_node_info:struct_size, Dasher-Y range,symbol(-1 for group/control),has_children,depth(tree depth — cube-extrusion source),is_game_node, clipped screen bounds,fill_argb/outline_argb,label_index.dasher_viewport: crosshair (fixed at origin), visible Y range, canvas size.struct_sizefield for ABI evolution (per the RFC).dasher_frame/dasher_get_visible_nodescall.Opt-in by default: Strand 1 frontends that never call
dasher_set_visible_nodes_enabledpay zero overhead — the per-node recording only runs when capture is on.How it's captured
Recording happens inside
CDasherViewSquare::NewRender/DisjointRender, at each node actually drawn. So the captured node set matches exactly what the command buffer draws for the same frame (Strand 1/Strand 2 parity). The bounds use the same clip formula as theOVERLAPPING_RECTANGLEdraw (the parity reference shape).Minor core additions:
CDasherNode::IsSymbolNode()virtual —GetAlphSymbol()throws on the base class (onlyCSymbolNodeoverrides), so the C boundary gates onIsSymbolNode()rather than RTTI or try/catch.CDasherViewgains aVisibleNodestruct +SetVisibleNodeCapture/IsVisibleNodeCaptureEnabled/GetVisibleNodesvirtuals (default no-ops; onlyCDasherViewSquarerecords).Tests (
test_strand2_nodes.cpp)depth[i] <= depth[i-1]+1, first node depth 0)dasher_frame()for the same frame (the key invariant)struct_sizeABI guard rejects undersized caller structsVerification
--Werrordry-run)draw_snapshot,draw_command) unchanged — capture is off by default, so the command-buffer path is untouchedOpen follow-ups (not in this PR)
dasher_get_palette_colors. v1 ships per-node colours only.