Skip to content

fix(capi): cube node shape rendered black canvas (issue #47)#48

Merged
willwade merged 1 commit into
mainfrom
fix/cube-black-canvas-capi
Jul 25, 2026
Merged

fix(capi): cube node shape rendered black canvas (issue #47)#48
willwade merged 1 commit into
mainfrom
fix/cube-black-canvas-capi

Conversation

@willwade

Copy link
Copy Markdown

Problem

When LP_SHAPE_TYPE == CUBE (6), the canvas went black via the C API — only the background clear-screen fired, no nodes visible. The app didn't crash.

Root cause

CDasherViewSquare in cube mode drives rendering through Screen()->DrawCube (per node face), Screen()->Draw3DLabel (text), Screen()->DrawProjectedRectangle (crosshair bar), and Screen()->FinishRender3D (composite). All four are virtual on CDasherScreen with empty no-op defaults (DasherScreen.h:98-106).

The C API's CommandScreen (CAPI.cpp) — the screen adapter that translates DasherCore rendering into the flat int[] command buffer consumed by every frontend — never overrode them. So zero draw commands were emitted for cube nodes. Confirmed by opcode histogram in cube mode: 0:10 2:10 4:10 6:10 per 10 frames (just clear-screen + one decorative rect/line), with zero labels.

Same class of bug as the circle crashes (#46) — a rendering mode the C-API path was never tested with.

Fix (Option A from the issue, localized to the C-API screen)

Override the 3D hooks in CommandScreen to translate cube drawing into the existing flat command format (no new opcodes):

  • DrawCube → opcode 4 (filled front-face rect) + opcode 3 (outline when requested)
  • Draw3DLabel → opcode 5 (text), reusing DrawString
  • DrawProjectedRectangle → opcode 4 (the cube-mode crosshair bar)
  • FinishRender3D → keeps the base no-op (nothing to composite in 2D)

The flat buffer has no 3D backend, so cube mode renders as flat shaded rectangles over this API (the issue explicitly accepts losing the 3D extrusion: "simpler but loses the cube visual").

One clean interface change

DrawProjectedRectangle previously took no colour, and CDasherScreen has no palette access (only the view has). I added a color argument supplied by the view from its palette. No in-tree overrides existed and the only caller is the cube path (verified by full-repo grep + clean build of all targets). Out-of-tree 3D screens that override it need a one-line update to accept the colour.

Why not Option B (capability check + flat fallback)?

Both approaches produce flat rectangles via the command buffer; the only real difference is where the fallback lives. Option A is localized to CAPI.cpp (no core view logic change), reuses the cube path's blanking/margin behaviour, and keeps the interface improvement (passing the crosshair colour).

Docs

dasher.h: documented opcode 6 (set line width — already emitted by Polyline but undocumented) and noted that cube mode maps to opcode 3/4/5.

Test

test_cube_geometry.cpp — drives cube mode via the C API and asserts that filled rects (cube faces) and text labels are emitted. Fails on unpatched code (0 labels, ~1 decorative rect/frame) and passes after.

Verification

  • clang-format clean (--Werror dry-run)
  • Debug build: cube/circle/view/draw tests pass
  • Release build: cube/circle/draw_snapshot tests pass
  • dasher_draw_snapshot_tests (golden) unaffected — default-shape rendering unchanged

…mplements 3D draw hooks)

CUBE node shape (LP_SHAPE_TYPE == 6) rendered an empty/black canvas via the C API: CDasherViewSquare drives cube rendering through Screen()->DrawCube / Draw3DLabel / DrawProjectedRectangle / FinishRender3D, and CommandScreen (the C-API screen adapter) never overrode them — the base-class defaults are all no-ops, so zero draw commands were emitted for cube nodes (only the clear-screen fired). Fixes issue #47.

Fix (Option A from the issue, localized to the C-API screen): override the 3D hooks in CommandScreen to translate cube drawing into the existing flat command format — DrawCube emits opcode 4 (filled front-face rect) + opcode 3 (outline); Draw3DLabel emits opcode 5 (text); DrawProjectedRectangle emits opcode 4 (cube-mode crosshair bar). FinishRender3D stays the base no-op (nothing to composite in 2D). The flat buffer carries no 3D extrusion, so cube mode renders as flat shaded rectangles over this API.

DrawProjectedRectangle previously took no colour and CDasherScreen has no palette access, so it could not render the crosshair bar correctly. Added a colour argument (supplied by the view from its palette); no in-tree overrides existed and the only caller is the cube path. Out-of-tree 3D screens that override it need a one-line update to accept the colour.

Documented opcode 6 (set line width, already emitted but undocumented) and the cube-mode command mapping in dasher.h.

Test: test_cube_geometry.cpp confirms cube mode now emits filled rects (cube faces) and text labels instead of just clear-screen; fails on the unpatched code (0 labels, ~1 decorative rect/frame).
Signed-off-by: will wade <willwade@gmail.com>
@willwade
willwade merged commit d48c318 into main Jul 25, 2026
14 checks passed
@willwade
willwade deleted the fix/cube-black-canvas-capi branch July 25, 2026 08:05
willwade added a commit that referenced this pull request Jul 25, 2026
#49)

Cube mode (LP_SHAPE_TYPE == 6) reached the C API in #48 but looked identical to flat rectangles: DrawCube discarded the CubeDepthLevel and emitted one opcode-4 rect per node. Worse, the multi-face shaded-rectangle emulation I tried first does not actually work — Dasher renders depth-first, so each child's flat front face paints over the parent's shaded top/right faces, leaving only leaf nodes with any visible 3D. The flat command buffer (painter's algorithm only, no depth buffer) cannot replicate GTK/Windows cube mode. Fixes issue #49 (revised per review).

Fix (Option B from the issue): DrawCube now emits a dedicated opcode-7 record carrying the raw cube data — [7, x1, y1, x2, y2, extrusionLevel, fillARGB, outlineARGB, thickness] (9 ints) — so the depth info survives the buffer intact. Frontends render cube mode in two passes: (1) opcodes 0-6 as the flat layout, (2) opcode-7 cubes composited as a 3D overlay on top with their own graphics API. Opcode 7 is the only variable-length command (9 ints vs 6); parsers read the opcode first and advance accordingly.

Docs: dasher.h and docs/C_API.md document opcode 7, the variable-length parse rule, and the two-pass rendering model (with a parse example). Updated the 'divide by 6' notes.

Tests: cube nodes now arrive as opcode 7 (not opcode 4); labels as opcode 5. Added a case verifying the per-node extrusionLevel varies across the tree (>=2 distinct values) — i.e. the depth data is no longer discarded. The parse helper handles the 9-int stride.
Signed-off-by: will wade <willwade@gmail.com>
willwade added a commit that referenced this pull request Jul 25, 2026
#49)

Cube mode (LP_SHAPE_TYPE == 6) reached the C API in #48 but looked identical to flat rectangles: DrawCube discarded the CubeDepthLevel and emitted one opcode-4 rect per node. A shaded-rectangle emulation does not work either — Dasher renders depth-first, so each child's flat front face paints over the parent's shaded faces, leaving only leaf nodes with any visible 3D. The flat command buffer (painter's algorithm only, no depth buffer) cannot replicate GTK/Windows cube mode. Fixes issue #49 (revised per review).

Fix (Option B from the issue): DrawCube emits a dedicated opcode-7 record carrying the raw cube data so the depth info survives the buffer intact. Frontends render cube mode in two passes: (1) opcodes 0-6 as the flat layout, (2) opcode-7 cubes composited as a 3D overlay on top via the frontend's own graphics API.

Encoding: opcode 7 occupies TWO 6-int slots (12 ints), both prefixed with 7 — slot 1: [7, x1, y1, x2, y2, extrusionLevel], slot 2: [7, fillARGB, outlineARGB, thickness, 0, 0]. This keeps the buffer uniformly 6-int divisible: a parser advancing 6 ints per slot never desyncs, and frontends that don't know opcode 7 just see two unknown slots per cube and skip them (graceful degradation). Cost is 3 unused ints per cube — negligible (~50-200 visible nodes in a >=10K-int buffer).

Docs: dasher.h and docs/C_API.md document opcode 7, the two-slot layout, the two-pass rendering model, and a parse example. Draw3DLabel still emits opcode 5 and DrawProjectedRectangle opcode 4 (crosshair bar), so the flat-layout pass already has labels + crosshair.

Tests: cube nodes arrive as opcode 7 (not opcode 4); labels as opcode 5. Added cases verifying (a) the buffer stays 6-int divisible (cc % 6 == 0) and (b) the per-node extrusionLevel varies across the tree (>=2 distinct values) — i.e. the depth data is no longer discarded. The parse helper advances 12 ints for opcode 7.
Signed-off-by: will wade <willwade@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant