Skip to content

Scoped buses + embeddable projects (effects slice) - #218

Closed
Yona-Appletree wants to merge 9 commits into
mainfrom
claude/composite-effects-planning-f4f51b
Closed

Scoped buses + embeddable projects (effects slice)#218
Yona-Appletree wants to merge 9 commits into
mainfrom
claude/composite-effects-planning-f4f51b

Conversation

@Yona-Appletree

Copy link
Copy Markdown
Member

Scoped buses + embeddable projects — the effects slice. Plan:
~/.photomancer/planning/lp2025/2026-07-27-composite-effects/.

From Studio you can now author an effect as its own small project, see
two shipped effects in an Effects gallery category, open one to
study it, and drop one into your own project — with controls promoted
from the effect's inner nodes appearing as knobs on its card.

The model (2 ADRs)

Scoped buses with writer-shadowing (docs/adr/2026-07-28-scoped-buses.md)
Channels are keyed (scope, name). Every project node introduces a named
scope around its children; playlist entries wrap each owned child in an
anonymous scope. Consumes resolve to the nearest enclosing scope with a
writer
(else root), so an effect inherits host time; produces always
write their own scope, so an effect cannot clobber a host channel. This
retires the playlist-entry suppression special case — it was a
degenerate one-channel version of the same idea.

Every project node mirrors its scope's visual.out as a produced
output, which is what makes an effect playlist-playable with zero
playlist changes. A non-root mirror also default-publishes to its
containing scope, so effects are drop-in.

Effects are projects (docs/adr/2026-07-28-effects-are-projects.md)
No new node kind: ProjectDef gains optional controls (aliases to a
direct child's slot, carrying no value of their own) plus
author/version/license. All skip-if-default, so existing artifacts
stay byte-identical and there is no format bump. The "Project cannot be a
child node" authoring guard is lifted.

Studio

Embedded projects render as an effect card: mirror preview, promoted
knobs, provenance line. Promotion is DTO-level aliasing — each control's
address is the inner child's slot, so edits, dirty state, and
bound-violet ride the standard write path with no forwarding layer. The
add picker grows a New Effect creator and an Effects section that
vendors a shipped effect into effects/<name>/ in one gesture.

Content

examples/effects/plasma (pure visual) and examples/effects/meteor
(compute sim + visual renderer, controls promoted from both children).
Both CC0; examples/effects/README.md notes all sample content is CC0
unless stated otherwise.

Verified live

Gallery chips filter; plasma opens with the bus showing root channels
bare next to scope-qualified plasma›visual.out; dragging a promoted
knob moves the child's slot (one underlying slot); importing meteor
yields 4 channels carrying plasma›visual.out and
meteor›visual.out
— two effects coexisting, neither clobbering the
other or root.

Findings folded in

  • meteor's shader indexed a uniform struct array with a runtime value.
    Fixed; it compile-fails on 4 of 5 targets. Notably the engine render
    test cannot gate this (host backend accepts it) — verified by
    restoring the break. Filetest added pinning the supported idiom on all
    five targets, plus a defect entry and a debt entry for the missing
    cross-target example gate.
  • inactive playlist entries were rendering every frame. Retiring
    suppression gave entry children their own scope channels, and the
    bus-graph probe resolves a value per listed channel — so it demanded
    every entry's shader on each refresh. Entry scopes are sinks by
    construction and are now filtered out. Regression test verified to fail
    without the fix.
  • dt clamp in meteor's sim throttled motion on slow frames only,
    which is why it looked fine on GPU and stuttered on the CPU tier.

Validation

just check, just test, just schema-check green; firmware packs at
91.49% of the app partition. Story baselines deliberately not captured
locally — validate-stories owns those.

Draft: awaiting the final review gate.

🤖 Generated with Claude Code

Yona-Appletree and others added 9 commits July 28, 2026 13:17
Channels are now keyed (scope, name): every project node introduces a
named bus scope, playlist entries wrap children in anonymous scopes
(retiring the suppress-entry-visual-defaults rule), consumes resolve to
the nearest enclosing scope with a writer (else root), produces always
write their own scope. Every project node's runtime mirrors its scope's
visual.out as produced output (playlist-pattern render forwarding), so
an embedded project is playlist-playable with zero playlist changes.

ADR: docs/adr/2026-07-28-scoped-buses.md (+ effects-are-projects ADR
covering the M2 model slice landing next).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rd lift

ProjectDef grows optional controls{name: PromotedControlDef} (alias to a
direct child's slot, no value of its own) plus author/version/license;
all skip-if-default so existing artifacts are byte-identical, schemas
additive with no format bump. The 'kind Project cannot be created as a
child node' authoring guard is lifted (effects-are-projects ADR); child
project.json format fields are tolerated and ignored (root-only probe);
effect_starter_files composes the New Effect folder starter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ffect source

An embedded project renders as an effect card: output-mirror hero,
promoted-control knobs whose addresses are the inner child's slots (the
standard address-routed write path carries edits, dirty, bound-violet),
broken aliases render disabled with Invalid, provenance line below. The
add picker grows an Effect source that expands the effect folder starter
at effects/<name>/ for both attach sites.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…list import

Two CC0 effect examples ship as workbench projects (preview rig wrapping
a vendorable effect folder); an engine test renders both through the
LpsGlsl compiler via their output mirrors. The gallery's kind chips are
real (Projects | Effects), and the add picker's Effects section vendors
a shipped effect into effects/<name>/ and attaches it in one gesture.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cts-planning-f4f51b

# Conflicts:
#	lp-core/lpc-model/src/lib.rs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nt index

drawMeteor indexed meteors[slot] with a runtime parameter; a uniform
array of structs has no element address resolvable at lower time, so
members must be read with constant indices at the call site and passed
in as scalars (the shape examples/events/shader.glsl already carries).
Measured: the runtime-indexed form compile-fails on 4 of 5 targets.

Every automated gate stayed green on the broken shader. The engine
render test runs the host backend, which accepts the construct — adding
a node-status assertion did not catch it either, so its comment now
says plainly what it cannot gate. Adds a filetest pinning the supported
idiom on all five targets, plus defect + debt entries for the missing
cross-target example gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A non-root project node's output mirror now carries a produced default
onto visual.out at fallback priority. Per rule 4 that lands in the
project NODE's own nearest scope (the parent's) — the node sits there;
only its children are inside the scope it introduces — so an effect is
drop-in while its interior stays isolated. Root is excluded: its
containing scope is the one its mirror reads.

Accepted consequence: embedding an effect where visual.out is already
driven leaves two equal-priority writers and resolves ambiguous until
the author picks one. The nested-project test now pins the writer set
and that ambiguity rather than a winner. The workbench fixtures drop
their explicit node: refs and ride the default, which is also what makes
the effect gallery thumbnails preview.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The bus-graph probe resolves a value for every listed channel. Retiring
the playlist suppression rule gave each entry child its own anonymous
Entry-scope visual.out, so those channels started appearing — and the
probe then DEMANDED every entry child's shader on each bus refresh,
rendering inactive entries every frame. Found live on examples/fyeah-sign,
where the blast entry ran constantly instead of only while active.

Entry scopes are sinks by construction (the playlist reads each child's
produced output slot directly, bus-independent), so they are filtered out
of the graph entirely — restoring pre-change bus-pane parity for
playlists while named project scopes stay visible. Regression test pinned
and verified to fail without the filter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Yona-Appletree

Copy link
Copy Markdown
Member Author

Closing per ADR docs/adr/2026-07-31-module-model-supersedes-composite-effects-spike.md (Accepted, on main), decision 2: "PR #218 is not merged. The branch is kept as a harvest source and the PR closed once harvesting is complete."

This was never going to merge, and closing it now says so out loud instead of leaving it to look like a stalled review. The converged model in docs/design/modules.md deletes the four mechanisms this branch is built on — PromotedControlDef value-less aliasing, the transient BusScopes side-table, display-string scope on the wire, and root special-casing — so merging would mean building the new model through partial reverts of the old one, with the depth-2 resolution defect live in the meantime. That alternative is recorded and rejected in the ADR.

The branch is NOT deleted and must not be. claude/composite-effects-planning-f4f51b @ 7014d3897 stays on the remote as the harvest source; this diff also remains fully browsable here. Harvest lands at roadmap M11 (Studio module face) in the modules/buses/panels plan.

What survives, and where it goes

Spike piece Fate
Writer-shadowing resolution (produce-local / consume-inherit) Kept → R4/R5
Anonymous playlist-entry scopes Kept, promoted → sink scopes as a modeled property (R2)
Output mirror + non-root fallback publish Kept, generalized → R7 (authored exports added)
ScopedChannel key through QueryKey / binding index / resolver Kept as mechanism (resolver stays scope-dumb)
Effect-face component + panel widget reuse Un-harvested — due at M11
examples/effects/plasma, examples/effects/meteor Un-harvested — future example modules
Cross-target uniform-indexing filetest + defect/debt entries Already landed on main independently

Dropped outright: PromotedControlDef aliasing (promotion must be automatic, and meta must follow the currently-bound slot — aliases can express neither), the BusScopes side-table, display-string wire scope, "resolve to root, surface unfilled", and the ~7 root-special-case sites.

The "effects are projects" insight survives unchanged as "effects are modules" — a category, not a structure.

Two things the implementation must not inherit

  1. Do not inherit the disabled probe test. binding_graph_probe_reports_bindings_channels_and_values in lp-core/lpc-engine/src/engine/project_read_probes.rs lost its #[test] attribute on this branch and is a silently dead private fn.
  2. Pin both defect classes with real tests: the depth-2 resolution shape (modules.md E5 — a consumer beside an embedded effect resolves to root's visual instead of its sibling's) and sink-scope no-demand (R2.2).

Deliberately closing slightly ahead of the ADR's "once harvesting is complete" — M11 has not run yet. A closed PR harvests exactly as well as an open one, and this had been sitting open as a bookmark with no CI and no merge path since 2026-07-31.

Plan: ~/.photomancer/planning/lp2025/2026-07-31-1002-modules-buses-panels/

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