Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/explanation/architecture/moonlight.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ A **Layer** (a MoonModule, child of Effects) owns:

A layer can have **multiple effects**. Each effect writes to the buffer sequentially in its listed order, overwriting or adding to the previous, so the effects stack (a base-color effect followed by a sparkle effect).

A layer applies **all its enabled modifiers as a chain** during the mapping build (`Layer::rebuildLUT`): each modifier is a coordinate fold, and they compose in child order (M₁∘M₂∘…). Modifiers are **reorderable** in the UI, and order is meaningful (a multiply-then-checkerboard mask differs from checkerboard-then-multiply, as mirror-then-rotate differs from rotate-then-mirror). The fold contract (the three hooks, the physical→logical build, the live pass) is documented in [ModifierBase](../../../src/light/modifiers/ModifierBase.h).
A layer applies **all its enabled static modifiers as a chain** during the mapping build (`Layer::rebuildLUT`): each modifier is a coordinate fold, and they compose in child order (M₁∘M₂∘…). A live modifier runs its pass every frame instead, after all effects, on the logical buffer the static mapping then carries to the lights, whatever its position among the children. Modifiers are **reorderable** in the UI, and order is meaningful among the static ones (a multiply-then-checkerboard mask differs from checkerboard-then-multiply). The fold contract (the three hooks, the physical→logical build, the live pass) is documented in [ModifierBase](../../../src/light/modifiers/ModifierBase.h).

Each layer references the shared Layouts. The layer builds its mapping by walking the Layouts container's **physical** coordinates and folding each through the static modifier chain to its logical cell. N physical lights folding onto one logical cell is the fan-out, as in a Multiply kaleidoscope, so the build never produces a fan-out overflow. Different layers in Effects can have different modifiers, producing different mappings from the same Layouts.

Expand Down Expand Up @@ -186,7 +186,7 @@ Two traps worth naming. A quantity already gated by wallclock must not ALSO be s
**An effect renders a pattern; it does not transform geometry.** When migrating or adding an effect, strip out anything that is a *modifier* (mirroring, tiling, rotation, scrolling/offset, a kaleidoscope fold, masking, any remap of *where* pixels land), and add it as a separate [modifier](#modifiers) instead. WLED (and other sources we port from) routinely fold these into the effect's own loop (a "mirror" checkbox, a "2D" rotation, a built-in pinwheel), because WLED has no modifier concept; we do. Keeping them out of the effect is what lets any effect compose with any modifier (the same RotateModifier rotates Fire, Noise, or a network-received frame) instead of every effect re-implementing its own half-baked mirror. The test: an effect's `tick()` should only *write colors into the logical buffer for its own coordinates*; if it's reading or rewriting positions to move/fold/duplicate the image, that behavior belongs in a modifier. (This is the light-domain face of *Complexity lives in core; domain modules stay simple*: geometry transforms are the modifier's job, shared once, not duplicated into every effect.)
## Modifiers

A modifier (MoonModule) lives inside a layer alongside its effects. Modifiers expose a virtual interface: the Layer calls modifier methods without knowing the concrete type (no `dynamic_cast`). A layer applies **all** its enabled modifiers as a chain, in child order, each a coordinate fold composed into one mapping (see [Effects and Layer](#effects-and-layer)).
A modifier (MoonModule) lives inside a layer alongside its effects. Modifiers expose a virtual interface: the Layer calls modifier methods without knowing the concrete type (no `dynamic_cast`). A layer folds **all** its enabled static modifiers as a chain, in child order, each a coordinate fold composed into one mapping, and runs the live ones after the effects each frame (see [Effects and Layer](#effects-and-layer)).

A modifier is a coordinate transform, applied in one of two ways (the fold contract is in [ModifierBase](../../../src/light/modifiers/ModifierBase.h)):

Expand Down
2 changes: 1 addition & 1 deletion docs/explanation/architecture/moonmodule.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ Each MoonModule has two documentation surfaces under `docs/moonmodules/`. An end

## Disabling a module releases its resources

`applyState()` is the sole resource-lifecycle router: it calls `onBuildState()` on an effectively-enabled node and `teardown()` otherwise, recursing the tree so each child is routed by its own effective-enabled state. A module's `onBuildState()` therefore contains no `enabled()` check. It builds, and core decides whether to build or tear down.
`applyState()` is the sole resource-lifecycle router: it calls `prepare()` on an effectively-enabled node and `release()` otherwise, recursing the tree so each child is routed by its own effective-enabled state. A module's `prepare()` therefore contains no `enabled()` check. It builds, and core decides whether to build or tear down.

`effectivelyEnabled()` walks the parent chain, so a disabled parent releases its whole subtree: a disabled Layer frees its effects' heap. An ancestor that does not respect enabling, such as Network or HttpServer, stays neutral and never forces a child on.

Expand Down
2 changes: 1 addition & 1 deletion docs/help/help.json
Original file line number Diff line number Diff line change
Expand Up @@ -5171,7 +5171,7 @@
"title": "Modifiers",
"base": "ModifierBase",
"line": "A modifier sits between an [effect](https://moonmodules.org/MoonLight/moonmodules/light/effects.html) and the output: it reshapes where the pixels land, or masks them, without changing what the effect draws.",
"summary": "Modifiers compose: a [Layer](https://moonmodules.org/MoonLight/moonmodules/light/supporting.html#layer) applies its whole stack in child order, each reshaping the result of the one below. A static modifier is baked into the layer's mapping at rebuild. A dynamic one also runs a pass every frame, and an animated one rebuilds its static fold on its own clock.\n\nA modifier folds coordinates rather than drawing, so it uses little of the shared [power functions](https://moonmodules.org/MoonLight/moonmodules/light/power-functions.html). Each card's emoji are its tags, as the [tag emoji legend](https://moonmodules.org/MoonLight/explanation/architecture/index.html#tag-emoji-legend) explains."
"summary": "Modifiers compose: a [Layer](https://moonmodules.org/MoonLight/moonmodules/light/supporting.html#layer) folds its static modifiers in child order, each reshaping the result of the one below, into its mapping at rebuild. A dynamic modifier instead runs a pass every frame after all effects, on the logical buffer the static mapping then carries to the lights, whatever its place, and an animated one rebuilds its static fold on its own clock.\n\nA modifier folds coordinates rather than drawing, so it uses little of the shared [power functions](https://moonmodules.org/MoonLight/moonmodules/light/power-functions.html). Each card's emoji are its tags, as the [tag emoji legend](https://moonmodules.org/MoonLight/explanation/architecture/index.html#tag-emoji-legend) explains."
},
"light/layouts.md": {
"title": "Layouts",
Expand Down
4 changes: 2 additions & 2 deletions src/light/modifiers/ModifierBase.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ namespace mm {

/// A modifier sits between an [effect](https://moonmodules.org/MoonLight/moonmodules/light/effects.html) and the output: it reshapes where the pixels land, or masks them, without changing what the effect draws.
///
/// Modifiers compose: a @ref Layer applies its whole stack in child order, each reshaping the result of the one below. A static modifier is baked into the layer's mapping at rebuild. A dynamic one also runs a pass every frame, and an animated one rebuilds its static fold on its own clock.
/// Modifiers compose: a @ref Layer folds its static modifiers in child order, each reshaping the result of the one below, into its mapping at rebuild. A dynamic modifier instead runs a pass every frame after all effects, on the logical buffer the static mapping then carries to the lights, whatever its place, and an animated one rebuilds its static fold on its own clock.
///
/// A modifier folds coordinates rather than drawing, so it uses little of the shared [power functions](https://moonmodules.org/MoonLight/moonmodules/light/power-functions.html). Each card's emoji are its tags, as the [tag emoji legend](https://moonmodules.org/MoonLight/explanation/architecture/index.html#tag-emoji-legend) explains.
///
/// @technical
///
/// A layer builds its mapping by folding every physical light through each enabled modifier, and the whole chain collapses into one table, so the per-frame render stays a single lookup.
/// A layer builds its mapping by folding every physical light through each enabled static modifier, and the whole chain collapses into one table, so the per-frame render stays a single lookup.
///
/// Three hooks do it, each a no-op by default, so a modifier implements only what it needs. The build walks physical lights, so several folding onto one logical cell is the fan-out, which costs nothing, and the mapping cannot overflow. Float math is fine in a static fold, since it runs while the mapping builds, never per frame.
///
Expand Down
28 changes: 9 additions & 19 deletions src/light/util/FixtureChannels.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,39 +6,29 @@

namespace mm {

/// Where a fixture's non-color channels live inside one light.
/// Where a fixture's non-color channels live inside one light, @xref{why-color-has-no-entry|color having none}.
///
/// @technical
///
/// A light is `channelsPerLight` bytes wide, and that width is a runtime value: 3 for RGB, 4 for RGBW, 11 for a mini moving head, 25 for a bigger one.
/// The pipeline does not care which, since a wider light simply has more bytes and a driver that cannot use them ignores them.
/// This says which of those bytes are pan, tilt, zoom and so on, so an effect drives a moving head through the same buffer it paints pixels into.
/// A light is `channelsPerLight` bytes wide at runtime (3 for RGB, 4 for RGBW, 11 or 25 for a moving head), and this says which bytes are pan, tilt, zoom and the rest, so an effect steers a moving head through the buffer it paints.
///
/// ## Why color has no entry
///
/// Color is always at offset 0.
/// Every effect and every draw primitive writes RGB or RGBW at the start of a light.
/// `Correction` places those bytes at the fixture's own color channels on the way out.
/// The offsets here are the channels an effect could not otherwise reach at all.
/// Color is always at offset 0, where every draw primitive writes RGB or RGBW, and `Correction` moves it to the fixture's color channels on the way out; these offsets are the channels an effect has no other way to reach.
///
/// ## Why an absent channel is safe
///
/// `kAbsent` means the fixture has no such channel, and every setter is then a no-op.
/// That is what keeps a moving-head effect harmless on an LED strip.
/// It calls `setPan`, the strip carries no pan channel, and nothing is written.
/// The same effect on a moving head steers it.
/// MoonLight's LightsHeader is the same idea, credited in the work archive.
/// An absent role's setters write nothing, so one moving-head effect steers a moving head and leaves an LED strip untouched; MoonLight's LightsHeader is the same idea, credited in the work archive.
///
/// ## Layer slots are not fixture channels
///
/// These are offsets into the layer's light, which is not the fixture's layout.
/// A layer light always begins with RGB or RGBW, because every draw primitive writes there, so motion is packed after the color in a fixed order.
/// The fixture's own offsets live in `Correction`, whose `apply` maps a layer slot to a fixture channel on the way out.
/// Keeping the two apart is what stopped pan from landing on the red channel, which happened when both claimed byte 0.
/// These are offsets into the layer's light, motion packed after the color in a fixed order; `Correction::apply` maps each slot to the fixture's own channel, so motion and color never claim the same byte.
///
/// A slot past the layer's width widens the layer: pan at slot 4 on a 3-channel layer makes every light 5 bytes wide at the next prepare, and `setChannelsPerLight` rejects zero.
struct FixtureChannels {
/// Marks a role this fixture does not carry, making every setter for it a no-op.
/// Marks a role this fixture does not carry, making every setter for it a no-op: @xref{why-an-absent-channel-is-safe}.
static constexpr uint8_t kAbsent = 255;
/// Where motion starts in a layer light, after the RGBW the effects paint into.
/// Where motion starts in a layer light, after the RGBW the effects paint into: @xref{layer-slots-are-not-fixture-channels}.
static constexpr uint8_t kMotionBase = 4;

uint8_t pan = kAbsent; ///< the slot carrying pan, or absent
Expand Down
Loading