From 82cc6d910c2de759b5ec66a63cf8d9cde95c006f Mon Sep 17 00:00:00 2001 From: Shura Fedorovskyi Date: Sat, 10 Oct 2026 13:21:57 +0400 Subject: [PATCH 1/3] Follow the screen with the lights around it Lights around a display now glow the color of the picture nearest them: a video source publishes one decoded RGB frame per tick, and an effect gives each light the mean of the rectangle mapping to it. The frame comes from a synthesized test pattern or a binary PPM, so the whole path is reviewable and testable on a desktop build. Capturing a real picture from an HDMI grabber over USB follows in its own change. **Core** - VideoService: two sources, a strict Netpbm P6 header parser, and a one-active-source seat so a service added or removed at runtime hands over rather than going dark - VideoFrame: the frame contract, borrowing the producer's buffer for one tick - Every frame carries its tone curve, sRGB here, so a consumer averages light rather than encoded bytes **Light domain** - AmbilightEffect: a per-light box filter averaged in LINEAR light, then encoded once per light, so a part-lit zone reads as bright rather than as a dim mush - Positions reaching no LED are skipped, which on a border layout is most of the box, so a backlight strip costs its perimeter rather than its bounding box - edgeDepth widens the outermost zones past the overscan junk; detectBlackBars moves them inside a letterbox, which edgeDepth cannot do - Smoothing keeps 8.8 fixed-point state per channel, with snapAbove letting a cut land whole - MappingLUT: hasDestination(), the one question the effect asks per position **Tests** - unit_VideoService: 13 cases over the header grammar, the seat election and the pattern's sweep rate - unit_AmbilightEffect: 23 cases over orientation, coverage, the linear-light mean, smoothing in both directions, bar detection and the sparse-mapping skip, with a hollow-rectangle layout local to the file - scenario_Video_source: add, configure and remove the service and its consumer under a running pipeline, which also pins the type registry, since the runner constructs every module through src/module_types.cpp **Docs/CI** - A Video card under core/services.md and an Ambilight card under light/effects.md, each with its details section - screenshot_modules captures both A strip is placed on a wall's edge pixels by the MoonLive library's backlight.mll, so this adds no layout. docgen: appendix lines 5617 to 5643, for the three new headers, which is the one rule that rises. Co-Authored-By: Claude Opus 5 (1M context) --- docs/assets/core/VideoService.png | Bin 0 -> 58936 bytes docs/assets/light/effects/AmbilightEffect.gif | Bin 0 -> 162908 bytes docs/assets/light/effects/AmbilightEffect.png | Bin 0 -> 15506 bytes docs/moonmodules/core/services.md | 34 ++ docs/moonmodules/light/effects.md | 34 ++ moondeck/docs/screenshot_modules.py | 5 + src/core/services/VideoService.h | 304 ++++++++++ src/core/util/VideoFrame.h | 41 ++ src/light/effects/AmbilightEffect.h | 374 ++++++++++++ src/light/layers/MappingLUT.h | 7 + src/module_types.cpp | 4 + test/CMakeLists.txt | 2 + .../light/scenario_Video_source.json | 317 ++++++++++ test/unit/core/unit_VideoService.cpp | 177 ++++++ test/unit/light/unit_AmbilightEffect.cpp | 567 ++++++++++++++++++ 15 files changed, 1866 insertions(+) create mode 100644 docs/assets/core/VideoService.png create mode 100644 docs/assets/light/effects/AmbilightEffect.gif create mode 100644 docs/assets/light/effects/AmbilightEffect.png create mode 100644 src/core/services/VideoService.h create mode 100644 src/core/util/VideoFrame.h create mode 100644 src/light/effects/AmbilightEffect.h create mode 100644 test/scenarios/light/scenario_Video_source.json create mode 100644 test/unit/core/unit_VideoService.cpp create mode 100644 test/unit/light/unit_AmbilightEffect.cpp diff --git a/docs/assets/core/VideoService.png b/docs/assets/core/VideoService.png new file mode 100644 index 0000000000000000000000000000000000000000..ef4879dcde29c4a2b2fd6d49da0e07ef186d6f15 GIT binary patch literal 58936 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¥U3N#GwM>QTu0^wJeO3$8q%%X09sl={kLe2F5y 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�i@e4v|aMTj-rtGg|)&gOa-e*?53Vyva?Y;X2 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­*v=jZ|0n1O_2DY6^V zfboYe6TsmV=Po?{25b;uo<*C?v4v1HKEay(U&$|gz&0nN-5|6NpJ)rGR8))`8GYv8 zZkh{x#%7~kSnx3$w>bm@X4tD_2o`O^y-*l$@fEOTW0?~BgQ3fOVjli)u{HotZa~4- z1h)o>@w@Q864Eerj^3TOwvT6>jX$~4`4qa@HVz-3EOQfkmDZ!NjHkL7rjU#u))MQ4zsE8-G zTwm)zYIHskEVjf6H@H17$QfxBa9i>GR!iA7c)qSo7J0HcE3Ah;d#290##`X_L4iOs?@ z%^{Q)=3>v@=iKm-n!yBi^c7QJqK3Ye`*5+ve1TqBR)#Gzge?tzL8$ z3B#}5Sn1sO!w0pR+r6RJex|)wzoXql4Kf(T7$$OqojV2=xuQ#bc*APzQC{kBOQs;W zGQo#8oxl^SuZTnojrehe? zhYpQ4U1DSWM+S{*xgwj*p*bDqa;=ehq_Gj`a1QqHFfTl>?^qt>QeEaEGs=l7GO6i0 z)6u0q+VDlV*X{-K`3xNFp_~`njzSMB5{G{ab3JpR3sxhI*3DO}I$A^e;TkG-xq~Fv ze{17DE@epK1c+pSd*biHSuI?X-pRfT$RgFqFw%&!;S*^5altIgp3AXipxyrsYF*tR z*-q#Zywb_VoOiniPp*Y_+~8+g4>wz1{x^EK^m8SD%!YPJ_h+-$V zn5?HFMCCxU)B6w(9;9;WdGo|RBh-QDARV)lD?YU5eBesWZk|OOk1qN6XtRlV X`9arIkK14F)Xb;W*h;{COTGUA^F3@M literal 0 HcmV?d00001 diff --git a/docs/assets/light/effects/AmbilightEffect.png b/docs/assets/light/effects/AmbilightEffect.png new file mode 100644 index 0000000000000000000000000000000000000000..fc4429f72319b3084fd7d6331969aec79c022a23 GIT binary patch literal 15506 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 u{mJO-H(){W_ctF&qa + +### Video + +Video service card + +A user-added Service: the video source a screen-follow effect reads. The counterpart of [Audio](#audio) for a picture, decoded once per tick however many effects want it. `source` decides which controls show. The pattern, the file format and the tone curve: ⌄ details. + +- `source`: `test pattern` needs no hardware, `file` reads a PPM off the filesystem. +- `patternSpeed`: (test pattern) sweep rate of the white block, in pixels per second. 0 parks it. +- `file`: (file) path to a binary PPM (P6, maxval 255), uploaded through the File Manager. +- `reload`: (file) re-read the file in place, without rebuilding the pipeline. +- status: the live frame's dimensions (`64x36`), or the reason there is no frame. + +Detail: [technical](moxygen/VideoService.md) + +[Tests](../../reference/tests/unit-tests.md#videoservice) + ### OSC @@ -249,6 +267,22 @@ A received magnitude is clamped to 255, since a real WLED source reaches ~9500 a Prior art: the WLED-MM audio-reactive usermod by **Frank ([@softhack007](https://github.com/softhack007))**, the most-used open-source audio-reactive LED implementation, whose adaptive noise-gate concept the analysis here descends from (analyzed with his permission); and **[@troyhacks](https://github.com/troyhacks/WLED)**, who reworked that DSP onto Espressif's [esp-dsp](https://github.com/espressif/esp-dsp) FFT, the same choice this service makes. The line-in path exists because **wladi ([myhome-control](https://shop.myhome-control.de))** supplied the hardware and pinout for the [MHC-WLED ESP32-P4 shield](../../reference/hardware/mhc-wled-esp32-p4-shield.md): its onboard PCM1808 I2S ADC is what `mclkPin` is for. +## Video, details + +**What reads it.** The service publishes one decoded RGB frame per tick through a static accessor, and any number of effects read that same frame: it is decoded once here however many want it. A consumer holds the frame for one tick and never across frames, and before any frame exists the pointer is non-null but its pixels are, which every reader has to tolerate. That contract is what lets a source be added and removed under a running pipeline. + +**The test pattern is a diagnostic, not decoration.** Four colored border bands (red top, green right, blue bottom, yellow left) and a white block sweeping along the top edge. On a border-mounted strip that makes orientation self-evident: a strip mapped the wrong way round shows as the wrong physical edge lighting, rather than a subtly wrong picture. The sweeping block shows liveness and which way "forward" runs. Parking it with `patternSpeed` 0 turns the pattern into a still reference, so one light can be compared against a known color without the block passing through its zone mid-read. + +**Why PPM for the file source.** There is no software JPEG decoder in this codebase, and adding one so the desktop build could open a `.jpg` would buy a dependency for a development convenience. PPM is a raw RGB dump behind a three-line ASCII header, so it needs no decoder and is one command from any source material: + +```sh +ffmpeg -i clip.mp4 -frames:v 1 -vf scale=64:36 -pix_fmt rgb24 frame.ppm +``` + +The header is parsed strictly, because the file is whatever a user uploaded: comments and whitespace runs are skipped per Netpbm, a 16-bit maxval is refused rather than misread as 8-bit, and exactly one whitespace byte is consumed before the pixels. Accepting a non-whitespace separator there would eat a pixel and shift every channel one place, which tints the whole image instead of failing. + +**Every frame carries its tone curve.** A published frame names the curve from its own encoding to linear light, and a consumer reads pixels through it. SDR is sRGB, a curve like any other, so the frame always carries one rather than leaving the reader to assume. This matters because averaging is what a screen-follow effect does, and the mean of encoded bytes is not the mean of the light: half black and half white averages to 128 as bytes where the light is 188. The table is 12-bit rather than 8, since linear has no headroom at the dark end, which is what encodings exist for. + ## OSC, details **Feedback: the device answers.** With `feedback` on, a control that changes anywhere (the web UI, a preset recall, an audio-reactive effect) is mirrored back to the surface, which is what keeps a client honest and what moves a motorized fader. `addressing` picks where it goes: `unicast` to each of `hosts`, addresses or names, or to whoever last wrote when `hosts` is empty; `multicast` to `group`, which every listening device with the same `group` joins; the default, `239.255.77.78`, sits beside discovery's own group, so multicast works with nothing to fill in, and a second rig on the same network picks another. `feedbackPort` is where that client LISTENS, which is not the port we listen on (Open Stage Control calls its own `osc-port`). diff --git a/docs/moonmodules/light/effects.md b/docs/moonmodules/light/effects.md index 0be273c3e..918d6ea6c 100644 --- a/docs/moonmodules/light/effects.md +++ b/docs/moonmodules/light/effects.md @@ -10,6 +10,28 @@ Effects are built from the shared [power functions](power-functions.md): the dra ## MoonLight effects + + +### Ambilight 📺 + +Ambilight effect preview + +Paints the layer with the live frame from the [Video](../core/services.md#video) service, so lights around a display glow the color of the picture nearest them. Each light shows the **mean** of the rectangle mapping to it. Sampling, black bars and the layout's part: ⌄ details. + +- `brightness`: scales the sampled color, dimming the video not the output. +- `saturation`: how far each channel is pushed from its zone's luma. +- `smoothing`: how much of the gap to a new color closes per frame. +- `snapAbove`: a channel moving further than this jumps instead of easing. +- `edgeDepth`: how far into the picture the outermost lights look. +- `detectBlackBars`: map the lights across the picture, not the frame. +- `barLevel`: how dark a pixel counts as bar (0 to 64). + +Origin: MoonLight original, on the Ambilight / Hyperion behavior + +Detail: [technical](moxygen/AmbilightEffect.md) + +[Tests](../../reference/tests/unit-tests.md#ambilighteffect) + ### ColorTrails 💫🖌️💨🌫️ · 3D @@ -1367,3 +1389,15 @@ Origin: MoonLight (Sinus, AI-generated) · via [MoonLight](https://github.com/ew Detail: [technical](moxygen/SineEffect.md) [Tests](../../reference/tests/unit-tests.md#sineeffect) + +## Ambilight, details + +**Two spaces, and the layout decides the shape.** The SOURCE is the video frame, counted in pixels; the DESTINATION is the layer's logical box, counted in light positions. The source is far the bigger, a 640x480 picture onto a strip of 60 positions, so each position owns a whole rectangle of pixels and shows their average. The effect asks the mapping only one question, whether a position lights anything, and skips the averaging where the answer is no. On a border layout that is most of the box: the interior of a backlight strip's bounding box maps to no LED, so the perimeter shows the frame's border for free. On a full grid the same effect is a video wall. A [MoonLive layout](layouts.md#moonlive) running the library's `backlight.mll` is how a strip gets placed on a wall's edge pixels. + +**Averaging happens in linear light.** Each zone's pixels are summed through the frame's tone curve, averaged, then encoded once per light. Averaging the raw bytes instead averages a quantity that is not proportional to light: a zone half black and half white comes out at 128 where the light is 188, which reads as a dim mush rather than a bright scene. The accumulators are 32-bit because a 640x480 frame onto 32x18 positions is about 520 pixels each, and 520 full-scale 12-bit samples overflow 16 bits many times over. + +**`edgeDepth` widens the outermost zones.** A border light's own share is one row or column of the frame, a sliver at the very edge, which picks up the letterbox and the overscan junk before it picks up the picture. `edgeDepth` lets the outermost positions reach a percentage of the way in instead, without changing how many of them there are. It costs work in proportion: on a layout where every light is an edge light, 15% sends the pixel count up about sixfold. + +**Black bars move the zones, which `edgeDepth` cannot.** A letterboxed film puts bars exactly where the top and bottom lights look, so those lights go dark while the picture is bright. `edgeDepth` only widens a zone from its edge, so the bar stays inside it. `detectBlackBars` finds the bar and maps the lights across the picture within it. A reading is adopted only once several frames agree, because bars come and go at scene changes and a mapping that follows every dark frame twitches worse than one that ignores them. Reaching the scan ceiling reports no bar at all: darkness that deep is a dark scene rather than a letterbox. + +**No source paints black.** With nothing publishing, the effect fills its layer with black rather than returning early, or the previous effect's picture would stay frozen on the strip. A merely dropped frame never reaches that path, since the service keeps its buffer and only a removed service drops it. diff --git a/moondeck/docs/screenshot_modules.py b/moondeck/docs/screenshot_modules.py index 2af4563a4..99d4759cc 100644 --- a/moondeck/docs/screenshot_modules.py +++ b/moondeck/docs/screenshot_modules.py @@ -106,6 +106,9 @@ def asset_dir_for(type_name: str) -> Path: ("RainbowEffect", "Layer", {}, True), ("NoiseEffect", "Layer", {}, True), # The generative-fields showcases: each is Dim::D3, so the preview shows a volume. + # Paints the live video frame, so with no source it photographs black. The capture points it + # at the test pattern, whose four coloured bands and sweeping block are what the effect is for. + ("AmbilightEffect", "Layer", {}, True), ("AuroraEffect", "Layer", {}, True), ("ColorTrailsEffect", "Layer", {}, True), ("TrailsEffect", "Layer", {}, True), @@ -178,6 +181,7 @@ def asset_dir_for(type_name: str) -> Path: # (not in state); capture these against a board when needed. "ImprovProvisioningModule", "AudioService", + "VideoService", "AnalogService", "I2cBusModule", "InfraredService", @@ -209,6 +213,7 @@ def asset_dir_for(type_name: str) -> Path: # the desktop tree — so they're captured against an ESP32, where these entries route the # shot to the right nav root. "AudioService": "Services", + "VideoService": "Services", "AnalogService": "Services", "InfraredService": "Services", "ButtonService": "Services", diff --git a/src/core/services/VideoService.h b/src/core/services/VideoService.h new file mode 100644 index 000000000..e094ab88e --- /dev/null +++ b/src/core/services/VideoService.h @@ -0,0 +1,304 @@ +#pragma once + +#include "core/util/ActiveInstance.h" // the one-active-source seat (RAII vacate on destruct) +#include "core/util/color.h" // RGB: the pattern's band colors +#include "core/module/MoonModule.h" +#include "core/util/ScratchBuffer.h" +#include "core/util/VideoFrame.h" +#include "platform/platform.h" // fsSize / fsReadAt / millis + +#include +#include +#include +#include + +namespace mm { + +/// The device's video input: one decoded RGB frame per tick, published through the static `latestFrame()`. +/// +/// @moreinfo +/// +/// ## Two sources +/// +/// Decoded once here however many effects read it, and effects hold no pointer back to this module; the user adds it under `Services`, so it is not auto-wired. +/// `test pattern` is a DIAGNOSTIC rather than decoration: its colored border bands make a border-mapped effect's orientation self-evident. +/// `file` reads a binary PPM, chosen over JPEG because there is no software JPEG decoder here. +/// Both render into `buf_`, which this module owns, so a published frame lives exactly as long as the service does. +class VideoService : public MoonModule { +public: + /// A service: it produces for others and draws nothing itself. + ModuleRole role() const MM_NONBLOCKING override { return ModuleRole::Service; } + + // Appended, never reordered, so a persisted index keeps its meaning. + /// `source` value for the synthesized diagnostic pattern. + static constexpr uint8_t kSourcePattern = 0; + /// `source` value for a binary PPM on the filesystem. + static constexpr uint8_t kSourceFile = 1; + /// Dropdown labels for `source`, indexed by the constants above. + static constexpr const char* kSourceOptions[] = {"test pattern", "file"}; + /// How many of `kSourceOptions` this build offers. + static constexpr uint8_t kSourceCount = 2; + + /// Which of `kSourceOptions` fills the frame: @xref{two-sources|what each one is for}. + uint8_t source = kSourcePattern; + /// Path the `file` source reads. + char file[64] = "/frame.ppm"; + // Zero parks the block, which makes the pattern a still reference for checking a border light against a known color. Seventeen is the rate it used to be fixed at: a sweep every ~4 s. + /// Sweep rate of the test pattern's white block, in pixels per second. + uint8_t patternSpeed = 17; + + // Small on purpose: a border effect averages the frame down to a few dozen values, so pixels beyond that buy nothing but bandwidth and decode time. + /// Width of the synthesized pattern, 16:9 with kPatternH. + static constexpr uint16_t kPatternW = 64; + /// Height of the synthesized pattern. + static constexpr uint16_t kPatternH = 36; + /// Thickness of each colored edge band, in pixels. + static constexpr int kBand = kPatternH / 4; + // Comfortably past 4K, so a corrupt header is rejected at parse time rather than failing later as "too large for memory". What bounds the allocation is buf_.resize() failing. + /// Sanity ceiling on either dimension of a loaded file. + static constexpr uint32_t kMaxDim = 4096; + + /// The live frame. The POINTER is never null - with no source this returns kNoVideoFrame, which has a null `rgb`. So callers test the frame's contents, never the pointer + static const VideoFrame* latestFrame() MM_NONBLOCKING { + VideoService* v = ActiveInstance::active(); + return v ? &v->frame_ : &kNoVideoFrame; + } + + /// Test seam: the curve as built. + const uint16_t* toneForTest() const { return tone_; } + + /// Claims the one-active-source seat at construction, before any prepare() runs. + VideoService() { seat_.claim(); } + + /// The source picker, plus the settings that belong to whichever source is selected. + void defineControls() override { + controls_.addSelect("source", source, kSourceOptions, kSourceCount); + // Live (not in affectsPrepare): the rate changes what the NEXT frame draws, and nothing about the buffer, so it must not tear the pipeline down to take effect. + controls_.addControl("patternSpeed", patternSpeed, 0, 255); + controls_.setHidden(controls_.count() - 1, source != kSourcePattern); + controls_.addText("file", file, sizeof(file)); + controls_.setHidden(controls_.count() - 1, source != kSourceFile); + controls_.addButton("reload"); + controls_.setHidden(controls_.count() - 1, source != kSourceFile); + MoonModule::defineControls(); + } + + /// A source switch changes what the buffer must hold, so it re-runs the whole build. The reload button re-reads the same file in place: cheap, and it must NOT tear down the pipeline. + bool affectsPrepare(const char* name) const override { + return std::strcmp(name, "source") == 0 || std::strcmp(name, "file") == 0; + } + + /// Routes the one live edit, the reload button; every other control re-runs prepare(). + void onControlChanged(const char* name) override { + if (std::strcmp(name, "reload") == 0) loadFile(); + MoonModule::onControlChanged(name); + } + + /// Cold path: size the frame buffer for the selected source and fill it once, so a frame exists before the first tick rather than one tick later. + void prepare() override { + seat_.claim(); // re-take after a disable/enable cycle: release() vacated it + if (source >= kSourceCount) source = kSourcePattern; // a config naming a source this build lacks + rebuildTone(); // both sources are display-encoded, so the curve is built once and never moves + if (source == kSourceFile) { + loadFile(); + } else if (allocate(kPatternW, kPatternH)) { + renderPattern(); + } + } + + /// Only the synthesized pattern regenerates per frame, since it animates; a still file keeps the buffer it already holds. File I/O is blocking and belongs nowhere near this function. + void tick() MM_NONBLOCKING override { + // Take an EMPTY seat, so deleting the elected source while a second one runs hands over rather than going permanently dark. claim() only fills an empty seat, never yanks one. + seat_.claim(); + if (source == kSourcePattern && buf_.data()) + renderPattern(); + else if (source == kSourceFile && buf_.data()) + publish(); + MoonModule::tick(); + } + + + + /// Drops the published frame and the seat, so a disabled module holds neither. + void release() override { + frame_ = VideoFrame{}; + seat_.vacate(); + MoonModule::release(); + } + +private: + // Claimed at CONSTRUCTION, since an effect resolves latestFrame() during its own build, then re-claimed in prepare() after a disable/enable and in tick() so a survivor inherits it. + ActiveInstance seat_{*this}; + + + static constexpr int kMaxHeaderBytes = 256; // room for a comment, and its own error if not + ScratchBuffer buf_{*this}; // width*height*3, accounted in dynamicBytes() + VideoFrame frame_; + uint32_t seq_ = 0; + uint16_t tone_[256] = {}; // the published curve: source encoding -> linear light + // Sweep position in 1/1000 px and the millis() it was last advanced at. Milli-pixels because a per-second rate sampled per tick rounds to zero motion in whole pixels at 1 px/s. + uint32_t sweepMilliPx_ = 0; + uint32_t sweepAtMs_ = 0; + char status_[40] = {}; + + /// Drop the published frame and say why. Returns false so every failing path reads as one line, `return fail("...")`, and none can forget to un-publish the stale frame. + bool fail(const char* why) { + frame_ = VideoFrame{}; + setStatus(why, Severity::Error); + return false; + } + + /// Size the buffer and point the published frame at it. False on any failure, so a too-large image degrades to "no video" rather than to a crash. + bool allocate(uint16_t w, uint16_t h) { + if (w == 0 || h == 0 || w > kMaxDim || h > kMaxDim) return fail("frame size out of range"); + if (!buf_.resize(static_cast(w) * h * 3u)) return fail("frame too large for memory"); + frame_.rgb = buf_.data(); + frame_.width = w; + frame_.height = h; + // Published here, not from a tick: dimensions only change on a resize, so this keeps the snprintf off the render path. + std::snprintf(status_, sizeof(status_), "%ux%u", w, h); + setStatus(status_, Severity::Status); + return true; + } + + /// Publish the buffer as a NEW frame: the sequence bump is what tells a consumer the pixels changed, so every producer path ends here (see VideoFrame::seq). + void publish() { + frame_.tone = tone_; // per frame, so a curve change lands on the next one with nothing to remember + frame_.seq = ++seq_; + } + + /// Fill `tone_`: the source's transfer curve undone to linear light, where a consumer averages (the mean of encoded bytes is not the mean of the picture). Cold path: 256 float evaluations per edit or prepare, never per frame. + void rebuildTone() { + for (int i = 0; i < 256; i++) { + const float e = static_cast(i) / 255.0f; + // sRGB EOTF (IEC 61966-2-1), which is what both a PPM and the pattern carry. + float lin = e <= 0.04045f ? e / 12.92f : std::pow((e + 0.055f) / 1.055f, 2.4f); + if (lin < 0.0f) lin = 0.0f; + if (lin > 1.0f) lin = 1.0f; + tone_[i] = static_cast(lin * VideoFrame::kLinearMax + 0.5f); + } + } + + // Four colored border bands and a sweeping white block. Integer-only and allocation-free: it runs on the render tick. + void renderPattern() { + uint8_t* p = buf_.data(); + if (!p) return; + // An accumulator fed by elapsed time, not a position derived from millis(): a derived one jumps the block the instant the rate changes and cannot express "stopped" at all. + const uint32_t now = platform::millis(); + if (sweepAtMs_ == 0) sweepAtMs_ = now; // first frame: no elapsed time to charge for + const uint32_t elapsed = now - sweepAtMs_; + sweepAtMs_ = now; + if (patternSpeed) sweepMilliPx_ = (sweepMilliPx_ + elapsed * patternSpeed) % (kPatternW * 1000u); + const int sweepX = static_cast(sweepMilliPx_ / 1000u); + for (int y = 0; y < kPatternH; y++) { + for (int x = 0; x < kPatternW; x++) { + // A white block riding the top edge: shows liveness, and which way "forward" runs. + const bool onSweep = y < kBand && x >= sweepX && x < sweepX + 4; + const RGB c = onSweep ? RGB{255, 255, 255} : bandColor(x, y); + uint8_t* px = p + (static_cast(y) * kPatternW + x) * 3; + px[0] = c.r; + px[1] = c.g; + px[2] = c.b; + } + } + publish(); + } + + /// Color of the pattern at (x, y): one hue per edge, black interior. + static RGB bandColor(int x, int y) { + if (y < kBand) return {255, 0, 0}; // top → red + if (y >= kPatternH - kBand) return {0, 0, 255}; // bottom → blue + if (x < kBand) return {255, 255, 0}; // left → yellow + if (x >= kPatternW - kBand) return {0, 255, 0}; // right → green + return {0, 0, 0}; // interior stays dark + } + + // --- PPM (P6) file source ----------------------------------------------------------------- + /// Read the header, size the buffer, then read the pixel block straight into it. Cold path only (prepare / the reload button): this blocks on the filesystem. + bool loadFile() { + const long size = platform::fsSize(file); + if (size <= 0) return fail("file not found"); + + // Netpbm allows comments and any run of whitespace between tokens, so a header is not a fixed length, and the ceiling has to clear the comment GIMP writes. + char header[kMaxHeaderBytes] = {}; + const int headerLen = platform::fsReadAt(file, 0, header, sizeof(header) - 1); + uint16_t w = 0, h = 0; + const int pixOff = parsePpmHeader(header, headerLen, w, h); + if (pixOff < 0) + return fail(headerLen >= static_cast(sizeof(header)) - 1 + ? "PPM header too long" + : "not a binary PPM (P6, maxval 255)"); + if (!allocate(w, h)) return false; // allocate() already reported why + + const uint32_t need = static_cast(w) * h * 3u; + if (static_cast(size - pixOff) < need) return fail("PPM truncated"); + + const int read = platform::fsReadAt(file, pixOff, reinterpret_cast(buf_.data()), need); + if (read < 0 || static_cast(read) != need) return fail("PPM read failed"); + + setStatus(status_, Severity::Status); // allocate() formatted the size; restore it over an error + publish(); + return true; + } + +public: + /// Parse a binary-PPM header (Netpbm). Returns the byte offset where pixel data begins, or -1 if `buf` is not one. Pure: `len` bounds the read, so a truncated file is rejected rather than parsed into whatever follows it. + static int parsePpmHeader(const char* buf, int len, uint16_t& w, uint16_t& h) { + // P6 + HeaderCursor cur{buf, len}; + cur.skipBlanks(); + if (cur.pos + 1 >= len || buf[cur.pos] != 'P' || buf[cur.pos + 1] != '6') return -1; + cur.pos += 2; + + const long ww = cur.readInt(); + const long hh = cur.readInt(); + const long maxval = cur.readInt(); + if (ww <= 0 || ww > static_cast(kMaxDim)) return -1; + if (hh <= 0 || hh > static_cast(kMaxDim)) return -1; + if (maxval != 255) return -1; // 16-bit samples are two big-endian bytes: another format + // Netpbm requires ONE whitespace byte here. Accepting whatever is present would eat a pixel: "P6\n2 2\n255X" would read as valid with the X swallowed. + if (cur.pos >= len || !HeaderCursor::isBlank(buf[cur.pos])) return -1; + + w = static_cast(ww); + h = static_cast(hh); + return cur.pos + 1; // the pixels begin straight after that one byte + } + +private: + /// Position within an ASCII header. Netpbm separates tokens with any run of whitespace and `#` comments to end-of-line, so both readers skip those first. + struct HeaderCursor { + const char* buf; + int len; + int pos = 0; + + /// Spelled out rather than isspace(), which is locale-dependent and undefined for a signed char above 127. + static bool isBlank(char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r'; } + + void skipBlanks() { + while (pos < len) { + if (buf[pos] == '#') { + while (pos < len && buf[pos] != '\n') pos++; + } else if (isBlank(buf[pos])) { + pos++; + } else { + break; + } + } + } + + /// Next decimal token, or -1 when the next token is not one. Capped well above any real dimension purely so a long digit run cannot overflow; the true bounds are the caller's. + long readInt() { + skipBlanks(); + if (pos >= len || buf[pos] < '0' || buf[pos] > '9') return -1; + long v = 0; + while (pos < len && buf[pos] >= '0' && buf[pos] <= '9') { + v = v * 10 + (buf[pos] - '0'); + if (v > 100000) return -1; + pos++; + } + return v; + } + }; +}; + +} // namespace mm diff --git a/src/core/util/VideoFrame.h b/src/core/util/VideoFrame.h new file mode 100644 index 000000000..dfb462537 --- /dev/null +++ b/src/core/util/VideoFrame.h @@ -0,0 +1,41 @@ +#pragma once + +#include + +namespace mm { + +/// One decoded video frame, produced by VideoService and read by video-reactive effects. +/// +/// @moreinfo +/// +/// ## The tone curve +/// +/// Borrows the producer's buffer rather than carrying the pixels, so `rgb` is valid for one effect tick and never across frames. Before any frame exists it is null, which every consumer must tolerate. +/// `tone` maps the source's encoding to linear light, and SDR is sRGB, so a published frame always carries one. +/// Read pixels through channel(): a consumer averaging the encoded bytes averages a quantity that is not proportional to light. +/// The table is 12-bit rather than 8 because linear has no headroom at the dark end, and small enough that a zone of ~1M pixels still sums inside a uint32. +struct VideoFrame { + /// The pixels: width*height*3, row-major, top-left origin, no padding. + const uint8_t* rgb = nullptr; + /// Frame width in pixels. + uint16_t width = 0; + /// Frame height in pixels. + uint16_t height = 0; + // Compare for INEQUALITY, never ordering: a still PPM bumps it every tick, the way a camera aimed at a still object sends one every period. + /// Bumped per published frame. + uint32_t seq = 0; + // Same one-tick lifetime as `rgb`. + /// 256-entry curve from the source's encoding to linear light, 0..kLinearMax: @xref{the-tone-curve|why a reader must use it}. + const uint16_t* tone = nullptr; + /// The full-scale value of a linear sample. + static constexpr uint16_t kLinearMax = 4095; + + // One lookup on a read the caller already makes. + /// One channel of the pixel at `px` as linear light, or the encoded byte when no curve is published. + uint16_t channel(const uint8_t* px, int c) const { return tone ? tone[px[c]] : px[c]; } +}; + +/// The "no source" frame consumers fall back to. +inline constexpr VideoFrame kNoVideoFrame{}; + +} // namespace mm diff --git a/src/light/effects/AmbilightEffect.h b/src/light/effects/AmbilightEffect.h new file mode 100644 index 000000000..3927d3ade --- /dev/null +++ b/src/light/effects/AmbilightEffect.h @@ -0,0 +1,374 @@ +#pragma once + +#include "core/services/VideoService.h" +#include "light/effects/EffectBase.h" + +#include // std::max / std::min +#include +#include + +namespace mm { + +/// Screen-follow ambient light: paints the layer with the live video frame, so lights around a display glow the color of the picture nearest them. +/// +/// @moreinfo +/// +/// ## The layout decides the shape +/// +/// Two spaces are in play. The SOURCE is the frame in pixels, the DESTINATION is the layer's box in light positions. The source is far the bigger, so each position owns a rectangle of pixels and shows their average. +/// The effect asks the mapping only ONE question, whether a position lights anything, and skips the averaging where the answer is no. On a border layout that is most of the box. +/// So a backlight strip shows the frame's border for free, and a full grid makes the same effect a video wall. +class AmbilightEffect : public EffectBase { +public: + /// Flat, because a frame is; the Layer extrudes z. + Dim dimensions() const override { return Dim::D2; } + + /// Dims THE VIDEO, where the driver's brightness dims everything. + uint8_t brightness = 255; + /// Percent of the distance from gray, so 100 leaves the mean untouched. + uint8_t saturation = 130; + /// How slowly a light moves toward the frame; 0 follows it exactly. + uint8_t smoothing = 0; + /// Jump rather than smooth when a channel moves further than this; 0 never jumps. + uint8_t snapAbove = 80; + /// Percent of the frame the OUTERMOST positions look in; 0 gives them their own share. + uint8_t edgeDepth = 0; + /// Find the letterbox and map the lights across the picture inside it. + bool detectBlackBars = false; + /// A channel at or below this counts as bar, ~5%, which clears compression noise. + uint8_t barLevel = 12; + + /// The video's own brightness and saturation, how fast a light follows it, and the two bar controls. + void defineControls() override { + controls_.addControl("brightness", brightness, 0, 255); + controls_.addControl("saturation", saturation, 0, 200); + controls_.addControl("smoothing", smoothing, 0, 255); + // A cut is a real jump, and smoothing through it reads as the lights lagging the picture. + controls_.addControl("snapAbove", snapAbove, 0, 255); + controls_.setHidden(controls_.count() - 1, smoothing == 0); + controls_.addControl("edgeDepth", edgeDepth, 0, 50); // Hyperion samples ~8% + // A letterboxed film puts bars where the top and bottom lights look, and edgeDepth cannot help since it widens a zone from the edge. This moves the zones instead. + controls_.addControl("detectBlackBars", detectBlackBars); + // Raise it if bars are missed, lower it if dark scenes get cropped; the doc page has why. + controls_.addControl("barLevel", barLevel, 0, 64); + controls_.setHidden(controls_.count() - 1, !detectBlackBars); + } + + /// Turning smoothing on or off allocates or frees the accumulators, so it has to re-run prepare(): without this the buffer stays empty and the setting does nothing. + bool affectsPrepare(const char* name) const override { return std::strcmp(name, "smoothing") == 0; } + + /// Cold path. applyState() prepares a parent before its children, so the Layer's mapping is already built when buildLitList() reads it. + void prepare() override { + // lengthType is signed: a stray negative would cast to a colossal size_t, not to nothing. + const lengthType w = width(), h = height(); + const size_t positions = (w > 0 && h > 0) ? static_cast(w) * static_cast(h) : 0; + + state_.resize(smoothing != 0 ? positions * 3u : 0); // 8.8 per channel, only while smoothing + encodeTable(); // built here, off the tick + primed_ = false; + buildLitList(positions); + } + + /// The positions that reach an LED, packed y<<16|x, so tick() walks a few hundred of tens of thousands on a border layout: @xref{the-layout-decides-the-shape|why most of the box is skipped}. + void buildLitList(size_t positions) { + litCount_ = 0; + const MappingLUT& lut = layer()->lut(); + // A table-free (identity) mapping lights every position, so the list would be 0,1,2,3, four bytes a position to say "all of them" where the plain loop needs none. + allLit_ = !lut.hasLUT(); + if (allLit_ || positions == 0) { + lit_.resize(0); + return; + } + // Count first, then size to the count. Sizing by the box reserves 156 KB on a 200x200 rectangle to hold the 3 KB its perimeter needs. + size_t lit = 0; + for (size_t i = 0; i < positions; i++) + if (columnLit(lut, i, positions)) lit++; + if (lit == 0 || !lit_.resize(lit)) return; // no list: tick() paints the whole box instead + const lengthType w = width(); + for (size_t i = 0; i < positions; i++) + if (columnLit(lut, i, positions)) + lit_[litCount_++] = static_cast((i / w) << 16 | (i % w)); + } + + // This effect is D2 and Layer::extrude() copies z=0 across the depth. On a sparse 3D layout a column whose only LED sits at z > 0 is still lit from here. + /// Whether front-face position `i` reaches an LED in ANY z plane, where `slice` is width*height. + bool columnLit(const MappingLUT& lut, size_t i, size_t slice) const MM_NONBLOCKING { + const lengthType d = depth(); + for (lengthType z = 0; z < d; z++) + if (lut.hasDestination(static_cast(i + static_cast(z) * slice))) + return true; + return false; + } + + /// Average each lit position's share of the frame and paint it, or paint black when no source is publishing. + void tick() MM_NONBLOCKING override { + const VideoFrame* frame = VideoService::latestFrame(); + const draw::Canvas out = canvas(); + + // No source: paint black rather than return, or the PREVIOUS effect's picture stays frozen on the strip. A merely dropped frame never lands here. + if (!frame->rgb || frame->width == 0 || frame->height == 0) { + draw::fill(out, {0, 0, 0}); + primed_ = false; // so the next frame lands whole instead of creeping up out of black + return; + } + + // The frame already on the strip, which `primed_` is what makes true: prepare() clears the layer and resets it, so a rebuild against a frozen frame would stay black. + if (primed_ && frame->seq == lastSeq_) return; + lastSeq_ = frame->seq; + + const lengthType lightsX = width(), lightsY = height(); + if (lightsX <= 0 || lightsY <= 0) return; + + const Region region = regionFor(*frame); + if (region.width <= 0 || region.height <= 0) return; + + const bool canSmooth = smootherReady(lightsX, lightsY); + + // Three ways to reach the same set of positions, cheapest first. + if (allLit_) { + // Nothing to skip: the plain box, no clear, no mapping queries. + for (lengthType y = 0; y < lightsY; y++) + for (lengthType x = 0; x < lightsX; x++) + paint(out, *frame, region, x, y, lightsX, lightsY, canSmooth); + } else if (lit_) { + // The list. Unlit positions are never written, so they keep the black Layer::prepare() left. BlendMap never reads them, but PreviewDriver shows the raw buffer and must not see a ghost. + for (size_t i = 0; i < litCount_; i++) + paint(out, *frame, region, static_cast(lit_[i] & 0xFFFF), + static_cast(lit_[i] >> 16), lightsX, lightsY, canSmooth); + } else { + // The list could not be allocated. Same output, asking the mapping per position - which is the cost the list exists to avoid. + const MappingLUT& lut = layer()->lut(); + const size_t slice = static_cast(lightsX) * lightsY; + draw::fill(out, {0, 0, 0}); + for (lengthType y = 0; y < lightsY; y++) + for (lengthType x = 0; x < lightsX; x++) + if (columnLit(lut, static_cast(y) * lightsX + x, slice)) + paint(out, *frame, region, x, y, lightsX, lightsY, canSmooth); + } + primed_ = true; + } + +private: + /// Half-open range of SOURCE pixels `[begin, end)` along one axis. + struct Span { + int begin, end; + Span shifted(int by) const { return {begin + by, end + by}; } + }; + + // The actual region of the source frame that the lights cover. Could be smaller than the full frame if black bars are detected. + struct Region { + int left = 0, top = 0; // where the picture starts inside the frame + int width = 0, height = 0; + int deepX = 0, deepY = 0; // edgeDepth in pixels, so the divide is not per position + + /// Which source pixels one light position covers: its share of the picture, shifted back into frame coordinates. Every input lives here, so the loop only asks. + Span cols(int x, int lightsX) const { return spanFor(x, lightsX, width, deepX).shifted(left); } + Span rows(int y, int lightsY) const { return spanFor(y, lightsY, height, deepY).shifted(top); } + }; + + /// One light position: average its pixels, correct, smooth, write. + void paint(const draw::Canvas& out, const VideoFrame& frame, const Region& region, lengthType x, + lengthType y, lengthType lightsX, lengthType lightsY, bool canSmooth) MM_NONBLOCKING { + RGB color = adjust(meanOf(frame, region.cols(x, lightsX), region.rows(y, lightsY))); + if (canSmooth) color = smooth(static_cast(y) * lightsX + x, color); + draw::pixel(out, {x, y, 0}, color); + } + + Region regionFor(const VideoFrame& frame) MM_NONBLOCKING { + const Bars bars = trackBars(frame); + Region r; + r.left = bars.left; + r.top = bars.top; + r.width = frame.width - bars.left - bars.right; + r.height = frame.height - bars.top - bars.bottom; + // Rounded UP, so any non-zero percentage is at least one pixel. Flooring would let a small setting on a small frame land on 0, which is the off value: the control would go quiet. + r.deepX = (r.width * edgeDepth + 99) / 100; + r.deepY = (r.height * edgeDepth + 99) / 100; + return r; + } + + /// Whether this frame can be smoothed: turned on, and the accumulators are there and big enough. + bool smootherReady(lengthType lightsX, lengthType lightsY) const MM_NONBLOCKING { + return smoothing != 0 && state_ && + state_.count() >= static_cast(lightsX) * static_cast(lightsY) * 3u; + } + + /// How thick the bar at each edge is, in pixels + struct Bars { + int top = 0, bottom = 0, left = 0, right = 0; + bool operator==(const Bars& o) const { + return top == o.top && bottom == o.bottom && left == o.left && right == o.right; + } + }; + + static constexpr int kProbes = 8; // sample points per scanned line + static constexpr int kMaxBarPercent = 40; // a "bar" deeper than this is a dark scene + static constexpr uint8_t kStableFrames = 30; + + /// Which edge a bar is being measured from. Top and Bottom scan rows, Left and Right columns. + enum class Edge : uint8_t { Top, Bottom, Left, Right }; + + static bool scansRows(Edge e) MM_NONBLOCKING { return e == Edge::Top || e == Edge::Bottom; } + static bool scansFromEnd(Edge e) MM_NONBLOCKING { return e == Edge::Bottom || e == Edge::Right; } + + /// Is this line dark all the way across? Sampled at a few evenly spaced points rather than every pixel: a bar is uniform, so a handful of probes settles it for a fraction of the cost. + bool lineIsDark(const VideoFrame& frame, int line, Edge edge) const MM_NONBLOCKING { + const bool horizontal = scansRows(edge); + const int along = horizontal ? frame.width : frame.height; + for (int i = 0; i < kProbes; i++) { + const int v = along * (2 * i + 1) / (2 * kProbes); // midpoints, so the corners are skipped + const int x = horizontal ? v : line; + const int y = horizontal ? line : v; + const uint8_t* px = frame.rgb + (static_cast(y) * frame.width + x) * 3; + if (px[0] > barLevel || px[1] > barLevel || px[2] > barLevel) return false; + } + return true; + } + + // Returning the ceiling cropped a dark frame to its middle, and kStableFrames then held that into the next scene. + /// How many dark lines run inward from one edge, where reaching the ceiling reports NO bar: darkness that deep is a dark SCENE rather than a letterbox. + int barFrom(const VideoFrame& frame, Edge edge) const MM_NONBLOCKING { + const int extent = scansRows(edge) ? frame.height : frame.width; + const int limit = extent * kMaxBarPercent / 100; + for (int i = 0; i < limit; i++) { + const int line = scansFromEnd(edge) ? extent - 1 - i : i; + if (!lineIsDark(frame, line, edge)) return i; + } + return 0; + } + + // Bars come and go at scene changes, and a mapping that follows every dark frame twitches worse than one that ignores them. Hence the state. + /// Scan this frame and return the bars IN EFFECT, which is what the caller maps across: a reading is adopted only once kStableFrames of them agree. + Bars trackBars(const VideoFrame& frame) MM_NONBLOCKING { + if (!detectBlackBars) { + // All of it, not just the adopted value. A surviving candidate_ with a saturated stable_ would make the next enable agree with itself at once and never re-adopt. + bars_ = candidate_ = Bars{}; + stable_ = 0; + return bars_; + } + Bars found; + found.top = barFrom(frame, Edge::Top); + found.bottom = barFrom(frame, Edge::Bottom); + found.left = barFrom(frame, Edge::Left); + found.right = barFrom(frame, Edge::Right); + if (!(found == candidate_)) { + candidate_ = found; + stable_ = 0; + } else if (stable_ < kStableFrames && ++stable_ == kStableFrames) { + bars_ = found; + } + return bars_; + } + + /// Which source pixels light position `lightId` covers along one axis. + /// - `pixels` shared evenly among `lightsSize` positions, cut at the edges so ranges meet exactly + /// - a position ON an edge takes exactly `deep` instead of its share, deeper OR shallower, so the control sets the depth rather than raising a floor under it + /// - `deep` of 0 leaves the plain division, and interior positions are on no edge either way + /// - an empty range widens to one pixel, so a strip finer than the picture still lights up + static Span spanFor(int lightId, int lightsSize, int pixels, int deep) { + int begin = static_cast((static_cast(lightId) * pixels) / lightsSize); + int end = static_cast((static_cast(lightId + 1) * pixels) / lightsSize); + if (deep > 0) { + if (lightId == 0) + end = deep; // down/right, inward + else if (lightId == lightsSize - 1) + begin = pixels - deep; // up/left, inward + } + if (begin < 0) begin = 0; + if (begin >= pixels) begin = pixels - 1; + if (end <= begin) end = begin + 1; + if (end > pixels) end = pixels; + return {begin, end}; + } + + /// Linear light back to a display byte (sRGB OETF). Built on first use, which prepare() makes a cold path: 4096 pow() calls have no place in a tick. + static const uint8_t* encodeTable() { + static const auto table = [] { + std::array t{}; + for (size_t i = 0; i < t.size(); i++) { + const float lin = static_cast(i) / VideoFrame::kLinearMax; + const float v = lin <= 0.0031308f ? 12.92f * lin : 1.055f * std::pow(lin, 1.0f / 2.4f) - 0.055f; + t[i] = static_cast(v * 255.0f + 0.5f); + } + return t; + }(); + return table.data(); + } + + // uint32 accumulators, because 640x480 onto 32x18 is ~520 pixels each and 520 x 4095 overflows 16 bits many times over. + /// Mean of one light position's pixels: the box filter Hyperion uses, taken in linear light and encoded once per LIGHT. + static RGB meanOf(const VideoFrame& frame, Span cols, Span rows) { + uint32_t sr = 0, sg = 0, sb = 0; + for (int py = rows.begin; py < rows.end; py++) { + const uint8_t* px = frame.rgb + (static_cast(py) * frame.width + cols.begin) * 3; + for (int pxX = cols.begin; pxX < cols.end; pxX++, px += 3) { + sr += frame.channel(px, 0); + sg += frame.channel(px, 1); + sb += frame.channel(px, 2); + } + } + const uint32_t pixels = + static_cast(rows.end - rows.begin) * static_cast(cols.end - cols.begin); + const uint32_t mr = sr / pixels, mg = sg / pixels, mb = sb / pixels; + if (!frame.tone) return {static_cast(mr), static_cast(mg), static_cast(mb)}; + const uint8_t* enc = encodeTable(); + return {enc[mr], enc[mg], enc[mb]}; + } + + /// Move one channel `saturation` percent of the way out from `luma`, clamped to a byte. + uint8_t stretch(uint8_t v, int luma) const MM_NONBLOCKING { + const int out = luma + ((static_cast(v) - luma) * static_cast(saturation)) / 100; + return static_cast(out < 0 ? 0 : (out > 255 ? 255 : out)); + } + + /// Saturation runs on the RAW mean, before brightness: stretching around an already-dimmed luma would shrink the boost as the lights were turned down. + RGB adjust(RGB c) const { + if (saturation != 100) { + // Rec.601 weights (77/150/29 of 256). A flat (r+g+b)/3 would brighten greens and dim blues as saturation rose, because it is not what the eye does. + const int luma = static_cast((77 * c.r + 150 * c.g + 29 * c.b) >> 8); + c = {stretch(c.r, luma), stretch(c.g, luma), stretch(c.b, luma)}; + } + if (brightness != 255) { + c = {static_cast((c.r * brightness) / 255), + static_cast((c.g * brightness) / 255), + static_cast((c.b * brightness) / 255)}; + } + return c; + } + + /// Walk each channel a fraction of the way toward `color`. The state is 8.8 so the fraction of a step survives between frames: in whole bytes a slow setting rounds every step to zero. + RGB smooth(size_t lightId, RGB color) MM_NONBLOCKING { + const uint8_t target[3] = {color.r, color.g, color.b}; + const int32_t step = 256 - smoothing; // gap closed per frame, of 256 + const int32_t snap = static_cast(snapAbove) << 8; // 8.8, to compare against delta + uint8_t out[3]; + for (uint8_t ch = 0; ch < 3; ch++) { + const size_t slot = lightId * 3 + ch; // three accumulators per position, row-major + const int32_t held = state_[slot]; + const int32_t want = static_cast(target[ch]) << 8; + const int32_t delta = want - held; + // The first frame after a gap, and any move big enough to be a cut, land whole. + const bool jump = !primed_ || (snapAbove && (delta > snap || delta < -snap)); + // >> floors, so without the nudge a rising channel stalls one count short for ever, rendering white as 254, while a falling one arrives. + int32_t move = (delta * step) >> 8; + if (move == 0 && delta != 0) move = delta > 0 ? 1 : -1; + state_[slot] = static_cast(jump ? want : held + move); + out[ch] = static_cast(state_[slot] >> 8); + } + return {out[0], out[1], out[2]}; + } + + Bars bars_; // in effect + Bars candidate_; // seen most recently + uint8_t stable_ = 0; + + ScratchBuffer lit_{*this}; // packed y<<16|x, the positions that reach an LED + size_t litCount_ = 0; + bool allLit_ = true; // no list: the mapping lights every position + + ScratchBuffer state_{*this}; // 8.8 per channel per light position, while smoothing is on + bool primed_ = false; // false until one frame has been written + uint32_t lastSeq_ = 0; // the frame already on the strip +}; + +} // namespace mm diff --git a/src/light/layers/MappingLUT.h b/src/light/layers/MappingLUT.h index 416f283a7..e57bbb3fa 100644 --- a/src/light/layers/MappingLUT.h +++ b/src/light/layers/MappingLUT.h @@ -149,6 +149,13 @@ class MappingLUT { + static_cast(maxDest) * sizeof(nrOfLightsType); } + /// Hot path: does this logical index reach any physical light? Empty run = no destination. + bool hasDestination(nrOfLightsType logicalIdx) const MM_NONBLOCKING { + if (identity_) return true; + if (!offsets_ || logicalIdx >= logicalCount_) return false; + return offsets_[logicalIdx + 1] > offsets_[logicalIdx]; + } + /// Walk the physical destinations of one logical light, on the hot path. template void forEachDestination(nrOfLightsType logicalIdx, F&& callback) const { diff --git a/src/module_types.cpp b/src/module_types.cpp index edc9d0726..02ca9e010 100644 --- a/src/module_types.cpp +++ b/src/module_types.cpp @@ -84,6 +84,7 @@ #include "light/effects/RadialSpectrumEffect.h" #include "light/effects/VuMetersEffect.h" #include "light/effects/BeatRipplesEffect.h" +#include "light/effects/AmbilightEffect.h" #include "light/effects/AudioSpectrumEffect.h" #include "light/effects/SineEffect.h" #include "light/effects/DistortionWavesEffect.h" @@ -177,6 +178,7 @@ #include "core/system/ControlModule.h" #include "core/services/Services.h" #include "core/services/AudioService.h" +#include "core/services/VideoService.h" #include "core/services/OscModule.h" #include "core/system/I2cBusModule.h" #include "core/system/TasksModule.h" @@ -237,6 +239,7 @@ void mm::registerModuleTypes() { mm::ModuleFactory::registerType("TubesLayout", "light/layouts.md#tubes"); mm::ModuleFactory::registerType("WheelLayout", "light/layouts.md#wheel"); // Effects, registered alphabetically by display name: @xref{what-registertype-captures}. + mm::ModuleFactory::registerType("AmbilightEffect", "light/effects.md#ambilight"); mm::ModuleFactory::registerType("AudioSpectrumEffect", "light/effects.md#audiospectrum"); mm::ModuleFactory::registerType("RadialSpectrumEffect", "light/effects.md#radialspectrum"); mm::ModuleFactory::registerType("VuMetersEffect", "light/effects.md#vumeters"); @@ -355,6 +358,7 @@ void mm::registerModuleTypes() { mm::ModuleFactory::registerType("ControlModule", "core/system.md#control"); mm::ModuleFactory::registerType("Services", "core/services.md#services"); mm::ModuleFactory::registerType("AudioService", "core/services.md#audio"); + mm::ModuleFactory::registerType("VideoService", "core/services.md#video"); mm::ModuleFactory::registerType("OscModule", "core/services.md#osc"); mm::ModuleFactory::registerType("I2cBusModule", "core/system.md#i2c-bus"); mm::ModuleFactory::registerType("TasksModule", "core/system.md#tasks"); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 94626e2e5..0b5b422a3 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -7,6 +7,7 @@ add_executable(mm_tests unit/core/unit_SpscRing.cpp unit/core/unit_AnalogService.cpp unit/core/unit_AudioCapture.cpp + unit/core/unit_VideoService.cpp unit/core/unit_platform_audiofft.cpp unit/core/unit_Buffer.cpp unit/core/unit_ScratchBuffer.cpp @@ -165,6 +166,7 @@ add_executable(mm_tests unit/light/unit_Shader.cpp unit/light/unit_PolarLut.cpp unit/light/unit_PolarLut_equivalence.cpp + unit/light/unit_AmbilightEffect.cpp unit/light/unit_AuroraEffect.cpp unit/light/unit_NoiseEffect.cpp unit/light/unit_Effects_framerate.cpp diff --git a/test/scenarios/light/scenario_Video_source.json b/test/scenarios/light/scenario_Video_source.json new file mode 100644 index 000000000..aeedbaa14 --- /dev/null +++ b/test/scenarios/light/scenario_Video_source.json @@ -0,0 +1,317 @@ +{ + "name": "scenario_Video_source", + "module": "VideoService", + "mode": "mutate", + "also": [ + "SystemModule", + "Services", + "Layouts", + "GridLayout", + "Effects", + "Layer", + "RainbowEffect", + "Drivers", + "PreviewDriver", + "AmbilightEffect" + ], + "description": "Add / configure / remove the VideoService while the render pipeline runs, the video half of what scenario_Audio_mutation pins for audio. A Service is user-added at runtime, so every step asserts the pipeline keeps RENDERING through it: adding the service, writing its controls one at a time (source is an affectsPrepare control, so that write drives a full prepareTree mid-render), and removing it again. This also pins the type REGISTRY, since the runner builds every module through the firmware's own src/module_types.cpp: a service left out of it cannot be named here, where the unit tests construct the class directly and pass either way. It then adds the AmbilightEffect consumer, configures it, and REMOVES the service while the consumer is still live. That last step is what the one-active-source seat exists for: latestFrame() falls back to the static kNoVideoFrame and the effect paints black instead of dereferencing a frame whose buffer went with the service. Pixel correctness is pinned by unit_AmbilightEffect and unit_VideoService; this is the live wired-pipeline gate.", + "fixture": [ + { + "name": "fix-system", + "description": "SystemModule: the device's fixed identity/vitals root, mirroring main.cpp.", + "op": "add_module", + "id": "System", + "type": "SystemModule" + }, + { + "name": "fix-services", + "description": "Services: the top-level container user-added capability modules hang under, mirroring main.cpp where VideoService is a Service, not a System child.", + "op": "add_module", + "id": "Services", + "type": "Services" + }, + { + "name": "fix-layouts", + "op": "add_module", + "id": "Layouts", + "type": "Layouts" + }, + { + "name": "fix-grid", + "description": "The grid the pipeline renders on: 16:9 at 96x54. No effect reads the video frame in this scenario, so the layout is here to give the pipeline something real to tick, not to be sampled.", + "op": "add_module", + "id": "Grid", + "type": "GridLayout", + "parent_id": "Layouts", + "props": { + "width": 96, + "height": 54 + } + }, + { + "name": "fix-layers", + "op": "add_module", + "id": "Effects", + "type": "Effects", + "props": { + "layouts": "Layouts" + } + }, + { + "name": "fix-layer", + "op": "add_module", + "id": "Layer", + "type": "Layer", + "parent_id": "Effects", + "props": { + "channelsPerLight": 3 + } + }, + { + "name": "fix-rainbow", + "description": "A non-video effect so the pipeline has something to render before, and after, the video pair exists.", + "op": "add_module", + "id": "Rainbow", + "type": "RainbowEffect", + "parent_id": "Layer" + }, + { + "name": "fix-drivers", + "op": "add_module", + "id": "Drivers", + "type": "Drivers", + "props": { + "effects": "Effects" + } + }, + { + "name": "fix-preview", + "op": "add_module", + "id": "Preview", + "type": "PreviewDriver", + "parent_id": "Drivers" + } + ], + "steps": [ + { + "name": "measure-pipeline-only", + "description": "Baseline: the render pipeline runs with no video module present.", + "op": "measure", + "measure": true, + "bounds": { + "fps": { + "min": 1 + } + }, + "observed": { + "desktop-macos": { + "tick_us": { + "p50": 16, + "p95": 16, + "min": 16, + "max": 16, + "n": 1, + "samples": [16] + }, + "last_updated": "2026-10-10" + } + } + }, + { + "name": "add-video-module", + "description": "Add the VideoService under the Services container. It claims the one-active-source seat on construction and renders its test pattern; the pipeline must keep rendering.", + "op": "add_module", + "id": "Video", + "type": "VideoService", + "parent_id": "Services" + }, + { + "name": "measure-video-added", + "description": "Pipeline still renders with the video service added and no consumer reading it.", + "op": "measure", + "measure": true, + "bounds": { + "fps": { + "min": 1 + } + }, + "observed": { + "desktop-macos": { + "tick_us": { + "p50": 16, + "p95": 16, + "min": 16, + "max": 16, + "n": 1, + "samples": [16] + }, + "last_updated": "2026-10-10" + } + } + }, + { + "name": "configure-source", + "description": "Select the test-pattern source. This is an affectsPrepare control, so the write drives a full prepareTree mid-render: the buffer is resized and refilled while the pipeline ticks.", + "op": "set_control", + "id": "Video", + "key": "source", + "value": 0 + }, + { + "name": "measure-video-configured", + "description": "Pipeline still renders after the config writes, one of which rebuilt the tree.", + "op": "measure", + "measure": true, + "bounds": { + "fps": { + "min": 1 + } + }, + "observed": { + "desktop-macos": { + "tick_us": { + "p50": 16, + "p95": 16, + "min": 16, + "max": 16, + "n": 1, + "samples": [16] + }, + "last_updated": "2026-10-10" + } + } + }, + { + "name": "add-video-consumer", + "description": "Add an AmbilightEffect under the Layer. It reads the service through the static accessor, so it picks up the live frame with nothing having wired the two together.", + "op": "add_module", + "id": "Ambi", + "type": "AmbilightEffect", + "parent_id": "Layer" + }, + { + "name": "measure-consumer-live", + "description": "Producer and consumer both live: the border is being painted from the published frame.", + "op": "measure", + "measure": true, + "bounds": { + "fps": { + "min": 1 + } + }, + "observed": { + "desktop-macos": { + "tick_us": { + "p50": 70, + "p95": 70, + "min": 70, + "max": 70, + "n": 1, + "samples": [70] + }, + "last_updated": "2026-10-10" + } + } + }, + { + "name": "configure-consumer-brightness", + "description": "Configure the consumer while it renders: a plain scalar write that touches no buffer, so the resize in the next step is the only allocation under test.", + "op": "set_control", + "id": "Ambi", + "key": "brightness", + "value": 200 + }, + { + "name": "configure-consumer-smoothing", + "description": "Smoothing allocates the 8.8 per-channel state in prepare(), so this write resizes a live effect's buffer mid-render.", + "op": "set_control", + "id": "Ambi", + "key": "smoothing", + "value": 64 + }, + { + "name": "measure-consumer-configured", + "description": "Pipeline still renders after both consumer writes, one of which resized a buffer.", + "op": "measure", + "measure": true, + "bounds": { + "fps": { + "min": 1 + } + }, + "observed": { + "desktop-macos": { + "tick_us": { + "p50": 114, + "p95": 114, + "min": 114, + "max": 114, + "n": 1, + "samples": [114] + }, + "last_updated": "2026-10-10" + } + } + }, + { + "name": "remove-video-module", + "description": "Remove the service while the consumer is STILL live. latestFrame() must fall back to the static kNoVideoFrame, and the effect must paint black rather than dereference a frame whose buffer went with the service. This is the robustness rule's hardest case for this pair, and the one the seat and the borrowed-buffer teardown order exist for.", + "op": "remove_module", + "id": "Video" + }, + { + "name": "measure-after-video-removed", + "description": "Pipeline still renders with an orphaned consumer and no producer.", + "op": "measure", + "measure": true, + "bounds": { + "fps": { + "min": 1 + } + }, + "observed": { + "desktop-macos": { + "tick_us": { + "p50": 20, + "p95": 20, + "min": 20, + "max": 20, + "n": 1, + "samples": [20] + }, + "last_updated": "2026-10-10" + } + } + }, + { + "name": "remove-video-consumer", + "description": "Remove the orphaned consumer too: clean teardown, pipeline still live.", + "op": "remove_module", + "id": "Ambi" + }, + { + "name": "measure-back-to-baseline", + "description": "Back to the baseline tree, still rendering.", + "op": "measure", + "measure": true, + "bounds": { + "fps": { + "min": 1 + } + }, + "observed": { + "desktop-macos": { + "tick_us": { + "p50": 16, + "p95": 16, + "min": 16, + "max": 16, + "n": 1, + "samples": [16] + }, + "last_updated": "2026-10-10" + } + } + } + ] +} diff --git a/test/unit/core/unit_VideoService.cpp b/test/unit/core/unit_VideoService.cpp new file mode 100644 index 000000000..ceb7dc915 --- /dev/null +++ b/test/unit/core/unit_VideoService.cpp @@ -0,0 +1,177 @@ +/// @module VideoService +/// +/// Pins the PPM header grammar VideoService's file source accepts, the seat election that decides which service publishes, and the test pattern's sweep rate. +/// +/// @moreinfo +/// +/// The parser is the part with real edge cases: comments, whitespace runs, a 16-bit maxval, a truncated header. +/// It also decides where pixel data starts, so a wrong offset shows as a picture shifted by a few bytes rather than as a clean failure. +/// Driven directly, since it is a pure static, so these run without a filesystem and a malformed file is testable without writing one. + +#include "doctest.h" +#include "core/services/VideoService.h" + +#include +#include + +using mm::VideoService; + +namespace { +// Parse helper: returns the pixel offset, or -1, and reports the dimensions it read. +int parse(const char* text, uint16_t& w, uint16_t& h) { + return VideoService::parsePpmHeader(text, static_cast(std::strlen(text)), w, h); +} +} // namespace + +// The canonical form ffmpeg and ImageMagick emit: magic, dimensions, maxval, one newline, pixels. +TEST_CASE("VideoService PPM: a canonical P6 header yields the dimensions and the pixel offset") { + uint16_t w = 0, h = 0; + const char* hdr = "P6\n64 36\n255\n"; + CHECK(parse(hdr, w, h) == 13); // pixels begin straight after the final newline + CHECK(w == 64); + CHECK(h == 36); +} + +// Netpbm allows any run of whitespace between tokens and `#` comments to end of line. Both appear in real files, so both must be skipped without shifting the offset. +TEST_CASE("VideoService PPM: comments and whitespace runs are skipped, not counted as pixels") { + uint16_t w = 0, h = 0; + const char* hdr = "P6\n# CREATOR: GIMP\n 16 9 \n255\n"; + const int off = parse(hdr, w, h); + CHECK(off == static_cast(std::strlen(hdr))); + CHECK(w == 16); + CHECK(h == 9); +} + +// Exactly ONE whitespace byte separates the header from the binary block; any further byte is already a pixel. Consuming two would tint the whole image by shifting every channel one place. +TEST_CASE("VideoService PPM: only one separator byte is consumed before the pixels") { + uint16_t w = 0, h = 0; + // A leading pixel byte that happens to be whitespace-valued (0x20) must survive as data. + const char hdr[] = {'P', '6', '\n', '2', ' ', '2', '\n', '2', '5', '5', '\n', ' ', 'X'}; + const int off = VideoService::parsePpmHeader(hdr, static_cast(sizeof(hdr)), w, h); + CHECK(off == 11); // after the newline: NOT after the following 0x20 + CHECK(w == 2); + CHECK(h == 2); +} + +// P3 is the ASCII variant: same dimensions, completely different body (decimal text, not bytes). Accepting it would read numerals as pixel values and render noise. +TEST_CASE("VideoService PPM: the ASCII variant P3 is rejected, not read as binary") { + uint16_t w = 0, h = 0; + CHECK(parse("P3\n8 8\n255\n", w, h) == -1); +} + +// A 16-bit maxval means two big-endian bytes per sample: a different pixel format. Reading it as 8-bit would show the high bytes as a dim, doubled image, so it is refused rather than guessed at. +TEST_CASE("VideoService PPM: a 16-bit maxval is rejected rather than misread as 8-bit") { + uint16_t w = 0, h = 0; + CHECK(parse("P6\n8 8\n65535\n", w, h) == -1); +} + +// Garbage, an empty buffer, and a header cut off mid-token must all fail cleanly. The file source is fed by whatever the user uploads, so this is the ordinary case. +TEST_CASE("VideoService PPM: malformed and truncated headers fail without reading past the buffer") { + uint16_t w = 0, h = 0; + CHECK(parse("", w, h) == -1); + CHECK(parse("not an image at all", w, h) == -1); + CHECK(parse("P6", w, h) == -1); // magic only + CHECK(parse("P6\n64", w, h) == -1); // no height + CHECK(parse("P6\n64 36\n", w, h) == -1); // no maxval + CHECK(parse("P6\n64 36\n255", w, h) == -1); // no separator, so no pixel data can follow +} + +// A zero side has no pixels, and an absurd dimension would overflow the width*height*3 allocation size. Both are refused at the header rather than at the allocation. +TEST_CASE("VideoService PPM: zero and out-of-range dimensions are refused at the header") { + uint16_t w = 0, h = 0; + CHECK(parse("P6\n0 36\n255\n", w, h) == -1); + CHECK(parse("P6\n64 0\n255\n", w, h) == -1); + CHECK(parse("P6\n99999 36\n255\n", w, h) == -1); // past kMaxDim +} + +// With no service instantiated, latestFrame() still returns a readable struct, so an effect null-checks the frame's contents rather than the POINTER. Every device is in that state at boot. +TEST_CASE("VideoService: latestFrame is readable with no service present and reports no frame") { + const mm::VideoFrame* f = VideoService::latestFrame(); + REQUIRE(f != nullptr); + CHECK(f->rgb == nullptr); + CHECK(f->seq == 0); + CHECK(f->width == 0); + CHECK(f->height == 0); +} + +// Deleting the elected source while a second one runs must hand the seat over rather than go dark. The destructor vacates it and a running module re-claims an empty one on its next tick. +TEST_CASE("VideoService: a survivor takes over the seat when the elected source is destroyed") { + auto* elected = new VideoService(); // constructed first, so it claims the seat + elected->source = VideoService::kSourcePattern; // needs no file + elected->applyState(); + REQUIRE(VideoService::latestFrame()->rgb != nullptr); + + VideoService survivor; // seat already held, so its claim is a no-op + survivor.source = VideoService::kSourcePattern; + survivor.applyState(); + + delete elected; // ~ActiveInstance vacates: the seat is now empty + CHECK(VideoService::latestFrame()->rgb == nullptr); + + survivor.tick(); // the survivor inherits it + CHECK(VideoService::latestFrame()->rgb != nullptr); +} + +// Accepting a non-whitespace separator eats a pixel and shifts every channel one place, which tints the whole image rather than failing. +TEST_CASE("VideoService PPM: the separator must be whitespace, not merely present") { + uint16_t w = 0, h = 0; + CHECK(parse("P6\n2 2\n255X", w, h) == -1); + CHECK(parse("P6\n2 2\n255\n", w, h) == 11); // the same header with a real separator +} + +// The sweep is a RATE rather than a phase of the clock, so a rate change moves smoothly rather than jumping, and 0 parks the block as a still reference. +TEST_CASE("VideoService: the test pattern sweeps at patternSpeed pixels per second, and 0 parks it") { + struct ClockGuard { ~ClockGuard() { mm::platform::setTestNowMs(0); } } guard; + // Column of the white block on the top row, which is otherwise the red band. + auto sweepX = [] { + const mm::VideoFrame* f = VideoService::latestFrame(); + for (int x = 0; x < f->width; x++) + if (f->rgb[static_cast(x) * 3 + 1] == 255) return x; + return -1; + }; + + mm::platform::setTestNowMs(1000); + VideoService v; + v.source = VideoService::kSourcePattern; + v.patternSpeed = 10; + v.applyState(); // the first frame takes the clock and charges nothing + const int start = sweepX(); + REQUIRE(start >= 0); + + mm::platform::setTestNowMs(1500); // 0.5 s at 10 px/s + v.tick(); + CHECK(sweepX() == (start + 5) % VideoService::kPatternW); + + v.patternSpeed = 0; + mm::platform::setTestNowMs(4000); + v.tick(); + CHECK(sweepX() == (start + 5) % VideoService::kPatternW); // parked, however long passes +} + +// Every source publishes one, SDR included: sRGB is a curve like any other, and a consumer averaging raw bytes averages a quantity that is not proportional to light. +TEST_CASE("VideoService: an SDR source publishes the sRGB curve, not nothing") { + VideoService v; + v.source = VideoService::kSourcePattern; + v.applyState(); + const uint16_t* t = VideoService::latestFrame()->tone; + REQUIRE(t != nullptr); + CHECK(t[0] == 0); + CHECK(t[255] == mm::VideoFrame::kLinearMax); + // sRGB's midpoint is about 21% of full light: the whole reason averaging bytes is wrong. + CHECK(t[128] < mm::VideoFrame::kLinearMax / 3); + CHECK(t[128] > mm::VideoFrame::kLinearMax / 8); +} + +// The frame carries its curve and consumers read through channel(), so every reader corrects the same way and none has to know which curve it is. Null means the bytes are taken as they are. +TEST_CASE("VideoFrame: channel() reads through the tone curve when one is published") { + uint8_t px[3] = {10, 20, 30}; + uint16_t tone[256]; + for (int i = 0; i < 256; i++) tone[i] = static_cast(255 - i); + mm::VideoFrame f; + f.rgb = px; + CHECK(f.channel(px, 0) == 10); + CHECK(f.channel(px, 2) == 30); + f.tone = tone; + CHECK(f.channel(px, 0) == 245); + CHECK(f.channel(px, 1) == 235); +} diff --git a/test/unit/light/unit_AmbilightEffect.cpp b/test/unit/light/unit_AmbilightEffect.cpp new file mode 100644 index 000000000..08ad9070d --- /dev/null +++ b/test/unit/light/unit_AmbilightEffect.cpp @@ -0,0 +1,567 @@ +/// @module AmbilightEffect +/// +/// Pins the frame to light mapping end to end through the real static seam: a live VideoService publishes a frame, the effect renders it, the buffer is read back. +/// +/// @moreinfo +/// +/// The checks are about orientation and coverage, because a picture averaged over the wrong rectangle, or flipped top for bottom, still looks like *a* picture. + +#include "doctest.h" +#include "core/services/VideoService.h" +#include "light/effects/AmbilightEffect.h" +#include "light/layouts/GridLayout.h" +#include "light/layouts/LayoutBase.h" +#include "light/layouts/SphereLayout.h" +#include "platform/platform.h" // fsRootPath: ctest roots the filesystem in the build tree +#include "light/layouts/Layouts.h" + +#include +#include +#include + +using mm::AmbilightEffect; +using mm::VideoService; +namespace platform = mm::platform; + +namespace { + +// A live VideoService in test-pattern mode: red top band, blue bottom, yellow left, green right. Its seat is claimed on construction and vacated on destruction, so each case starts clean. +struct PatternSource { + VideoService svc; + PatternSource() { + svc.source = 0; // test pattern + svc.applyState(); // builds the buffer and renders the first frame + } +}; + +// Render one frame of the effect over a `w`x`h` grid and hand back the layer for inspection. +struct Rig { + mm::Layouts layouts; + mm::GridLayout grid; + mm::Layer layer; + AmbilightEffect fx; + + Rig(uint16_t w, uint16_t h) { + grid.width = w; + grid.height = h; + grid.depth = 1; + layouts.addChild(&grid); + layer.setLayouts(&layouts); + layer.setChannelsPerLight(3); + layer.addChild(&fx); + } + void render() { + layer.applyState(); + layer.tick(); + } + // applyState() re-runs prepare(), which un-primes the smoother and makes every frame jump. Anything testing the SMOOTHING has to tick without it. + void tickOnly() { layer.tick(); } + /// A tick with a NEW frame behind it: the effect skips a repeated one, so anything measuring per-frame behavior has to advance the source too, as the scheduler does. + void tickOnly(VideoService& source) { + source.tick(); + layer.tick(); + } + const uint8_t* px(int x, int y) const { + return layer.buffer().data() + (static_cast(y) * grid.width + x) * 3; + } +}; + +} // namespace + +// Orientation must survive to the buffer: red top band → red first row. Swapped, the whole picture is upside down: on a TV border, the difference between matching the screen and mirroring it. +TEST_CASE("AmbilightEffect: the frame's orientation reaches the buffer, top band to top row") { + PatternSource src; + Rig rig(8, 8); + rig.fx.saturation = 100; // identity, so the raw zone means are what we read + rig.render(); + + const uint8_t* top = rig.px(4, 0); + const uint8_t* bottom = rig.px(4, 7); + CHECK(top[0] > top[2]); // top row reads red-dominant + CHECK(bottom[2] > bottom[0]); // bottom row reads blue-dominant +} + +// The horizontal counterpart of the test above; together they pin all four edges. Red separates the bands: it is in yellow and absent from green. +TEST_CASE("AmbilightEffect: the frame's left and right bands reach the matching columns") { + PatternSource src; + Rig rig(8, 8); + rig.fx.saturation = 100; + rig.render(); + + const uint8_t* left = rig.px(0, 4); // mid-height, so neither the top nor bottom band + const uint8_t* right = rig.px(7, 4); + CHECK(left[0] > right[0]); // yellow carries red; green does not + CHECK(right[1] > 0); // and the right column is lit at all +} + +// Every light must be written. An empty zone leaves its light holding the previous frame, so a mapping bug shows as dead lights scattered through the strip rather than an obvious failure. +TEST_CASE("AmbilightEffect: every light is written, none left dark by an empty zone") { + PatternSource src; + Rig rig(16, 9); + rig.fx.saturation = 100; + rig.render(); + + // WRITTEN, not lit: the pattern's center is black, so counting lit cells cannot tell "every position was painted" from "one was". Fill with a value the effect cannot produce instead. + constexpr uint8_t kSentinel = 0x5A; + std::memset(rig.layer.buffer().data(), kSentinel, static_cast(16) * 9 * 3); + rig.render(); + + for (int y = 0; y < 9; y++) + for (int x = 0; x < 16; x++) { + const uint8_t* p = rig.px(x, y); + const bool untouched = p[0] == kSentinel && p[1] == kSentinel && p[2] == kSentinel; + CHECK_FALSE(untouched); + } + // The corners sit inside the colored bands and must never be dark. + for (const auto& [x, y] : {std::pair{0, 0}, std::pair{15, 0}, std::pair{0, 8}, std::pair{15, 8}}) { + const uint8_t* p = rig.px(x, y); + CHECK((p[0] || p[1] || p[2])); + } +} + +// More lights than source pixels: neighboring positions must SHARE one rather than resolve to an empty zone. The pattern is 64 wide, so a 128-wide layer forces the guard. +TEST_CASE("AmbilightEffect: a layer finer than the frame still writes every light") { + PatternSource src; + Rig rig(128, 4); + rig.fx.saturation = 100; + rig.render(); + + // Top row of the pattern is the red band. At 4 rows tall every row samples some band, so the whole first row must be lit across its full width. + for (int x = 0; x < 128; x++) { + const uint8_t* p = rig.px(x, 0); + CHECK((p[0] || p[1] || p[2])); + } +} + +// brightness scales the result down uniformly. Distinct from the driver's brightness: this one dims the video relative to whatever else is composited beside it. +TEST_CASE("AmbilightEffect: brightness scales the sampled color down") { + PatternSource src; + Rig full(8, 8); + full.fx.saturation = 100; + full.fx.brightness = 255; + full.render(); + const int fullRed = full.px(4, 0)[0]; + + Rig dim(8, 8); + dim.fx.saturation = 100; + dim.fx.brightness = 64; + dim.render(); + const int dimRed = dim.px(4, 0)[0]; + + CHECK(fullRed > 0); + CHECK(dimRed < fullRed); + CHECK(dimRed == (fullRed * 64) / 255); +} + +// Saturation stretches each channel away from the zone's luma: above 100 a colored zone gets more saturated, which is what pulls averaged means back off gray. +TEST_CASE("AmbilightEffect: saturation above 100 pushes a colored zone further from gray") { + PatternSource src; + Rig flat(8, 8); + flat.fx.saturation = 100; + flat.render(); + const uint8_t* a = flat.px(4, 0); + const int flatSpread = static_cast(a[0]) - static_cast(a[2]); + + Rig boosted(8, 8); + boosted.fx.saturation = 180; + boosted.render(); + const uint8_t* b = boosted.px(4, 0); + const int boostedSpread = static_cast(b[0]) - static_cast(b[2]); + + CHECK(flatSpread > 0); + CHECK(boostedSpread >= flatSpread); // red pulled further from blue, or already clipped at 255 +} + +// No source paints black rather than returning early, which would leave the PREVIOUS effect's picture frozen on the strip. Every effect owns its background (unit_Effects_gridsweep). +TEST_CASE("AmbilightEffect: no video source paints black, never the previous effect's frame") { + // No PatternSource here, so no service holds the seat and latestFrame() reports nothing. + REQUIRE(VideoService::latestFrame()->rgb == nullptr); + + Rig rig(4, 4); + rig.layer.applyState(); + // Paint a recognizable frame, standing in for whatever effect ran before this one. + uint8_t* buf = rig.layer.buffer().data(); + REQUIRE(buf != nullptr); + for (size_t i = 0; i < rig.layer.buffer().count(); i++) { + buf[i * 3 + 0] = 11; + buf[i * 3 + 1] = 22; + buf[i * 3 + 2] = 33; + } + rig.layer.tick(); + CHECK(rig.px(2, 2)[0] == 0); + CHECK(rig.px(2, 2)[1] == 0); + CHECK(rig.px(2, 2)[2] == 0); +} + +// --- Smoothing --------------------------------------------------------------------------------- +// The accumulators are 8.8 precisely so a slow setting still ARRIVES: in whole bytes every step rounds to zero and the light stalls short of its target forever. These check that it converges, and that a big move still lands at once. + +// Off must be bit-identical to no smoothing at all, since it is the default. +TEST_CASE("AmbilightEffect: smoothing off follows the frame exactly") { + PatternSource src; + Rig plain(8, 8), off(8, 8); + plain.fx.saturation = 100; + off.fx.saturation = 100; + off.fx.smoothing = 0; + plain.render(); + off.render(); + CHECK(std::memcmp(plain.px(4, 0), off.px(4, 0), 3) == 0); +} + +// The first frame after a gap must land immediately: creeping up from black would show as a fade-in every time the source reconnects. +TEST_CASE("AmbilightEffect: the first frame lands whole, not smoothed up out of black") { + PatternSource src; + Rig fast(8, 8), slow(8, 8); + fast.fx.saturation = 100; + slow.fx.saturation = 100; + slow.fx.smoothing = 240; // very slow, so a smoothed first frame would be nearly black + fast.render(); + slow.render(); + CHECK(std::memcmp(fast.px(4, 0), slow.px(4, 0), 3) == 0); +} + +// The one that 8-bit state would fail: with heavy smoothing each step is a fraction of a byte. Both directions, because >> floors and the two arrive by different routes. +TEST_CASE("AmbilightEffect: a heavily smoothed light converges exactly, rising and falling") { + PatternSource src; + Rig rig(8, 8); + rig.fx.saturation = 100; + rig.fx.smoothing = 250; // a step of ~6/256 of the remaining distance + rig.fx.snapAbove = 0; // never jump, so only the smoothing can get it there + rig.render(); // primes on the first frame, so this one lands whole + + const uint8_t bright = rig.px(4, 0)[0]; + REQUIRE(bright > 8); // the band has somewhere to fall from + + // Falling: drive the target down and let it smooth in. + rig.fx.brightness = 8; + for (int i = 0; i < 2000; i++) rig.tickOnly(src.svc); + const uint8_t dim = rig.px(4, 0)[0]; + CHECK(dim < bright / 2); + + // Rising back to where it started must land on the SAME value, not one short. + rig.fx.brightness = 255; + for (int i = 0; i < 2000; i++) rig.tickOnly(src.svc); + CHECK(rig.px(4, 0)[0] == bright); +} + +// --- edgeDepth --------------------------------------------------------------------------------- +// A border light's own share is 1/height of the frame, a sliver at the very edge. edgeDepth lets the outermost positions reach further in without changing how many there are. + +// Off is the default, so it must be the plain division exactly. +TEST_CASE("AmbilightEffect: edgeDepth 0 leaves the zones as the plain division") { + PatternSource src; + Rig plain(8, 8), zero(8, 8); + plain.fx.saturation = 100; + zero.fx.saturation = 100; + zero.fx.edgeDepth = 0; + plain.render(); + zero.render(); + CHECK(std::memcmp(plain.px(4, 0), zero.px(4, 0), 3) == 0); +} + +// The point of the control: the top row must see further down the picture than its own 1/8 share. +TEST_CASE("AmbilightEffect: edgeDepth makes the outer row sample deeper") { + PatternSource src; + Rig shallow(8, 8), deep(8, 8); + shallow.fx.saturation = 100; + deep.fx.saturation = 100; + deep.fx.edgeDepth = 50; // half the frame, so it reaches well past the top band + shallow.render(); + deep.render(); + CHECK(std::memcmp(shallow.px(4, 0), deep.px(4, 0), 3) != 0); +} + +// The control SETS the depth, it does not raise a floor: a value below a position's own share must make its zone thinner +TEST_CASE("AmbilightEffect: edgeDepth below the natural share makes the outer row thinner") { + PatternSource src; + // Three rows over a 36-tall pattern is a 12-row share, which reaches past the 9-row red band into the black center. A shallower zone stays inside the band, so it reads BRIGHTER. + Rig plain(8, 3), thin(8, 3); + plain.fx.saturation = 100; + thin.fx.saturation = 100; + thin.fx.edgeDepth = 10; // ~4 rows, against a natural share of 12 + plain.render(); + thin.render(); + CHECK(thin.px(4, 0)[0] > plain.px(4, 0)[0]); +} + +// Only the outermost ring moves. An interior position has no edge to reach in from, so a video wall is unaffected even with the control turned up. +TEST_CASE("AmbilightEffect: edgeDepth leaves interior positions alone") { + PatternSource src; + Rig plain(8, 8), deep(8, 8); + plain.fx.saturation = 100; + deep.fx.saturation = 100; + deep.fx.edgeDepth = 50; + plain.render(); + deep.render(); + for (int y = 1; y < 7; y++) + for (int x = 1; x < 7; x++) CHECK(std::memcmp(plain.px(x, y), deep.px(x, y), 3) == 0); +} + +// --- Black bars -------------------------------------------------------------------------------- +// A letterboxed film puts bars exactly where the top and bottom lights look, so those lights go dark while the picture is bright. Detection moves the zones past the bar, which edgeDepth cannot do. + +namespace { + +// A VideoService reading a P6 PPM written here: `bar` black rows top and bottom, green between. Written to a file because that is the seam the file source actually uses. +struct Letterbox { + VideoService svc; + char path[64] = {}; + Letterbox(int w, int h, int bar) { + // The service resolves a bare name under the desktop filesystem root, which ctest points into the build tree. Ask for the resolved root rather than assuming one. + std::snprintf(path, sizeof(path), "mm_letterbox_%dx%d_%d.ppm", w, h, bar); + char real[256]; + std::snprintf(real, sizeof(real), "%s/%s", mm::platform::fsRootPath(), path); + mm::platform::fsMkdir("/"); // ctest's root is a path, not necessarily a directory yet + std::FILE* f = std::fopen(real, "wb"); + REQUIRE(f != nullptr); + std::fprintf(f, "P6\n%d %d\n255\n", w, h); + for (int y = 0; y < h; y++) + for (int x = 0; x < w; x++) { + const bool picture = y >= bar && y < h - bar; + const uint8_t px[3] = {0, picture ? uint8_t(200) : uint8_t(0), 0}; + std::fwrite(px, 1, 3, f); + } + std::fclose(f); + svc.source = 1; + std::strncpy(svc.file, path, sizeof(svc.file) - 1); + svc.applyState(); + } + ~Letterbox() { + char real[256]; + std::snprintf(real, sizeof(real), "%s/%s", mm::platform::fsRootPath(), path); + std::remove(real); + } +}; + + +// A frame every line of which reads DARK to the bar probe, with brighter outer rows than middle ones, so a wrongly adopted bar changes what the edge lights see. Same shape as Letterbox. +struct DimFrame { + VideoService svc; + char path[64] = {}; + DimFrame(int w, int h, uint8_t outer, uint8_t inner, int band) { + std::snprintf(path, sizeof(path), "mm_dim_%dx%d_%d.ppm", w, h, band); + char real[256]; + std::snprintf(real, sizeof(real), "%s/%s", mm::platform::fsRootPath(), path); + mm::platform::fsMkdir("/"); + std::FILE* f = std::fopen(real, "wb"); + REQUIRE(f != nullptr); + std::fprintf(f, "P6\n%d %d\n255\n", w, h); + for (int y = 0; y < h; y++) + for (int x = 0; x < w; x++) { + const uint8_t v = (y < band || y >= h - band) ? outer : inner; + const uint8_t px[3] = {v, v, v}; + std::fwrite(px, 1, 3, f); + } + std::fclose(f); + svc.source = 1; + std::strncpy(svc.file, path, sizeof(svc.file) - 1); + svc.applyState(); + } + ~DimFrame() { + char real[256]; + std::snprintf(real, sizeof(real), "%s/%s", mm::platform::fsRootPath(), path); + std::remove(real); + } +}; + +} // namespace + +// Off by default: no scan, no shift, identical to before the feature existed. +TEST_CASE("AmbilightEffect: blackBars off maps across the whole frame") { + PatternSource src; + Rig plain(8, 8), off(8, 8); + plain.fx.saturation = 100; + off.fx.saturation = 100; + off.fx.detectBlackBars = false; + plain.render(); + off.render(); + CHECK(std::memcmp(plain.px(4, 0), off.px(4, 0), 3) == 0); +} + +// The top light sits in the bar and reads black; with detection it reads the picture instead. +TEST_CASE("AmbilightEffect: the top light escapes the letterbox once bars are detected") { + Letterbox src(64, 64, 16); // a quarter of the height black at each end + Rig rig(8, 8); + rig.fx.saturation = 100; + rig.fx.detectBlackBars = true; + rig.render(); + + CHECK(rig.px(4, 0)[1] == 0); // first frame: still inside the bar + for (int i = 0; i < 60; i++) rig.tickOnly(src.svc); // past the hysteresis window + CHECK(rig.px(4, 0)[1] > 100); // now reading the green picture +} + +// Dark EVERYWHERE by the probe, but not uniform. Scanning to the ceiling used to report the deepest bar on all four edges, cropping a dark scene to its middle for kStableFrames after it ended. +TEST_CASE("AmbilightEffect: an all-dark frame reports no bars, not the deepest ones") { + // 40% of 64 is a 25-line ceiling. Rows 0..24 sit at 60, under barLevel 64, so the scan runs the whole way; rows 25..38 are black, which the top light would read if a bar were adopted. + DimFrame src(64, 64, 60, 0, 25); + Rig rig(8, 8); + rig.fx.saturation = 100; + rig.fx.detectBlackBars = true; + rig.fx.barLevel = 64; + rig.render(); + + const uint8_t first = rig.px(4, 0)[0]; + REQUIRE(first > 0); // reading the bright outer rows, not the middle + for (int i = 0; i < 60; i++) rig.tickOnly(src.svc); // past the hysteresis window + CHECK(rig.px(4, 0)[0] == first); // nothing adopted, so nothing moved +} + +// The pattern's center is black and its edges are colored: the OPPOSITE of a letterbox. Nothing must be detected in it, or a picture that fills the frame would get cropped. +TEST_CASE("AmbilightEffect: a frame that fills the picture reports no bars") { + PatternSource src; + Rig plain(8, 8), armed(8, 8); + plain.fx.saturation = 100; + armed.fx.saturation = 100; + armed.fx.detectBlackBars = true; + plain.render(); + armed.render(); + for (int i = 0; i < 60; i++) armed.tickOnly(src.svc); + CHECK(std::memcmp(plain.px(4, 0), armed.px(4, 0), 3) == 0); +} + +// --- Skipping positions that reach no LED ------------------------------------------------------- +// A border layout maps its interior to nothing, so the effect asks the LUT and skips those positions. GridLayout is identity, so the video-wall case is unaffected. + +namespace { + +// A hollow rectangle, local to this file: perimeter lit, interior reaching no LED. Local rather than a shipped layout, since what is under test is the EFFECT asking the mapping. +struct HollowRect : mm::LayoutBase { + uint16_t width = 8; + uint16_t height = 8; + + mm::nrOfLightsType lightCount() const override { + return static_cast(2 * width + 2 * height - 4); // corners counted once + } + + void placeLights(const mm::CoordSink& sink) const override { + mm::nrOfLightsType i = 0; + const int w = width, h = height; + for (int x = 0; x < w; x++) sink.pixel(i++, static_cast(x), 0, 0); + for (int y = 1; y < h; y++) sink.pixel(i++, static_cast(w - 1), static_cast(y), 0); + for (int x = w - 2; x >= 0; x--) sink.pixel(i++, static_cast(x), static_cast(h - 1), 0); + for (int y = h - 2; y >= 1; y--) sink.pixel(i++, 0, static_cast(y), 0); + } +}; + +// The Rig above, with that hollow rectangle in place of the grid. +struct RectRig { + mm::Layouts layouts; + HollowRect rect; + mm::Layer layer; + AmbilightEffect fx; + + RectRig(uint16_t w, uint16_t h) { + rect.width = w; + rect.height = h; + layouts.addChild(&rect); + layer.setLayouts(&layouts); + layer.setChannelsPerLight(3); + layer.addChild(&fx); + } + void render() { + layer.applyState(); + layer.tick(); + } + const uint8_t* px(int x, int y) const { + return layer.buffer().data() + (static_cast(y) * rect.width + x) * 3; + } +}; + +} // namespace + +TEST_CASE("AmbilightEffect: a border layout paints its perimeter and skips the interior") { + PatternSource src; + RectRig rig(8, 8); + rig.fx.saturation = 100; + rig.render(); + + // The perimeter reaches LEDs, so it carries the picture. + const uint8_t* top = rig.px(4, 0); + CHECK((top[0] | top[1] | top[2]) != 0); + + // The interior reaches none. Never averaged, never written. it holds the black that Layer::prepare() left in the buffer on the rebuild the effect's own prepare() rode in on. + for (int y = 1; y < 7; y++) + for (int x = 1; x < 7; x++) { + const uint8_t* p = rig.px(x, y); + CHECK((p[0] | p[1] | p[2]) == 0); + } + + // And it stays black across further frames, rather than only on the first. + for (int i = 0; i < 5; i++) rig.layer.tick(); + for (int y = 1; y < 7; y++) + for (int x = 1; x < 7; x++) { + const uint8_t* p = rig.px(x, y); + CHECK((p[0] | p[1] | p[2]) == 0); + } +} + +// The effect is D2 and Layer::extrude() copies its front slice across the depth. On a sparse 3D layout a column whose only LEDs sit at z > 0 must be painted at z = 0 too. +TEST_CASE("AmbilightEffect: a sparse 3D layout lights an LED behind an empty front-face position") { + PatternSource src; + mm::Layouts layouts; + mm::SphereLayout sphere; + mm::Layer layer; + AmbilightEffect fx; + sphere.radius = 2; // a 5x5x5 box; the shell point (4,2,2) has no LED anywhere at z = 0 in its column + layouts.addChild(&sphere); + layer.setLayouts(&layouts); + layer.setChannelsPerLight(3); + layer.addChild(&fx); + fx.saturation = 100; + layer.applyState(); + layer.tick(); + + const mm::MappingLUT& lut = layer.lut(); + REQUIRE(layer.depth() == 5); + REQUIRE_FALSE(lut.hasDestination(2 * 5 + 4)); // (4,2) at z = 0: nothing + REQUIRE(lut.hasDestination((2 * 5 + 2) * 5 + 4)); // (4,2) at z = 2: on the shell + const uint8_t* p = layer.buffer().data() + ((2 * 5 + 2) * 5 + 4) * 3; + CHECK((p[0] | p[1] | p[2]) != 0); +} + + +// --- Skipping a repeated frame ----------------------------------------------------------------- +// The render loop outruns the source (60 Hz against 30 fps video, or a still picture) and re-averaging a frame already on the strip buys nothing. What the effect advances then runs at the source's rate, which is the rate it should run at. + +// A skipped tick must leave the strip exactly as it was, not half-painted or cleared. +TEST_CASE("AmbilightEffect: a repeated frame leaves the strip untouched") { + PatternSource src; + Rig rig(8, 8); + rig.fx.saturation = 100; + rig.render(); + + uint8_t before[3]; + std::memcpy(before, rig.px(4, 0), 3); + for (int i = 0; i < 5; i++) rig.tickOnly(); // no new frame published + CHECK(std::memcmp(before, rig.px(4, 0), 3) == 0); +} + +// Turning detection off has to clear the hysteresis, not only the adopted bars. A surviving candidate with a saturated counter agrees with itself the moment detection returns, so the adoption branch never fires again and the control looks dead. +TEST_CASE("AmbilightEffect: detection can be turned off and on again") { + Letterbox src(64, 64, 16); + Rig rig(8, 8); + rig.fx.saturation = 100; + rig.fx.detectBlackBars = true; + rig.render(); + for (int i = 0; i < 60; i++) rig.tickOnly(src.svc); + REQUIRE(rig.px(4, 0)[1] > 100); // bars adopted, reading the picture + + rig.fx.detectBlackBars = false; + rig.tickOnly(src.svc); + CHECK(rig.px(4, 0)[1] == 0); // back to the frame, so back inside the bar + + rig.fx.detectBlackBars = true; + for (int i = 0; i < 60; i++) rig.tickOnly(src.svc); + CHECK(rig.px(4, 0)[1] > 100); // and adopted again rather than stuck +} + +// Averaging happens in LINEAR light rather than in the encoding: half black and half white is half the light, which sRGB writes as 188 where the byte mean is 128. +TEST_CASE("AmbilightEffect: a part-lit zone averages as light, not as bytes") { + PatternSource src; + Rig rig(1, 1); + rig.fx.saturation = 100; // identity, so the zone mean is what we read + rig.render(); + CHECK(rig.px(0, 0)[0] > 120); + CHECK(rig.px(0, 0)[0] < 180); +} From 7e32f1e9446b15bd74a27cfd068960accf42ff7c Mon Sep 17 00:00:00 2001 From: Shura Fedorovskyi Date: Sat, 10 Oct 2026 13:19:38 +0400 Subject: [PATCH 2/3] Capture video from a USB grabber on the ESP32-P4 The video source gains a third option: an HDMI grabber presenting itself as a UVC webcam, streaming MJPEG that the P4's JPEG hardware decodes. A console or a PC on the grabber's input now drives the lights directly. Every other target links a stub whose init fails, which the service reports as a status rather than an error. **Platform** - A UVC capture seam in platform.h: the device handle, the advertised format rows, the frame accessor and the drop counters - platform_esp32_usbvideo.cpp: the UVC stream and the hardware JPEG decode across three threads, with the decode off the render tick and the newest frame winning when one is still pending - Buffers belong to the platform for the life of one open, so a published frame borrows rather than copies, and the renderer drops it before deinit - Bulk payload headers are stripped by a stride read off the first header, which usb_host_uvc leaves in when a device's payload is a multiple of the packet size - Gated on the chip AND both SoC capabilities, since only the P4 has a High-Speed USB PHY and a JPEG decoder **Core** - VideoService: the usb source, the offered-format picker, staleMs, and hdr with the PQ and HLG curves, since MJPEG carries no HDR metadata and a grabber strips what the console sends - A hotplug is noticed by one atomic load a second, answered by a tree rebuild on the thread that may open the device **Tests** - unit_VideoService: the capture-capability pair and the PQ curve recovering a measured source's linear ratio **Docs/CI** - The Video card gains its four capture controls; details gain the P4 requirement, format choice, HDR and staleness - esp32: the usb_host_uvc component for the P4 alone, and a 2 KB control-transfer buffer, without which an MS2130 never enumerates - .clangd points the ESP32 platform directory at a firmware compile database Built for esp32p4rev3-eth-wifi, which links the UVC driver, and for esp32s3-n16r8, which links stubs and no component. docgen: appendix lines 5643 to 5669, four for VideoService's restored capture sections. Co-Authored-By: Claude Opus 5 (1M context) --- docs/moonmodules/core/services.md | 20 +- esp32/main/CMakeLists.txt | 1 + esp32/main/idf_component.yml | 6 + esp32/sdkconfig.defaults.esp32p4rev1-eth | 3 + src/core/services/VideoService.h | 245 +++++++- src/platform/desktop/platform_config.h | 3 + src/platform/desktop/platform_desktop.cpp | 14 + src/platform/esp32/.clangd | 15 + src/platform/esp32/platform_config.h | 9 + .../esp32/platform_esp32_usbvideo.cpp | 548 ++++++++++++++++++ src/platform/platform.h | 59 ++ test/unit/core/unit_VideoService.cpp | 38 ++ 12 files changed, 940 insertions(+), 21 deletions(-) create mode 100644 src/platform/esp32/.clangd create mode 100644 src/platform/esp32/platform_esp32_usbvideo.cpp diff --git a/docs/moonmodules/core/services.md b/docs/moonmodules/core/services.md index 9825615f2..b0e7df9aa 100644 --- a/docs/moonmodules/core/services.md +++ b/docs/moonmodules/core/services.md @@ -48,13 +48,17 @@ Detail: [technical](moxygen/AudioService.md) · [the sync packet](moxygen/WLEDAu Video service card -A user-added Service: the video source a screen-follow effect reads. The counterpart of [Audio](#audio) for a picture, decoded once per tick however many effects want it. `source` decides which controls show. The pattern, the file format and the tone curve: ⌄ details. +A user-added Service: the video source a screen-follow effect reads. The counterpart of [Audio](#audio) for a picture, decoded once per tick however many effects want it. `source` decides which controls show. Sources, HDR and staleness: ⌄ details. -- `source`: `test pattern` needs no hardware, `file` reads a PPM off the filesystem. +- `source`: `test pattern` needs no hardware, `file` reads a PPM, `usb` captures from an HDMI grabber. - `patternSpeed`: (test pattern) sweep rate of the white block, in pixels per second. 0 parks it. - `file`: (file) path to a binary PPM (P6, maxval 255), uploaded through the File Manager. - `reload`: (file) re-read the file in place, without rebuilding the pipeline. -- status: the live frame's dimensions (`64x36`), or the reason there is no frame. +- `offered`: (usb) the resolution and frame rate to request, from what the device advertises. +- `staleMs`: (usb) how long a gap in frames is tolerated before the lights go dark. +- `hdr`: (usb) the source's transfer curve: `off`, `HDR10 (PQ)` or `HLG`. Declared, not detected. +- `hdrNits`: (usb, PQ only) the reference white PQ's absolute luminance is scaled to. +- status: the live frame's dimensions (`640x480`), or the reason there is no frame. Detail: [technical](moxygen/VideoService.md) @@ -281,6 +285,16 @@ ffmpeg -i clip.mp4 -frames:v 1 -vf scale=64:36 -pix_fmt rgb24 frame.ppm The header is parsed strictly, because the file is whatever a user uploaded: comments and whitespace runs are skipped per Netpbm, a 16-bit maxval is refused rather than misread as 8-bit, and exactly one whitespace byte is consumed before the pixels. Accepting a non-whitespace separator there would eat a pixel and shift every channel one place, which tints the whole image instead of failing. +**The USB source needs an ESP32-P4.** It wants two things at once and only that chip has both: a High-Speed USB PHY (the S3 has USB, but too slow to carry video) and a hardware JPEG decoder. Elsewhere the option is not offered and the two software sources still work. Choosing a format: + +- **MJPEG only.** It is what the JPEG hardware decodes, so other encodings the device advertises are filtered out of `offered` rather than listed and then refused. +- **Pick 16:9.** A 4:3 capture makes a 16:9 source letterbox into it, putting black bars where the top and bottom lights look. For bars that are in the source itself, see [Ambilight](../light/effects.md#ambilight)'s `detectBlackBars`. +- **Then prefer frame rate over resolution.** A border light averages a few hundred pixels whatever the capture size, so resolution buys nothing and bandwidth is the scarce thing. + +**HDR is declared, not detected.** MJPEG carries no HDR metadata and a grabber strips what the console sends, so the curve is a control. With an HDR source left at `off` the lights read washed out and hue-shifted, green lifted against red being the usual sign, because the bytes are averaged on the HDR curve rather than the display's. `hdrNits` sets the reference white PQ's absolute luminance is scaled to: too low and bright channels clamp, dragging saturated hues toward their neighbors; too high and the picture reads dim. HLG is relative and does not use it. + +**Staleness is why `staleMs` exists.** The decoder runs at its own rate, so a gap of one tick is normal and the last frame stays up. A long gap means the source stopped, a console asleep or a cable out, and holding the picture would leave the room lit by a frozen frame. Past `staleMs` the frame is dropped and every effect falls back to black. Floored above a frame interval rather than 0, since the render loop outruns the capture and ordinary gaps would otherwise read as loss and strobe the room. + **Every frame carries its tone curve.** A published frame names the curve from its own encoding to linear light, and a consumer reads pixels through it. SDR is sRGB, a curve like any other, so the frame always carries one rather than leaving the reader to assume. This matters because averaging is what a screen-follow effect does, and the mean of encoded bytes is not the mean of the light: half black and half white averages to 128 as bytes where the light is 188. The table is 12-bit rather than 8, since linear has no headroom at the dark end, which is what encodings exist for. ## OSC, details diff --git a/esp32/main/CMakeLists.txt b/esp32/main/CMakeLists.txt index 391660095..a88dfc5c3 100644 --- a/esp32/main/CMakeLists.txt +++ b/esp32/main/CMakeLists.txt @@ -42,6 +42,7 @@ idf_component_register( "../../src/platform/esp32/platform_esp32_i2c.cpp" "../../src/platform/esp32/platform_esp32_ir.cpp" "../../src/platform/esp32/platform_esp32_usb_midi.cpp" + "../../src/platform/esp32/platform_esp32_usbvideo.cpp" "../../src/platform/esp32/platform_esp32_es8311.cpp" INCLUDE_DIRS "../../src" diff --git a/esp32/main/idf_component.yml b/esp32/main/idf_component.yml index 1ba79df79..df7e23249 100644 --- a/esp32/main/idf_component.yml +++ b/esp32/main/idf_component.yml @@ -87,3 +87,9 @@ dependencies: version: "~2.12.9" rules: - if: "$CONFIG{MM_P4_WIFI} == True" + # usb_host_uvc: the UVC (webcam) host, which is how a grabber's frames are read. P4 only, since no + # other ESP32 has a High-Speed USB host and at Full Speed (~1 MB/s) MJPEG starves. + espressif/usb_host_uvc: + version: "^2.5.2" + rules: + - if: "target == esp32p4" diff --git a/esp32/sdkconfig.defaults.esp32p4rev1-eth b/esp32/sdkconfig.defaults.esp32p4rev1-eth index 43a80033f..39479d839 100644 --- a/esp32/sdkconfig.defaults.esp32p4rev1-eth +++ b/esp32/sdkconfig.defaults.esp32p4rev1-eth @@ -92,3 +92,6 @@ CONFIG_HEAP_HAS_EXEC_HEAP=y # nothing (3 reboots in 70 s against 4), which is what a blocked network cascade looks like, not an # FFT fault. Lives in the board fragment because every P4 image (rev1/rev3, eth/eth-wifi) layers on it. # CONFIG_DSP_ANSI=y + +# USB host control-transfer buffer. A UVC configuration descriptor lists every format and frame size, 1-3 KB on a grabber, and enum.c rejects any descriptor longer than this buffer before a class driver is consulted: at the default 256 an MS2130 never enumerates. Costs 2 KB of heap per attached device, and lives here because every P4 image layers on this fragment. +CONFIG_USB_HOST_CONTROL_TRANSFER_MAX_SIZE=2048 diff --git a/src/core/services/VideoService.h b/src/core/services/VideoService.h index e094ab88e..4391d1ea4 100644 --- a/src/core/services/VideoService.h +++ b/src/core/services/VideoService.h @@ -1,6 +1,7 @@ #pragma once #include "core/util/ActiveInstance.h" // the one-active-source seat (RAII vacate on destruct) +#include "core/module/Scheduler.h" // requestPrepareTree: a hotplug needs a cold-path rebuild #include "core/util/color.h" // RGB: the pattern's band colors #include "core/module/MoonModule.h" #include "core/util/ScratchBuffer.h" @@ -18,12 +19,16 @@ namespace mm { /// /// @moreinfo /// -/// ## Two sources +/// ## Three sources /// /// Decoded once here however many effects read it, and effects hold no pointer back to this module; the user adds it under `Services`, so it is not auto-wired. /// `test pattern` is a DIAGNOSTIC rather than decoration: its colored border bands make a border-mapped effect's orientation self-evident. -/// `file` reads a binary PPM, chosen over JPEG because there is no software JPEG decoder here. -/// Both render into `buf_`, which this module owns, so a published frame lives exactly as long as the service does. +/// `file` reads a binary PPM, chosen over JPEG because there is no software JPEG decoder here, and `usb` captures from a grabber through the P4's JPEG hardware. +/// +/// ## Who owns the pixels +/// +/// The software sources render into `buf_`, which this module owns, while `usb` BORROWS the decoder's output buffer, owned by the platform while the device is open. +/// So the device is only ever closed through closeCapture(), which drops the frame first. class VideoService : public MoonModule { public: /// A service: it produces for others and draws nothing itself. @@ -34,15 +39,39 @@ class VideoService : public MoonModule { static constexpr uint8_t kSourcePattern = 0; /// `source` value for a binary PPM on the filesystem. static constexpr uint8_t kSourceFile = 1; + /// `source` value for an HDMI grabber, offered only where platform::hasUsbVideo. + static constexpr uint8_t kSourceUsb = 2; /// Dropdown labels for `source`, indexed by the constants above. - static constexpr const char* kSourceOptions[] = {"test pattern", "file"}; + static constexpr const char* kSourceOptions[] = {"test pattern", "file", "usb"}; + // A target with no High-Speed USB host and no JPEG decoder cannot capture, so it is not offered the option, and the two software sources still work everywhere. /// How many of `kSourceOptions` this build offers. - static constexpr uint8_t kSourceCount = 2; + static constexpr uint8_t kSourceCount = platform::hasUsbVideo ? 3 : 2; - /// Which of `kSourceOptions` fills the frame: @xref{two-sources|what each one is for}. + /// Which of `kSourceOptions` fills the frame: @xref{three-sources|what each one is for}. uint8_t source = kSourcePattern; /// Path the `file` source reads. char file[64] = "/frame.ppm"; + /// Index into the device's advertised format list, and the only USB setting persisted. + uint8_t usbFormat = 0; + /// Gap tolerated before the lights go dark, in milliseconds. + uint16_t staleMs = 2000; + + // MJPEG carries no HDR metadata, so the source's transfer curve is declared here rather than detected. + /// `hdr` value for a display-encoded source, which is sRGB. + static constexpr uint8_t kHdrOff = 0; + /// `hdr` value for HDR10, the SMPTE ST 2084 curve. + static constexpr uint8_t kHdrPq = 1; + /// `hdr` value for HLG, the ARIB STD-B67 curve. + static constexpr uint8_t kHdrHlg = 2; + /// Dropdown labels for `hdr`, indexed by the constants above. + static constexpr const char* kHdrOptions[] = {"off", "HDR10 (PQ)", "HLG"}; + /// How many of `kHdrOptions` are offered. + static constexpr uint8_t kHdrCount = 3; + /// Which transfer curve the capture source carries. + uint8_t hdr = kHdrOff; + // Too low and bright channels clamp, dragging saturated hues toward their neighbors, and too high and the picture reads dim. + /// Reference white for the PQ curve, in nits, since PQ is absolute luminance. + uint16_t hdrNits = 2000; // Zero parks the block, which makes the pattern a still reference for checking a border light against a known color. Seventeen is the rate it used to be fixed at: a sweep every ~4 s. /// Sweep rate of the test pattern's white block, in pixels per second. uint8_t patternSpeed = 17; @@ -80,29 +109,59 @@ class VideoService : public MoonModule { controls_.setHidden(controls_.count() - 1, source != kSourceFile); controls_.addButton("reload"); controls_.setHidden(controls_.count() - 1, source != kSourceFile); + // The device decides what is on offer, so there is nothing to type. Until one has enumerated the control still renders (read-only, holding a placeholder) rather than appearing out of nowhere once a cable is plugged in. + static constexpr const char* kNoDevice[] = {"no device"}; + const bool known = formatCount_ > 0; + controls_.addSelect("offered", usbFormat, known ? formatOptions_ : kNoDevice, + known ? formatCount_ : 1); + controls_.setHidden(controls_.count() - 1, source != kSourceUsb); + controls_.setReadOnly(controls_.count() - 1, !known); + // How long a gap in frames is tolerated before the lights go dark. Floored above a frame interval, not 0: the render loop outruns the capture, so ordinary gaps between frames would otherwise read as loss and strobe the room. + controls_.addControl("staleMs", staleMs, 100, 10000); + controls_.setHidden(controls_.count() - 1, source != kSourceUsb); + // Live: changes how the buffer is read, not its size. Capture-only; other sources are authored display-encoded. + controls_.addSelect("hdr", hdr, kHdrOptions, kHdrCount); + controls_.setHidden(controls_.count() - 1, source != kSourceUsb); + controls_.addControl("hdrNits", hdrNits, 100, 10000); + controls_.setHidden(controls_.count() - 1, source != kSourceUsb || hdr != kHdrPq); MoonModule::defineControls(); } /// A source switch changes what the buffer must hold, so it re-runs the whole build. The reload button re-reads the same file in place: cheap, and it must NOT tear down the pipeline. bool affectsPrepare(const char* name) const override { - return std::strcmp(name, "source") == 0 || std::strcmp(name, "file") == 0; + return std::strcmp(name, "source") == 0 || std::strcmp(name, "file") == 0 || + std::strcmp(name, "offered") == 0; } - /// Routes the one live edit, the reload button; every other control re-runs prepare(). + /// Routes the live edits: the reload button, a format pick, and anything that moves the curve. void onControlChanged(const char* name) override { if (std::strcmp(name, "reload") == 0) loadFile(); + if (std::strcmp(name, "offered") == 0) applyFormat(); + if (std::strcmp(name, "hdr") == 0 || std::strcmp(name, "hdrNits") == 0 || std::strcmp(name, "source") == 0) + rebuildTone(); MoonModule::onControlChanged(name); } /// Cold path: size the frame buffer for the selected source and fill it once, so a frame exists before the first tick rather than one tick later. void prepare() override { seat_.claim(); // re-take after a disable/enable cycle: release() vacated it - if (source >= kSourceCount) source = kSourcePattern; // a config naming a source this build lacks - rebuildTone(); // both sources are display-encoded, so the curve is built once and never moves - if (source == kSourceFile) { - loadFile(); - } else if (allocate(kPatternW, kPatternH)) { - renderPattern(); + if (source >= kSourceCount) source = kSourcePattern; // a config restored from a capture-capable board + rebuildTone(); // every source has a curve, and a restored `hdr` lands as a VALUE, not an edit + if (source == kSourceUsb) { + // Resolve the selected row FIRST: usbWidth/Height are what captureCurrent() compares against, and a restored usbFormat arriving later would never reach them. + applyFormat(); + // Every tree-wide rebuild lands here too, and the device stays open through those, since a reopen drops the published frame and blocks on negotiation. + if (!captureCurrent()) { + closeCapture(); + openCapture(); + } + } else { + closeCapture(); // a source switch releases the device, and the frame borrowed from it + if (source == kSourceFile) { + loadFile(); + } else if (allocate(kPatternW, kPatternH)) { + renderPattern(); + } } } @@ -114,14 +173,41 @@ class VideoService : public MoonModule { renderPattern(); else if (source == kSourceFile && buf_.data()) publish(); + else if (source == kSourceUsb) + readCapture(); MoonModule::tick(); } + // One atomic load a second, and the one path by which a device that came back is picked up again. + /// Notices a grabber that enumerated on its own and asks for a rebuild, since prepare() runs on the render thread, the only one that may open or close the device. + void tick1s() MM_NONBLOCKING override { + if (source == kSourceUsb && (platform::videoCaptureFormatGeneration() != formatGen_ || selectionStale())) + if (Scheduler* s = Scheduler::instance()) s->requestPrepareTree(); + // A dropped frame is invisible and just stutters, so name it as faulty/noSlot/busy, where the last two are the newest-wins policy at work rather than a fault. + if (source == kSourceUsb && capture_.impl) { + const platform::VideoCaptureStats st = platform::videoCaptureStats(); + const uint32_t bad = st.infoFail + st.oversize + st.decodeFail; + if (bad || st.noSlot || st.busy) { + std::snprintf(status_, sizeof(status_), "%ux%u drop %u/%u/%u", shownW_, shownH_, + static_cast(bad), static_cast(st.noSlot), + static_cast(st.busy)); + setStatus(status_, bad ? Severity::Warning : Severity::Status); + } + } + MoonModule::tick1s(); + } + // Watching the format generation alone never catches this, because no device came or went. Four int compares. + /// Whether the selected row disagrees with what is open, which is how a `usbFormat` restored after prepare() reaches the device at all. + bool selectionStale() const MM_NONBLOCKING { + if (!capture_.impl || usbFormat >= formatCount_) return false; + const platform::VideoCaptureFormat& f = formats_[usbFormat]; + return f.width != opened_.width || f.height != opened_.height || f.fps != opened_.fps; + } - /// Drops the published frame and the seat, so a disabled module holds neither. + /// Gives up the device and the seat, so a disabled module holds neither. void release() override { - frame_ = VideoFrame{}; + closeCapture(); // drops the published frame too, whichever source it came from seat_.vacate(); MoonModule::release(); } @@ -130,8 +216,112 @@ class VideoService : public MoonModule { // Claimed at CONSTRUCTION, since an effect resolves latestFrame() during its own build, then re-claimed in prepare() after a disable/enable and in tick() so a survivor inherits it. ActiveInstance seat_{*this}; + // --- USB capture source ------------------------------------------------------------------- + // Only the last attempt reports, or a failed probe would leave an error standing over the retry that fixed it. + /// Open the device at the selected format, where the first open doubles as a probe: a device lists its formats only once it enumerates, which happens inside init. + void openCapture() { + bool open = platform::videoCaptureInit(capture_, usbWidth, usbHeight, usbFps); + readFormats(); + if (applyFormat()) { + closeCapture(); + open = platform::videoCaptureInit(capture_, usbWidth, usbHeight, usbFps); + } + if (!open) { + fail("no capture device"); + return; + } + opened_ = {usbWidth, usbHeight, usbFps}; + // What was ASKED for, until a frame arrives: the device negotiates, and readCapture() replaces this with the dimensions actually being decoded. + std::snprintf(status_, sizeof(status_), "asked %ux%u", usbWidth, usbHeight); + setStatus(status_, Severity::Status); + shownW_ = shownH_ = 0; + } + + /// Whether the open device already serves the selection: the same format list (a replug, even of the same grabber, publishes a new generation) and the same requested format. A (re)open is worth its cost only when one of those moved. + bool captureCurrent() const { + return capture_.impl && platform::videoCaptureFormatGeneration() == formatGen_ && + opened_.width == usbWidth && opened_.height == usbHeight && opened_.fps == usbFps; + } + + // This is the one place that order is decided, and every teardown path goes through here. + /// Release the device. The published frame borrows one of ITS buffers (platform.h, videoCaptureFrame), so it is dropped first. + void closeCapture() { + frame_ = VideoFrame{}; + opened_ = {}; + platform::videoCaptureDeinit(capture_); + } + + /// Resolve the selected row into the request fields. True when that changed something: the index survives a reboot but the list behind it does not, so this is how a restored pick reaches the device. + bool applyFormat() { + if (usbFormat >= formatCount_) return false; + const platform::VideoCaptureFormat& f = formats_[usbFormat]; + const bool changed = f.width != usbWidth || f.height != usbHeight || f.fps != usbFps; + usbWidth = f.width; + usbHeight = f.height; + usbFps = f.fps; + return changed; + } + + /// Cold path: cache what the device advertises as dropdown labels. Kept out of defineControls(), which must stay pure. it only reads what this leaves behind. + void readFormats() { + const uint32_t gen = platform::videoCaptureFormatGeneration(); + const bool changed = gen != formatGen_; + formatGen_ = gen; + formatCount_ = static_cast(platform::videoCaptureFormats(formats_, kMaxFormats)); + for (uint8_t i = 0; i < formatCount_; i++) { + std::snprintf(formatLabels_[i], sizeof(formatLabels_[i]), "%ux%u %ufps", formats_[i].width, + formats_[i].height, formats_[i].fps); + formatOptions_[i] = formatLabels_[i]; + } + // Only once there IS a list: an empty one means the device has not enumerated yet, not that the pick is invalid. Clamping against 0 threw away a restored index every boot. + if (formatCount_ && usbFormat >= formatCount_) usbFormat = 0; + // On the GENERATION rather than the count, since a replacement device advertising the same number of different formats overwrites the labels in place. + if (changed) rebuildControls(); + } + + // The borrow is safe for as long as the device stays open, which closeCapture() alone ends, and it drops the frame first. + /// Publish the newest decoded frame, which unlike the other sources does not fill buf_: the JPEG decoder owns its DMA output buffer and the frame borrows that. + void readCapture() MM_NONBLOCKING { + uint16_t w = 0, h = 0; + const uint8_t* rgb = platform::videoCaptureFrame(capture_, w, h); + if (rgb) { + lastFrameMs_ = platform::millis(); + frame_.rgb = rgb; + frame_.width = w; + frame_.height = h; + publish(); + // What the device actually negotiated, once per change (so, in practice, once): compared against what was last SHOWN rather than the frame, which a stale drop resets. + if (w != shownW_ || h != shownH_) { + shownW_ = w; + shownH_ = h; + std::snprintf(status_, sizeof(status_), "%ux%u", w, h); + setStatus(status_, Severity::Status); + } + return; + } + // A gap of one tick is normal, since the decoder runs at its own rate. A long one means the source stopped, and holding the picture would light the room from a frozen frame. + if (frame_.rgb && platform::millis() - lastFrameMs_ > staleMs) frame_ = VideoFrame{}; + } + + /// The open device, whose buffers the published frame borrows: @xref{who-owns-the-pixels|the borrow's lifetime}. + platform::VideoCaptureHandle capture_; + platform::VideoCaptureFormat opened_ = {}; // the request the open device was made with + + // Derived from the selected row and never typed, and the opening bid is 640x480 because almost every UVC device offers it. See @moreinfo, "The opening bid". + uint16_t usbWidth = 640; + uint16_t usbHeight = 480; + uint8_t usbFps = 60; + + uint32_t lastFrameMs_ = 0; + uint16_t shownW_ = 0, shownH_ = 0; // the dimensions the status last reported static constexpr int kMaxHeaderBytes = 256; // room for a comment, and its own error if not + static constexpr uint8_t kMaxFormats = platform::kVideoCaptureMaxFormats; + platform::VideoCaptureFormat formats_[kMaxFormats] = {}; + char formatLabels_[kMaxFormats][24] = {}; + const char* formatOptions_[kMaxFormats] = {}; + uint8_t formatCount_ = 0; + uint32_t formatGen_ = 0; // the platform generation formats_ was read at ScratchBuffer buf_{*this}; // width*height*3, accounted in dynamicBytes() VideoFrame frame_; uint32_t seq_ = 0; @@ -171,8 +361,27 @@ class VideoService : public MoonModule { void rebuildTone() { for (int i = 0; i < 256; i++) { const float e = static_cast(i) / 255.0f; - // sRGB EOTF (IEC 61966-2-1), which is what both a PPM and the pattern carry. - float lin = e <= 0.04045f ? e / 12.92f : std::pow((e + 0.055f) / 1.055f, 2.4f); + float lin; + if (source == kSourceUsb && hdr == kHdrHlg) { + // ARIB STD-B67 inverse OETF. Relative, so hdrNits does not apply. No OOTF: a second-order tilt that border averages do not need. + constexpr float a = 0.17883277f, b = 0.28466892f, c = 0.55991073f; + lin = e <= 0.5f ? (e * e) / 3.0f : (std::exp((e - c) / a) + b) / 12.0f; + } else if (source == kSourceUsb && hdr == kHdrPq) { + // SMPTE ST 2084 (PQ) EOTF: absolute nits, referred to hdrNits. + constexpr float m1 = 2610.0f / 16384.0f; + constexpr float m2 = 2523.0f / 4096.0f * 128.0f; + constexpr float c1 = 3424.0f / 4096.0f; + constexpr float c2 = 2413.0f / 4096.0f * 32.0f; + constexpr float c3 = 2392.0f / 4096.0f * 32.0f; + const float p = std::pow(e, 1.0f / m2); + const float num = p > c1 ? p - c1 : 0.0f; + const float den = c2 - c3 * p; // > 0 across the whole domain + const float nits = 10000.0f * std::pow(num / den, 1.0f / m1); + lin = nits / static_cast(hdrNits ? hdrNits : 1); + } else { + // sRGB EOTF (IEC 61966-2-1). The default, and what a file or the pattern carries. + lin = e <= 0.04045f ? e / 12.92f : std::pow((e + 0.055f) / 1.055f, 2.4f); + } if (lin < 0.0f) lin = 0.0f; if (lin > 1.0f) lin = 1.0f; tone_[i] = static_cast(lin * VideoFrame::kLinearMax + 0.5f); diff --git a/src/platform/desktop/platform_config.h b/src/platform/desktop/platform_config.h index 33d0f69d4..78a72e81c 100644 --- a/src/platform/desktop/platform_config.h +++ b/src/platform/desktop/platform_config.h @@ -61,6 +61,9 @@ constexpr bool hasI2sMic = false; /// OS capture devices, reaching the same audioMicRead seam a wired microphone would. constexpr bool hasAudioCapture = true; +/// No USB host on a desktop, so VideoService keeps its test-pattern and file sources. +constexpr bool hasUsbVideo = false; + /// True, so the host compiles the WiFi path even though it ships stubs behind it. constexpr bool hasWiFi = true; diff --git a/src/platform/desktop/platform_desktop.cpp b/src/platform/desktop/platform_desktop.cpp index 04fa47142..82d359b96 100644 --- a/src/platform/desktop/platform_desktop.cpp +++ b/src/platform/desktop/platform_desktop.cpp @@ -2508,6 +2508,20 @@ void audioFft(const float* windowed, size_t n, float* outMag) { for (size_t k = 0; k < n / 2; k++) outMag[k] = std::sqrt(re[k] * re[k] + im[k] * im[k]); } +// USB video capture: no USB host on desktop, so init fails and VideoService's usb source reports "no capture device" while its other sources keep working. +bool videoCaptureInit(VideoCaptureHandle& /*h*/, uint16_t /*width*/, uint16_t /*height*/, + uint8_t /*fps*/) { + return false; +} +size_t videoCaptureFormats(VideoCaptureFormat* /*out*/, size_t /*max*/) { return 0; } +uint32_t videoCaptureFormatGeneration() { return 0; } +const uint8_t* videoCaptureFrame(VideoCaptureHandle& /*h*/, uint16_t& /*width*/, + uint16_t& /*height*/) MM_NONBLOCKING { + return nullptr; +} +void videoCaptureDeinit(VideoCaptureHandle& /*h*/) {} +VideoCaptureStats videoCaptureStats() { return {}; } + // No wires here, so the bus is state alone: open, closed and its generation behave as on a board, and a scan finds what a test injected. namespace { bool testI2cOpen_ = false; diff --git a/src/platform/esp32/.clangd b/src/platform/esp32/.clangd new file mode 100644 index 000000000..6678b2b72 --- /dev/null +++ b/src/platform/esp32/.clangd @@ -0,0 +1,15 @@ +# ESP32 platform sources are compiled by the ESP-IDF build, not the desktop one, so the root +# .clangd's build/macos database has no entry for them: clangd guesses flags and reports +# `'sdkconfig.h' file not found` on every file here. Point this directory at a firmware build +# instead, and the real flags come with it: CONFIG_IDF_TARGET_*, the managed-component include +# paths, and the generated config/sdkconfig.h that defines the target macro the #if reads. +# +# Needs that firmware built at least once (`uv run moondeck/build/build_esp32.py --firmware +# esp32p4rev1-eth-wifi`); the database is a build product. Pointing at ONE variant means files here +# are shown as that chip sees them, so a file with per-target branches has the others grayed +# out, so change the path when working on a different one. +# +# Scoped to this directory on purpose: the P4 database carries no entry for test/ or +# platform/desktop, so making it the root would cost IntelliSense on all 142 test files. +CompileFlags: + CompilationDatabase: ../../../build/esp32-esp32p4rev1-eth-wifi diff --git a/src/platform/esp32/platform_config.h b/src/platform/esp32/platform_config.h index 6a5318b80..c1c33f9a3 100644 --- a/src/platform/esp32/platform_config.h +++ b/src/platform/esp32/platform_config.h @@ -146,6 +146,15 @@ constexpr bool hasI2sMic = false; /// False: capture devices are a host concept, and a board uses the wired microphone above. constexpr bool hasAudioCapture = false; +// USB video needs a High-Speed USB PHY, which the S3 lacks, and a hardware JPEG decoder. Only the P4 compiles an implementation, so widen all three together or none. +#if defined(CONFIG_IDF_TARGET_ESP32P4) && defined(CONFIG_SOC_USB_UTMI_PHY_NUM) && \ + defined(CONFIG_SOC_JPEG_DECODE_SUPPORTED) +/// Whether this target can capture video off a UVC device. +constexpr bool hasUsbVideo = true; +#else +constexpr bool hasUsbVideo = false; +#endif + // The Ethernet-only profile drops the WiFi components and defines this, there being no SDK switch. #ifdef MM_NO_WIFI diff --git a/src/platform/esp32/platform_esp32_usbvideo.cpp b/src/platform/esp32/platform_esp32_usbvideo.cpp new file mode 100644 index 000000000..f11c8701f --- /dev/null +++ b/src/platform/esp32/platform_esp32_usbvideo.cpp @@ -0,0 +1,548 @@ +/// USB video capture: the peripheral half of VideoService, owning the UVC stream and the JPEG decode. +/// +/// @moreinfo +/// +/// An HDMI grabber presents itself as a UVC webcam. See src/core/services/VideoService.h for the module that drives this. +/// MJPEG, because uncompressed does not fit: 640x480 YUY2 at 60 fps is 37 MB/s against a USB 2.0 host's ~24.6 MB/s. +/// +/// ## Three threads +/// +/// The USB host library is a per-application singleton, installed on first use and left running, with its event loop on a task of its own. +/// uvc_host_stream_open() blocks waiting for an enumeration that loop drives, so it cannot share a thread with init. +/// Decoding runs on a task of its own too, since jpeg_decoder_process() blocks and the render tick is MM_NONBLOCKING. +/// So videoCaptureFrame only reads an index, and the frame it names was decoded earlier by decoderTask. +/// A frame arriving while one is still pending is dropped, the newest being the only one worth having. +/// +/// ## Who owns the buffers +/// +/// The buffers are allocated in videoCaptureInit and freed in videoCaptureDeinit, both on the caller's thread. +/// Nothing in between touches the set, so no task can reallocate while another reads. +/// A device that goes away is not chased from here: its return re-enumerates, bumping the format generation, and the caller re-inits on that. +/// +/// ## Bulk payload headers +/// +/// The header stride is not exposed by the driver, so stripPayloadHeaders reads it off the first header, which can only sit 12 bytes before a packet end. +/// A candidate is confirmed by the header that must follow it one stride on, since entropy-coded bytes pass the field checks about once per 2^18 tries. + +#include "platform/platform.h" + +#include "sdkconfig.h" + +#if defined(CONFIG_IDF_TARGET_ESP32P4) + +#include "driver/jpeg_decode.h" +#include "esp_heap_caps.h" +#include "esp_log.h" +#include "usb/usb_host.h" +#include "usb/uvc_host.h" + +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +#include "freertos/task.h" + +#include +#include +#include + +namespace mm::platform { +namespace { + +constexpr char kTag[] = "usbvideo"; + +constexpr int kSlots = 3; +static_assert(kSlots >= 3, "freeSlot() needs a spare beyond the published and the in-use slot"); + +struct Capture { + uvc_host_stream_hdl_t stream = nullptr; + jpeg_decoder_handle_t jpeg = nullptr; + + // The frame the UVC callback handed over, or null. Exchanged rather than assigned so the callback never blocks and never overwrites one the decoder is already reading. + std::atomic pending{nullptr}; + + TaskHandle_t decoder = nullptr; + SemaphoreHandle_t wake = nullptr; // callback -> decoder + SemaphoreHandle_t stopped = nullptr; // decoder -> deinit + std::atomic running{false}; + + // Decoded RGB888, from jpeg_alloc_decoder_mem for its cache-line and 2D-DMA alignment: a plain malloc shows up as intermittent corruption, not an error. Written once, in init, before the decoder task exists; read-only from then on. + uint8_t* rgb[kSlots] = {}; + size_t rgbCap = 0; + uint16_t width[kSlots] = {}; + uint16_t height[kSlots] = {}; + // ONE word, because reading which slot is newest and claiming it must be a single step, or the decoder could decode into a buffer being displayed. Packed (published+1) << 8 | (inUse+1), where 0 in a field means none. + std::atomic slots{0}; + + static int pubOf(uint16_t s) { return static_cast(s >> 8) - 1; } + static int useOf(uint16_t s) { return static_cast(s & 0xFF) - 1; } + static uint16_t pack(int pub, int use) { + return static_cast(((pub + 1) << 8) | ((use + 1) & 0xFF)); + } +}; + +// What the attached device advertises. File scope rather than inside Capture because it is learned from the driver event, which fires before a stream exists and outlives a failed open. +constexpr size_t kMaxFormats = kVideoCaptureMaxFormats; +uvc_host_frame_info_t frameList[kMaxFormats]; // file scope: too big for the driver task's stack + +// A seqlock whose generation is odd during a write, so a reader that sees it move across the copy retries. The payload is atomic rather than plain bytes, since two threads touching a non-atomic object concurrently is a data race. +struct FormatBank { + std::atomic width[kMaxFormats]; + std::atomic height[kMaxFormats]; + std::atomic fps[kMaxFormats]; + std::atomic interval[kMaxFormats]; // as published: `fps` is rounded for the UI + std::atomic count{0}; +}; +FormatBank formatBank; +std::atomic formatGen{0}; // 0 = nothing published yet; odd = a write in progress +// The device the bank describes, so the open reaches THAT one and not whichever the driver finds first when two are attached. 0 (UVC_HOST_ANY_DEV_ADDR) until a device has enumerated. +std::atomic formatDevAddr{UVC_HOST_ANY_DEV_ADDR}; +// Only the first streaming function of a device is ever opened, so only its list is published: a device exposing several would otherwise describe one function while another gets opened. +constexpr uint8_t kStreamIndex = 0; + +bool hostReady = false; +bool uvcReady = false; + +// usb_host_lib_handle_events() is where enumeration and the port state machine actually run, and it blocks. Nothing else may drive it, so this task owns it for the life of the application. +void pumpTask(void*) { + while (true) { + uint32_t flags = 0; + usb_host_lib_handle_events(portMAX_DELAY, &flags); + if (flags & USB_HOST_LIB_EVENT_FLAGS_NO_CLIENTS) usb_host_device_free_all(); + } +} + +// Installed once and never uninstalled: the library is a singleton the whole application shares, and tearing it down on a source switch only risks leaving it un-reinstallable. +bool ensureUsbHost() { + if (hostReady) return true; + usb_host_config_t hostCfg = {}; + hostCfg.intr_flags = ESP_INTR_FLAG_LOWMED; + if (usb_host_install(&hostCfg) != ESP_OK) { + ESP_LOGE(kTag, "usb_host_install failed"); + return false; + } + // Priority 4 is below the UVC driver task, which consumes what this one produces. Unpinned, since the render loop is fixed to core 0, so leaving placement to the scheduler keeps USB off it. + if (xTaskCreatePinnedToCore(pumpTask, "usbpump", 4 * 1024, nullptr, 4, nullptr, tskNO_AFFINITY) != + pdPASS) { + ESP_LOGE(kTag, "no USB event task"); + usb_host_uninstall(); + return false; + } + hostReady = true; + return true; +} + +// Counted on the UVC and decode tasks, read from the render tick: relaxed is enough for a diagnostic. +std::atomic statDecoded{0}, statBusy{0}, statInfoFail{0}, statOversize{0}, statNoSlot{0}, + statDecodeFail{0}; + +// Runs on the UVC driver task, so the ordinary FreeRTOS API is safe. It still only hands the frame over, since decoding here would stall the task that collects isochronous packets. +bool onFrame(const uvc_host_frame_t* frame, void* ctx) { + auto* cap = static_cast(ctx); + uvc_host_frame_t* expected = nullptr; + // Take the slot only if it is free. Returning false keeps the frame, so the loser of this race must return true to hand it straight back or the driver runs out of buffers. + if (!cap->pending.compare_exchange_strong(expected, const_cast(frame))) { + statBusy.fetch_add(1, std::memory_order_relaxed); + return true; + } + xSemaphoreGive(cap->wake); + return false; +} + +// UVC states a rate as dwFrameInterval, a period in 100 ns ticks, so one second is 10 million of them. Rounded rather than truncated: 59.94 fps is a real rate and reads better as 60 than 59. +uint8_t fpsFrom(uint32_t interval) { + constexpr uint32_t kTicksPerSecond = 10000000; + return static_cast((kTicksPerSecond + interval / 2) / interval); +} + +// One dropdown row per (resolution, rate) pair. A device that does 320x240 at both 30 and 60 lists the resolution ONCE with several intervals, so without this expansion only its default is reachable from the UI. +void addAdvertised(const uvc_host_frame_info_t& info, uint32_t interval, size_t& n) { + if (n >= kMaxFormats || interval == 0) return; + const uint16_t w = static_cast(info.h_res), h = static_cast(info.v_res); + const uint8_t fps = fpsFrom(interval); + formatBank.width[n].store(w, std::memory_order_relaxed); + formatBank.height[n].store(h, std::memory_order_relaxed); + formatBank.fps[n].store(fps, std::memory_order_relaxed); + formatBank.interval[n].store(interval, std::memory_order_relaxed); + n++; + ESP_LOGI(kTag, "offers MJPEG %ux%u @ %u fps", w, h, fps); +} + +// Runs on the UVC driver task when a device enumerates, before any stream is opened. That is what makes the list available even when the open then fails on an unsupported resolution. +void onDriverEvent(const uvc_host_driver_event_data_t* event, void*) { + if (event->type != UVC_HOST_DRIVER_EVENT_DEVICE_CONNECTED) return; + if (event->device_connected.uvc_stream_index != kStreamIndex) return; // never opened, so not listed + + const uint8_t devAddr = event->device_connected.dev_addr; + size_t count = kMaxFormats; + if (uvc_host_get_frame_list(devAddr, kStreamIndex, reinterpret_cast(frameList), + &count) != ESP_OK) { + ESP_LOGW(kTag, "device connected but its frame list could not be read"); + return; + } + if (count > kMaxFormats) count = kMaxFormats; // it reports what it NEEDS, not what it wrote + + // Bracket the rewrite in an odd generation, so a reader can tell it overlapped one. + formatGen.fetch_add(1, std::memory_order_release); + formatDevAddr.store(devAddr, std::memory_order_relaxed); + std::atomic_thread_fence(std::memory_order_release); + size_t n = 0; + for (size_t i = 0; i < count; i++) { + const uvc_host_frame_info_t& info = frameList[i]; + if (info.format != UVC_VS_FORMAT_MJPEG) continue; // nothing else is decodable here + if (info.interval_type == 0) { // a continuous range: offer both ends, fastest first + addAdvertised(info, info.interval_min, n); + if (info.interval_max != info.interval_min) addAdvertised(info, info.interval_max, n); + continue; + } + const uint8_t rates = info.interval_type < CONFIG_UVC_INTERVAL_ARRAY_SIZE + ? info.interval_type + : CONFIG_UVC_INTERVAL_ARRAY_SIZE; + for (uint8_t j = 0; j < rates; j++) addAdvertised(info, info.interval[j], n); + } + formatBank.count.store(n, std::memory_order_relaxed); + formatGen.fetch_add(1, std::memory_order_release); // even again: the list is settled +} + +// Runs on the UVC driver task, which pauses the stream before this fires, so the renderer sees a gap and its stale timeout takes it from there. Nothing to unwind: the caller re-inits on the next generation. +void onEvent(const uvc_host_stream_event_data_t* event, void*) { + if (event->type == UVC_HOST_DEVICE_DISCONNECTED) ESP_LOGW(kTag, "capture device disconnected"); +} + +// RGB888 bytes the decoder writes for a w x h JPEG: both axes padded to the 16-pixel MCU (jpeg_decoder_process, note 2). Sized w*h*3, 800x600 overruns by 19200 bytes and every frame fails. +size_t decodedBytes(uint16_t w, uint16_t h) { + const size_t aw = (static_cast(w) + 15) & ~static_cast(15); + const size_t ah = (static_cast(h) + 15) & ~static_cast(15); + return aw * ah * 3; +} + +// One set of slots per open, sized from the format the device agreed to. Called from init only, before the decoder task exists, so nothing can be reading a slot while it is (re)written. +bool allocSlots(Capture& cap, uint16_t w, uint16_t h) { + const size_t need = decodedBytes(w, h); + jpeg_decode_memory_alloc_cfg_t memCfg = {}; + memCfg.buffer_direction = JPEG_DEC_ALLOC_OUTPUT_BUFFER; + for (int i = 0; i < kSlots; i++) { + size_t got = 0; + cap.rgb[i] = static_cast(jpeg_alloc_decoder_mem(need, &memCfg, &got)); + if (!cap.rgb[i]) { + cap.rgbCap = 0; // a partial set is no set: decode() must not write into a missing slot + return false; // deinit frees what was allocated + } + cap.rgbCap = got; // every slot gets the same request, so the same rounded-up size + } + return true; +} + +// -1 when every slot is spoken for: unreachable while kSlots is 3, but returning a real index +// anyway would hand the decoder a buffer the render thread is reading. Corruption with no error is worse than a dropped frame. +int freeSlot(const Capture& cap) { + const uint16_t s = cap.slots.load(std::memory_order_acquire); + const int pub = Capture::pubOf(s), use = Capture::useOf(s); + for (int i = 0; i < kSlots; i++) + if (i != pub && i != use) return i; + return -1; +} + +// UVC payload header (UVC 1.5 2.4.3.3): bLength 12 when PTS and SCR are present, EOH set, SCR's top 5 reserved bits zero. `pts` pins later headers to the frame's first one: PTS is constant across one frame's payloads. +constexpr size_t kPayloadHeaderLen = 12; +constexpr size_t kBulkMps = 512; // high-speed bulk + +bool isPayloadHeader(const uint8_t* h, size_t avail, const uint8_t* pts) { + constexpr uint8_t kEoh = 0x80, kErr = 0x40, kScr = 0x08, kPts = 0x04, kEof = 0x02; + return avail >= kPayloadHeaderLen && h[0] == kPayloadHeaderLen && + (h[1] & (kEoh | kErr | kScr | kPts | kEof)) == (kEoh | kScr | kPts) && !(h[11] & 0xF8) && + (!pts || memcmp(h + 2, pts, 4) == 0); +} + +// Drops the payload headers usb_host_uvc leaves inside a bulk frame. uvc_bulk.c strips one only after a short transfer. Where the payload is a multiple of the packet size, every header after the first stays in at stride `payload`. See @moreinfo. +size_t stripPayloadHeaders(uint8_t* d, size_t len) { + size_t hdr = 0; + for (size_t i = kBulkMps - kPayloadHeaderLen; !hdr && i + kPayloadHeaderLen < len; i += kBulkMps) { + if (!isPayloadHeader(d + i, len - i, nullptr)) continue; + const size_t next = 2 * i + kPayloadHeaderLen; + if (next + kPayloadHeaderLen > len || isPayloadHeader(d + next, len - next, d + i + 2)) hdr = i; + } + if (!hdr) return len; + const size_t payload = hdr + kPayloadHeaderLen; + uint8_t pts[4]; // copied: the compaction overwrites the header it came from + memcpy(pts, d + hdr + 2, sizeof pts); + size_t w = hdr, r = hdr; + while (isPayloadHeader(d + r, len - r, pts)) { + r += kPayloadHeaderLen; + const size_t run = std::min(payload - kPayloadHeaderLen, len - r); + memmove(d + w, d + r, run); + w += run; + r += run; + } + memmove(d + w, d + r, len - r); // whatever follows a missing header is left as delivered + return w + (len - r); +} + +// Warn on the FIRST of each kind only. A drop repeats at frame rate, so logging every one buries the log and costs more than the fault; the counters carry the rate. +void warnOnce(bool& said, const char* what) { + if (said) return; + said = true; + ESP_LOGW(kTag, "%s - first occurrence; see the Video status for the running count", what); +} + +void decode(Capture& cap, uvc_host_frame_t* frame) { + static bool saidInfo = false, saidOversize = false, saidNoSlot = false, saidDecode = false; + const size_t len = stripPayloadHeaders(frame->data, frame->data_len); + // Dimensions from the bitstream, not from the request: a device may negotiate something else. + jpeg_decode_picture_info_t info = {}; + if (jpeg_decoder_get_info(frame->data, len, &info) != ESP_OK) { + statInfoFail.fetch_add(1, std::memory_order_relaxed); + warnOnce(saidInfo, "frame is not readable JPEG (payload stride mis-detected?)"); + return; + } + if (decodedBytes(static_cast(info.width), static_cast(info.height)) > cap.rgbCap) { + statOversize.fetch_add(1, std::memory_order_relaxed); + warnOnce(saidOversize, "frame exceeds the buffers sized at open"); + ESP_LOGW(kTag, "frame %ux%u exceeds the buffers sized at open", info.width, info.height); + return; + } + + const int slot = freeSlot(cap); + if (slot < 0) { // drop the frame rather than write over one being read + statNoSlot.fetch_add(1, std::memory_order_relaxed); + warnOnce(saidNoSlot, "renderer holds every slot, dropping the newest frame"); + return; + } + + jpeg_decode_cfg_t decodeCfg = {}; + decodeCfg.output_format = JPEG_DECODE_OUT_FORMAT_RGB888; + decodeCfg.rgb_order = JPEG_DEC_RGB_ELEMENT_ORDER_RGB; + decodeCfg.conv_std = JPEG_YUV_RGB_CONV_STD_BT601; + uint32_t outSize = 0; + if (jpeg_decoder_process(cap.jpeg, &decodeCfg, frame->data, len, cap.rgb[slot], cap.rgbCap, &outSize) != + ESP_OK) { + statDecodeFail.fetch_add(1, std::memory_order_relaxed); + warnOnce(saidDecode, "the decoder refused a bitstream whose header it accepted"); + return; + } + statDecoded.fetch_add(1, std::memory_order_relaxed); + + cap.width[slot] = static_cast(info.width); + cap.height[slot] = static_cast(info.height); + // Preserve whatever the renderer claimed while the decode ran. Pixels and dimensions are written first, and the release makes them visible to whoever acquires this. + uint16_t cur = cap.slots.load(std::memory_order_relaxed); + while (!cap.slots.compare_exchange_weak(cur, Capture::pack(slot, Capture::useOf(cur)), + std::memory_order_release, std::memory_order_relaxed)) { + } +} + +// The blocking half, kept off the render tick. Waits on a finite timeout rather than forever so `running` is seen without the callback having to signal. +void decoderTask(void* arg) { + auto* cap = static_cast(arg); + while (cap->running.load()) { + if (xSemaphoreTake(cap->wake, pdMS_TO_TICKS(100)) != pdTRUE) continue; + if (uvc_host_frame_t* frame = cap->pending.exchange(nullptr)) { + decode(*cap, frame); + uvc_host_frame_return(cap->stream, frame); + } + } + xSemaphoreGive(cap->stopped); + vTaskDelete(nullptr); +} + +// Priority 6 puts it above the UVC driver task: a decode that runs late holds the only free frame buffer, which is what starves the driver. +bool startDecoder(Capture& cap) { + cap.running = true; + if (xTaskCreatePinnedToCore(decoderTask, "usbjpeg", 4 * 1024, &cap, 6, &cap.decoder, tskNO_AFFINITY) == + pdPASS) + return true; + cap.running = false; + ESP_LOGE(kTag, "no decode task"); + return false; +} + +// Installed once, like the host library above it: the format list belongs to the bus, not to one open. Reinstalling per open would re-enumerate the attached device and bump the format generation, which is the very signal the caller re-inits on. +bool ensureUvcHost() { + if (uvcReady) return true; + uvc_host_driver_config_t driverCfg = {}; + driverCfg.driver_task_stack_size = 4 * 1024; + driverCfg.driver_task_priority = 5; + driverCfg.xCoreID = tskNO_AFFINITY; + driverCfg.create_background_task = true; + driverCfg.event_cb = onDriverEvent; // fills the format bank as soon as a device enumerates + if (uvc_host_install(&driverCfg) != ESP_OK) { + ESP_LOGE(kTag, "uvc_host_install failed"); + return false; + } + uvcReady = true; + return true; +} + +bool createJpeg(Capture& cap) { + jpeg_decode_engine_cfg_t jpegCfg = {}; + // 200, not 40: the timeout aborts the 2D-DMA mid-frame, and writing 6.2 MB of 1080p RGB into PSRAM alone takes ~34 ms. At 40 ms only 14% of intact frames survived. + jpegCfg.timeout_ms = 200; + if (jpeg_new_decoder_engine(&jpegCfg, &cap.jpeg) == ESP_OK) return true; + ESP_LOGE(kTag, "no JPEG decoder engine"); + return false; +} + +bool createSignals(Capture& cap) { + cap.wake = xSemaphoreCreateBinary(); + cap.stopped = xSemaphoreCreateBinary(); + if (cap.wake && cap.stopped) return true; + ESP_LOGE(kTag, "no semaphores"); + return false; +} + +// The published interval of a row as the float the driver compares against; 0 (device default) when the row is unknown, so the request still opens at that resolution. +float exactFpsFor(uint16_t w, uint16_t h, uint8_t fps) { + const size_t n = formatBank.count.load(std::memory_order_acquire); + for (size_t i = 0; i < n && i < kMaxFormats; i++) { + if (formatBank.width[i].load(std::memory_order_relaxed) != w) continue; + if (formatBank.height[i].load(std::memory_order_relaxed) != h) continue; + if (formatBank.fps[i].load(std::memory_order_relaxed) != fps) continue; + const uint32_t iv = formatBank.interval[i].load(std::memory_order_relaxed); + if (iv) return 10000000.0f / static_cast(iv); + } + return 0.0f; +} + +bool openStream(Capture& cap, uint16_t width, uint16_t height, uint8_t fps) { + uvc_host_stream_config_t streamCfg = {}; + streamCfg.event_cb = onEvent; + streamCfg.frame_cb = onFrame; + streamCfg.user_ctx = ∩ + // The device whose formats are on offer, once one has enumerated. Any device before that, and its enumeration bumps the generation, so the caller comes back and opens it by address. + streamCfg.usb.dev_addr = formatDevAddr.load(std::memory_order_relaxed); + streamCfg.usb.vid = UVC_HOST_ANY_VID; + streamCfg.usb.pid = UVC_HOST_ANY_PID; + streamCfg.usb.uvc_stream_index = kStreamIndex; + streamCfg.vs_format.h_res = width; + streamCfg.vs_format.v_res = height; + // The device's own interval, not the rounded fps: the driver matches within 0.0001 fps, so a 59.94 mode never matches a requested 60. + streamCfg.vs_format.fps = exactFpsFor(width, height, fps); + streamCfg.vs_format.format = UVC_VS_FORMAT_MJPEG; + // urb_size left at 0 (4x MPS): the driver's default, and every urb is internal SRAM. number_of_urbs has no default: 0 is malloc(0), and the open fails with ESP_ERR_NO_MEM. + streamCfg.advanced.number_of_urbs = 4; + streamCfg.advanced.number_of_frame_buffers = 3; + // frame_size 0 means dwMaxVideoFrameSize, the UNCOMPRESSED size: 3 x 4.1 MB at 1080p for MJPEG frames of 40-76 KB. Half of it still leaves an order of magnitude of headroom. + streamCfg.advanced.frame_size = static_cast(width) * height / 2; + streamCfg.advanced.frame_heap_caps = MALLOC_CAP_SPIRAM; // keep the internal heap for USB and WiFi + + // Wait rather than fail: the host enumerates asynchronously, so a device plugged in at boot is usually not ready when this runs. The driver takes ticks, not milliseconds. + if (uvc_host_stream_open(&streamCfg, pdMS_TO_TICKS(3000), &cap.stream) != ESP_OK) { + ESP_LOGW(kTag, "no UVC device offering MJPEG %ux%u", width, height); + return false; + } + uvc_host_desc_print(cap.stream); // the format list, for the bench + return true; +} + +// Sized from what the device agreed to, not from what we asked for, so no frame can arrive needing more room than the slots have. +bool sizeBuffers(Capture& cap) { + uvc_host_stream_format_t got = {}; + if (uvc_host_stream_format_get(cap.stream, &got) != ESP_OK) { + ESP_LOGE(kTag, "cannot read the negotiated format"); + return false; + } + ESP_LOGI(kTag, "streaming MJPEG %ux%u @ %.1f fps", got.h_res, got.v_res, got.fps); + if (allocSlots(cap, static_cast(got.h_res), static_cast(got.v_res))) return true; + ESP_LOGE(kTag, "no memory for %d decode buffers", kSlots); + return false; +} + +} // namespace + +bool videoCaptureInit(VideoCaptureHandle& handle, uint16_t width, uint16_t height, uint8_t fps) { + if (handle.impl) return true; + if (!ensureUsbHost() || !ensureUvcHost()) return false; + auto* cap = new Capture(); + handle.impl = cap; // every failure below unwinds through videoCaptureDeinit + + // In this order on purpose. The decoder task starts LAST, once every buffer it can reach exists, and the stream after it, so the first frame finds a task to wake. + const bool ok = createJpeg(*cap) && createSignals(*cap) && openStream(*cap, width, height, fps) && + sizeBuffers(*cap) && startDecoder(*cap) && uvc_host_stream_start(cap->stream) == ESP_OK; + if (!ok) videoCaptureDeinit(handle); + return ok; +} + +// Hot path: an index load and two field reads. Everything expensive already happened on decoderTask, which is what lets the render tick stay MM_NONBLOCKING. +const uint8_t* videoCaptureFrame(VideoCaptureHandle& handle, uint16_t& width, + uint16_t& height) MM_NONBLOCKING { + auto* cap = static_cast(handle.impl); + if (!cap) return nullptr; + // Read and claim in one step. The loop runs again only if the decoder published meanwhile, and then hands back that newer frame. + uint16_t cur = cap->slots.load(std::memory_order_acquire); + int slot; + do { + slot = Capture::pubOf(cur); + // Consecutive publishes land in different slots, so an unchanged one means no new frame. + if (slot < 0 || slot == Capture::useOf(cur)) return nullptr; + } while (!cap->slots.compare_exchange_weak(cur, Capture::pack(slot, slot), + std::memory_order_acq_rel, std::memory_order_acquire)); + width = cap->width[slot]; + height = cap->height[slot]; + return cap->rgb[slot]; +} + +uint32_t videoCaptureFormatGeneration() { return formatGen.load(std::memory_order_acquire); } + +size_t videoCaptureFormats(VideoCaptureFormat* out, size_t max) { + for (;;) { + const uint32_t before = formatGen.load(std::memory_order_acquire); + if (before == 0) return 0; // nothing published yet + if (before & 1u) continue; // a write is in progress; let it finish + size_t n = formatBank.count.load(std::memory_order_relaxed); + if (n > kMaxFormats) n = kMaxFormats; // a torn read of a rewrite in flight + if (n > max) n = max; + for (size_t i = 0; i < n; i++) { + out[i].width = formatBank.width[i].load(std::memory_order_relaxed); + out[i].height = formatBank.height[i].load(std::memory_order_relaxed); + out[i].fps = formatBank.fps[i].load(std::memory_order_relaxed); + } + std::atomic_thread_fence(std::memory_order_acquire); + if (formatGen.load(std::memory_order_relaxed) == before) return n; // no write overlapped + } +} + +void videoCaptureDeinit(VideoCaptureHandle& handle) { + auto* cap = static_cast(handle.impl); + if (!cap) return; + // Stop the stream first so no frame lands mid-teardown, then the task that would decode it. The join is unbounded on purpose, and the decoder's own 40 ms timeout bounds how long it takes. + if (cap->stream) uvc_host_stream_stop(cap->stream); + if (cap->decoder) { + cap->running = false; + xSemaphoreGive(cap->wake); // it may be parked on this + xSemaphoreTake(cap->stopped, portMAX_DELAY); + } + if (uvc_host_frame_t* frame = cap->pending.exchange(nullptr)) uvc_host_frame_return(cap->stream, frame); + if (cap->stream) uvc_host_stream_close(cap->stream); + if (cap->jpeg) jpeg_del_decoder_engine(cap->jpeg); + if (cap->wake) vSemaphoreDelete(cap->wake); + if (cap->stopped) vSemaphoreDelete(cap->stopped); + for (uint8_t* buf : cap->rgb) free(buf); // free(nullptr) is a no-op, so a partial set is fine + delete cap; + handle.impl = nullptr; +} + +VideoCaptureStats videoCaptureStats() { + VideoCaptureStats s; + s.decoded = statDecoded.load(std::memory_order_relaxed); + s.busy = statBusy.load(std::memory_order_relaxed); + s.infoFail = statInfoFail.load(std::memory_order_relaxed); + s.oversize = statOversize.load(std::memory_order_relaxed); + s.noSlot = statNoSlot.load(std::memory_order_relaxed); + s.decodeFail = statDecodeFail.load(std::memory_order_relaxed); + return s; +} + +} // namespace mm::platform + +#else // every other target: no High-Speed USB host, no JPEG decoder + +namespace mm::platform { + +bool videoCaptureInit(VideoCaptureHandle&, uint16_t, uint16_t, uint8_t) { return false; } +size_t videoCaptureFormats(VideoCaptureFormat*, size_t) { return 0; } +uint32_t videoCaptureFormatGeneration() { return 0; } +const uint8_t* videoCaptureFrame(VideoCaptureHandle&, uint16_t&, uint16_t&) MM_NONBLOCKING { return nullptr; } +void videoCaptureDeinit(VideoCaptureHandle&) {} +VideoCaptureStats videoCaptureStats() { return {}; } + +} // namespace mm::platform + +#endif diff --git a/src/platform/platform.h b/src/platform/platform.h index 74ec36f9c..afb3f84a8 100644 --- a/src/platform/platform.h +++ b/src/platform/platform.h @@ -1205,6 +1205,65 @@ void audioMicDeinit(AudioMicHandle& h); /// Fill `outMag` with the magnitude bins of `n` windowed samples, `n` being a power of two. void audioFft(const float* windowed, size_t n, float* outMag); +// --- USB video capture (UVC): an HDMI grabber presenting itself as a webcam --------------------- +// MJPEG off the wire, decoded by the target's JPEG hardware, and ESP32-P4 only: every other target links a stub whose init fails, which VideoService reports as a status. + +/// Opaque per-device state, owned by the platform between init and deinit. +struct VideoCaptureHandle { void* impl = nullptr; }; + +// MJPEG only, since nothing else is decodable here, so there is no format field. +/// One row of what the attached device advertises, so the UI offers real choices rather than guesses. +struct VideoCaptureFormat { + /// Frame width in pixels. + uint16_t width = 0; + /// Frame height in pixels. + uint16_t height = 0; + /// Frames per second the device offers at that size. + uint8_t fps = 0; +}; + +// A grabber lists its modes largest first, so a cap that is too low hides exactly the cheap ones. +/// Rows kept per device. +constexpr size_t kVideoCaptureMaxFormats = 64; + +// Learned when a device enumerates, so it survives a failed videoCaptureInit, which is exactly when it is worth reading. +/// Fill `out` with up to `max` of those rows and return how many were written; 0 means no device has been seen yet. +size_t videoCaptureFormats(VideoCaptureFormat* out, size_t max); + +// A consumer caching the list compares this instead of copying, so noticing a hotplug costs one load. +/// Bumped whenever that list is rewritten, which is when a device enumerates; 0 until then. +uint32_t videoCaptureFormatGeneration(); + +// Width, height and fps are requests rather than promises. A device that goes away is not followed: a reconnect bumps the generation, and the caller answers with a deinit/init pair. +/// Claim the first UVC device on the bus and stream MJPEG, false when nothing is attached or no MJPEG format matches. +bool videoCaptureInit(VideoCaptureHandle& h, uint16_t width, uint16_t height, uint8_t fps); + +// The buffer belongs to the platform, allocated in init and freed in deinit, and the decoder never writes into the one most recently returned. See videoCaptureInit's appendix. +/// Newest decoded frame as RGB888, held until the next call that RETURNS A FRAME, or nullptr when none arrived. +const uint8_t* videoCaptureFrame(VideoCaptureHandle& h, uint16_t& width, uint16_t& height) MM_NONBLOCKING; + +/// Release the device and free its buffers. The caller drops the frame it borrowed first. +void videoCaptureDeinit(VideoCaptureHandle& h); + +// A drop in ones is normal, and a climbing count is a fault worth naming, since in the picture it is only a stutter. +/// Why frames did not reach the renderer, cumulative since boot. +struct VideoCaptureStats { + /// Frames that reached a slot. + uint32_t decoded = 0; + /// Arrived while the previous frame was still waiting to be decoded. + uint32_t busy = 0; + /// Every decode buffer still held by the renderer. + uint32_t noSlot = 0; + /// Not a readable JPEG: a mis-detected payload stride lands here. + uint32_t infoFail = 0; + /// Larger than the buffers sized at open. + uint32_t oversize = 0; + /// The decoder refused a bitstream whose header it had accepted. + uint32_t decodeFail = 0; +}; +/// The counters above, read atomically enough for a once-a-second status line. +VideoCaptureStats videoCaptureStats(); + // The board's I2C bus: one master bus, opened by its owner (I2cBusModule) and shared by every device on it, such as an audio codec. /// Open the bus on these pins, closing one open on other pins first; false when the port cannot be opened. diff --git a/test/unit/core/unit_VideoService.cpp b/test/unit/core/unit_VideoService.cpp index ceb7dc915..bdad14029 100644 --- a/test/unit/core/unit_VideoService.cpp +++ b/test/unit/core/unit_VideoService.cpp @@ -112,6 +112,24 @@ TEST_CASE("VideoService: a survivor takes over the seat when the elected source CHECK(VideoService::latestFrame()->rgb != nullptr); } +// Unit tests link the desktop platform, which cannot capture, so the usb source is not offered. The static_assert fails loudly if the suite ever runs somewhere that CAN. +TEST_CASE("VideoService: a platform that cannot capture does not offer the usb source") { + static_assert(!mm::platform::hasUsbVideo, "tests assume the desktop platform"); + CHECK(VideoService::kSourceCount == 2); + + VideoService v; + v.source = VideoService::kSourceUsb; + v.applyState(); + CHECK(v.source == VideoService::kSourcePattern); // fell back + CHECK(VideoService::latestFrame()->rgb != nullptr); +} + +// A platform with no capture must report an EMPTY format list rather than a placeholder, since a phantom entry would let the user pick a dead format. +TEST_CASE("VideoService: a platform with no capture advertises no formats") { + mm::platform::VideoCaptureFormat formats[4]; + CHECK(mm::platform::videoCaptureFormats(formats, 4) == 0); +} + // Accepting a non-whitespace separator eats a pixel and shifts every channel one place, which tints the whole image rather than failing. TEST_CASE("VideoService PPM: the separator must be whitespace, not merely present") { uint16_t w = 0, h = 0; @@ -148,6 +166,26 @@ TEST_CASE("VideoService: the test pattern sweeps at patternSpeed pixels per seco CHECK(sweepX() == (start + 5) % VideoService::kPatternW); // parked, however long passes } +// Pinned to a measured case: a source showing (255,127,0) captured under PQ as (206,171,0). The curve has to land that pair on the LINEAR ratio the original color has, sRGB(127)/sRGB(255) = 0.212, which is the quantity an average is then taken of. +TEST_CASE("VideoService: the PQ tone table recovers the source's linear ratio") { + VideoService v; + v.source = VideoService::kSourceUsb; + v.hdr = VideoService::kHdrPq; + v.hdrNits = 2000; + v.onControlChanged("hdr"); // the path a UI edit takes + const uint16_t* t = v.toneForTest(); + REQUIRE(t != nullptr); + + CHECK(171.0 / 206.0 > 0.80); // the defect, as the bytes arrive + const double linear = static_cast(t[171]) / t[206]; + CHECK(linear > 0.15); // recovered toward 0.212 + CHECK(linear < 0.30); + + // Monotonic: a curve that reorders levels would posterize. + CHECK(t[0] == 0); + for (int i = 1; i < 256; i++) CHECK(t[i] >= t[i - 1]); +} + // Every source publishes one, SDR included: sRGB is a curve like any other, and a consumer averaging raw bytes averages a quantity that is not proportional to light. TEST_CASE("VideoService: an SDR source publishes the sRGB curve, not nothing") { VideoService v; From 9b9e2e2305382335f3cb02ddc27742af14c25a84 Mon Sep 17 00:00:00 2001 From: Shura Fedorovskyi Date: Sat, 10 Oct 2026 15:16:07 +0400 Subject: [PATCH 3/3] Trim each LED die to correct a color cast An LED's three emitters are not equally efficient, so a neutral triple can render with a cast: on an SK6812 the green die runs hot and grey scenes come out tinted. Four trims correct it at the output, once per strip, rather than leaving every effect to guess. **Light domain** - balanceRed, balanceGreen and balanceBlue pull one die down to meet the others. Trim DOWN only, since there is no headroom above 255 and raising a channel clips rather than balances - whiteLevel trims the white die, which the RGB trims cannot reach: a white phosphor is separate hardware, often brighter than the trio beside it - Folded into the SAME table fill that applies the curve, as a linear pre-scale, so the hot path stays one lookup per channel and the curve still lands last - briLut carries a row per trim, and the 16-bit table a driver allocates for fine channels carries the same rows, so a 16-bit output is balanced exactly as an 8-bit one is - Amber and UV read an untrimmed row, being their own dies that an RGB trim says nothing about - FixtureProfilesModule: profileHasWhiteDie(), narrower than profileHasSynthChannel, so a profile carrying amber but no white does not offer a slider that changes nothing **Tests** - Six cases: one trim leaves the others untouched, a trim does not reach the white die, the curve and a trim compose into one table, whiteLevel moves the white alone, and whiteLevel of 0 puts the die out rather than leaving it at the never-black floor **Docs/CI** - The Shared driver card gains the four controls, with the reasoning in its details section Co-Authored-By: Claude Opus 5 (1M context) --- docs/moonmodules/light/drivers.md | 9 ++ src/light/drivers/Correction.h | 59 +++++++--- src/light/drivers/DriverBase.h | 19 +++- src/light/drivers/FixtureProfilesModule.h | 19 ++++ test/unit/light/correction_presets.h | 8 ++ test/unit/light/unit_Correction.cpp | 122 +++++++++++++++++---- test/unit/light/unit_Drivers_container.cpp | 12 +- 7 files changed, 203 insertions(+), 45 deletions(-) diff --git a/docs/moonmodules/light/drivers.md b/docs/moonmodules/light/drivers.md index 025731725..758bef0e4 100644 --- a/docs/moonmodules/light/drivers.md +++ b/docs/moonmodules/light/drivers.md @@ -21,7 +21,9 @@ The block every driver card opens with, shown here on RMT LED. Added once by [`D - `localBrightness`: this driver's dim (0–255), multiplied with the global brightness. - `curve`: brightness to output: `CIE 1931`, `gamma 2.2`, `gamma 2.8`, or `linear`. - `fixture`: the [fixture profile](supporting.md) applied per light, for order and white. +- `balanceRed`, `balanceGreen`, `balanceBlue`: trim one die down (0–255) to correct a color cast. - `whiteMode`: how W is derived, shown when the profile carries a W channel. +- `whiteLevel`: the white die's own trim, shown when the profile carries one. - `start`: first light of the shared buffer this driver reads (default `0`). - `count`: how many lights from `start`. **Blank drives all of them.** @@ -244,6 +246,13 @@ Detail: [technical](moxygen/RtspDriver.md) · [the RTP packetiser](moxygen/RtpH2 ## Shared, details + +**White balance corrects the dies, not the picture.** An LED's three emitters are not equally efficient, and on an SK6812 the green runs hot, so a neutral triple renders green-cast and grey scenes come out tinted. `balanceRed`, `balanceGreen` and `balanceBlue` trim one channel down to meet the others. Trim **down only**: there is no headroom above 255, so raising a channel clips instead of balancing it. Set this once for the strip and every effect is corrected at the output, rather than each one guessing. + +**The white die carries its own trim.** `whiteLevel` reaches the W and warm-W emitters alone, which the three RGB trims cannot: a white phosphor is separate hardware, often brighter than the trio beside it. It is shown only when the referenced profile carries a white channel, a narrower test than `whiteMode`'s, so a profile with amber or UV but no white does not offer a slider that changes nothing. Amber and UV read an untrimmed row for the same reason: they are their own dies, and an RGB trim says nothing about them. + +**One table, not two stages.** A trim is a linear **pre-scale** folded into the same fill that applies the curve, so the hot path stays one lookup per channel and the curve still lands last. The table carries a row per trim, and the 16-bit table a driver allocates for fine channels carries the same rows, so a 16-bit output is balanced exactly as an 8-bit one is. + **A `fixture` reference survives some changes and not others.** The driver holds the profile's stable id at runtime, so **reordering** profiles never disturbs it, and the reference **survives a reboot** because the profile's name is persisted and re-resolved on load. The caveat is **renaming**: within a session the id keeps the link, but after a reboot a renamed profile no longer matches the persisted name and the driver falls back to the default. Re-pick it if you rename a profile a driver uses. **`start` and `count` are how several drivers share one buffer.** Blank `count` drives every light; a number drives only that slice. An onboard status LED takes `start 0, count 1` while the main strip runs from `start 1`, both reading the same buffer. diff --git a/src/light/drivers/Correction.h b/src/light/drivers/Correction.h index 69dc0cd3a..67f567a89 100644 --- a/src/light/drivers/Correction.h +++ b/src/light/drivers/Correction.h @@ -45,11 +45,24 @@ struct Correction { } - uint8_t briLut[256] = {}; // briLut[v] = curve(v * brightness / 255) - /// The same curve to 16 bits, filled only when the table is wanted; the driver owns the 256 entries. + // White, amber and UV are their own dies, so an RGB trim must not reach them, and the white dies carry a trim of their own. + /// Row of `briLut` carrying no trim, for a channel no RGB balance may reach. + static constexpr uint8_t kNeutral = 3; + /// Row of `briLut` carrying the white die's own trim. + static constexpr uint8_t kWhite = 4; + /// How many trim rows the tables carry, which is the stride of both of them. + static constexpr uint8_t kRows = 5; + /// briLut[ch][v] = curve(v * brightness * balance[ch]), with rows 0=R, 1=G, 2=B, 3=untrimmed, 4=white. + uint8_t briLut[kRows][256] = {}; + /// The same curves to 16 bits, row-major with a stride of 256, filled only when the table is wanted; the driver owns the kRows * 256 entries. uint16_t* lut16 = nullptr; /// Set by a driver whose output resolves more than a byte, so the 16-bit table is filled whatever the profile. bool wide = false; + /// Per-channel white balance, 255 = untouched. Trim DOWN only: there is no headroom above 255, so raising clips instead of balancing. + uint8_t balRed = 255, balGreen = 255, balBlue = 255; + // A separate emitter, often brighter than the RGB trio, that the three trims above cannot reach. + /// The white die's trim, 255 = untouched, pre-scaling like the others so the curve still lands last. + uint8_t whiteLevel = 255; /// Which curve the brightness rebuild fills through; a driver's setting, not a global one. Curve curve = Curve::Cie; // The output-byte position of each color role, recomputed from the role array. @@ -93,11 +106,17 @@ struct Correction { // ORDER is the whole design: brightness is a linear pre-scale and the curve is applied LAST. /// Refresh the brightness LUT alone, leaving the channel offsets untouched. void rebuildBrightness(uint8_t brightness) MM_NONBLOCKING { - for (int v = 0; v < 256; v++) { - const float out = shape(static_cast(v) * brightness / 255.0f); // scale first, then curve - const bool lit = v > 0 && brightness > 0; - briLut[v] = static_cast(quantize(out, 255, lit)); - if (wantsTable() && lut16) lut16[v] = static_cast(quantize(out * 257.0f, 65535, lit)); // 255 * 257 = 65535 + // The trim pre-scales like brightness, so the curve still lands last. + const uint8_t balance[kRows] = {balRed, balGreen, balBlue, 255, whiteLevel}; + for (uint8_t ch = 0; ch < kRows; ch++) { + const float scale = static_cast(brightness) * balance[ch] / 255.0f; + for (int v = 0; v < 256; v++) { + const float out = shape(static_cast(v) * scale / 255.0f); // scale first, then curve + const bool lit = v > 0 && scale > 0.0f; + briLut[ch][v] = static_cast(quantize(out, 255, lit)); + if (wantsTable() && lut16) + lut16[ch * 256 + v] = static_cast(quantize(out * 257.0f, 65535, lit)); // 255 * 257 = 65535 + } } } @@ -154,11 +173,16 @@ struct Correction { } /// Hot path: transform one source light into its output bytes, integer-only and allocation-free. - inline void apply(const uint8_t* src, uint8_t* out, uint8_t srcChannels) const { applyThrough(briLut, src, out, srcChannels); } + inline void apply(const uint8_t* src, uint8_t* out, uint8_t srcChannels) const { applyThrough(&briLut[0][0], src, out, srcChannels); } /// Hot path, wide drivers only: the same transform through the 16-bit table, one value per channel; needs `lut16`. inline void applyWide(const uint8_t* src, uint16_t* out, uint8_t srcChannels) const { applyThrough(lut16, src, out, srcChannels); } + // Both tables are row-major with a stride of 256, so one base pointer serves either width. + /// The trim row `ch` of a table, which is how a channel reaches its own balance. + template + static const T* row(const T* lut, uint8_t ch) MM_NONBLOCKING { return lut + static_cast(ch) * 256; } + // A REMAP, not a copy: the layer's packed slots become the fixture's own offsets. /// The transform through a curve table of either width, so the 8-bit and 16-bit outputs share every line. template @@ -193,24 +217,24 @@ struct Correction { } else { w = r < g ? (r < b ? r : b) : (g < b ? g : b); // min(r,g,b): the white component // Computed off the PRE-subtraction values, which only rebalance the RGB emitters. - if (offWarmWhite != kAbsent) out[offWarmWhite] = lut[w]; + if (offWarmWhite != kAbsent) out[offWarmWhite] = row(lut, kWhite)[w]; // yellow ≈ min(R,G) (the shared red+green component). - if (offYellow != kAbsent) out[offYellow] = lut[r < g ? r : g]; + if (offYellow != kAbsent) out[offYellow] = row(lut, kNeutral)[r < g ? r : g]; // Driven from the blue with no red or green to pair with, so it stays dark on warm colors. if (offUV != kAbsent) { const uint8_t rg = r > g ? r : g; - out[offUV] = lut[b > rg ? static_cast(b - rg) : 0]; + out[offUV] = row(lut, kNeutral)[b > rg ? static_cast(b - rg) : 0]; } // White last: it is the only emitter that rebalances RGB. if (offWhite != kAbsent) { if (whiteMode == WhiteMode::Accurate) { r -= w; g -= w; b -= w; } // pull white out of RGB - out[offWhite] = lut[w]; + out[offWhite] = row(lut, kWhite)[w]; } } // The curve, applied ONCE: everything above this line is linear light. - if (offRed != kAbsent) out[offRed] = lut[r]; - if (offGreen != kAbsent) out[offGreen] = lut[g]; - if (offBlue != kAbsent) out[offBlue] = lut[b]; + if (offRed != kAbsent) out[offRed] = row(lut, 0)[r]; + if (offGreen != kAbsent) out[offGreen] = row(lut, 1)[g]; + if (offBlue != kAbsent) out[offBlue] = row(lut, 2)[b]; if constexpr (sizeof(T) == 1) { if (hasFine) applyFine(r, g, b, w, out); } } @@ -221,10 +245,13 @@ struct Correction { const uint8_t value[5] = {r, g, b, w, w}; // both whites carry the white component, 0 when none is synthesized const uint8_t coarse[5] = {offRed, offGreen, offBlue, offWhite, offWarmWhite}; const uint8_t fine[5] = {offRedFine, offGreenFine, offBlueFine, offWhiteFine, offWarmWhiteFine}; + // The trim row each channel reads: its own for R/G/B, the white die's for both whites. + const uint8_t row[5] = {0, 1, 2, kWhite, kWhite}; for (uint8_t k = 0; k < 5; k++) { if (fine[k] == kAbsent) continue; // Without its table (an allocation that failed) the channel stays 8-bit: the coarse byte as written, the fine one 0. - const uint16_t q = lut16 ? lut16[value[k]] : static_cast(briLut[value[k]] << 8); + const uint16_t q = lut16 ? lut16[row[k] * 256 + value[k]] + : static_cast(briLut[row[k]][value[k]] << 8); if (coarse[k] != kAbsent) out[coarse[k]] = static_cast(q >> 8); out[fine[k]] = static_cast(q & 0xFF); } diff --git a/src/light/drivers/DriverBase.h b/src/light/drivers/DriverBase.h index d701a8ee7..1b978a306 100644 --- a/src/light/drivers/DriverBase.h +++ b/src/light/drivers/DriverBase.h @@ -72,6 +72,10 @@ class DriverBase : public MoonModule { // Applied unconditionally, so brightness works even before the fixture-profile library is up. const uint8_t tableBrightness = adoptBrightness(globalBrightness); correction_.whiteMode = static_cast(whiteMode_); + correction_.balRed = balanceRed_; + correction_.balGreen = balanceGreen_; + correction_.balBlue = balanceBlue_; + correction_.whiteLevel = whiteLevel_; correction_.curve = static_cast(curveSel_); // A missing id falls back to the default, so a driver degrades rather than crashing. if (auto* lib = FixtureProfilesModule::active()) { @@ -87,7 +91,7 @@ class DriverBase : public MoonModule { } // Allocated the first time the table is wanted and kept until release, since the encode task may be reading it while this rebuild runs. const bool firstTable = correction_.wantsTable() && !lut16_; - if (firstTable) lut16_.resize(256); + if (firstTable) lut16_.resize(Correction::kRows * 256); // one row per trim, as briLut carries correction_.lut16 = lut16_.data(); if (firstTable) correction_.rebuildBrightness(tableBrightness); // a table that existed was filled by the rebuild above onCorrectionChanged(); // let a driver resize its correction-applied buffer @@ -183,6 +187,8 @@ class DriverBase : public MoonModule { uint32_t profileId_ = 0; // stable id into the FixtureProfiles library (0 → resolve to default) uint8_t fixtureSel_ = 0; // the fixture Select's chosen INDEX (mapped to an id in onControlChanged) uint8_t whiteMode_ = static_cast(WhiteMode::Min); // index into kWhiteModeOptions + uint8_t balanceRed_ = 255, balanceGreen_ = 255, balanceBlue_ = 255; // per-channel white balance, 255 = untouched + uint8_t whiteLevel_ = 255; // the white die's own trim uint8_t localBrightness_ = 255; // per-driver dim, multiplied with the global brightness /// Which perceptual curve the output LUT is built through; CIE lightness by default. uint8_t curveSel_ = 0; // index into kCurveOptions; 0 = CIE @@ -208,10 +214,17 @@ class DriverBase : public MoonModule { buildFixtureOptions(); // fill fixtureOptions_ from the library, sync id and row controls_.addSelect("fixture", fixtureSel_, fixtureOptions_, fixtureOptionCount_); controls_.setPersistLabel(controls_.count() - 1); // saved by name, so a longer or reordered library never moves it + // Trim DOWN only: there is no headroom above 255, so raising clips rather than balancing. A strip whose green die runs hot is corrected here rather than in every effect. + controls_.addControl("balanceRed", balanceRed_, 0, 255); + controls_.addControl("balanceGreen", balanceGreen_, 0, 255); + controls_.addControl("balanceBlue", balanceBlue_, 0, 255); controls_.addSelect("whiteMode", whiteMode_, kWhiteModeOptions, kWhiteModeCount); // Hidden unless the referenced profile carries a channel there is something to synthesize for. auto* lib = FixtureProfilesModule::active(); controls_.setHidden(controls_.count() - 1, !(lib && lib->profileHasSynthChannel(profileId_))); + // A separate emitter the three RGB trims cannot reach, so it carries its own. + controls_.addControl("whiteLevel", whiteLevel_, 0, 255); + controls_.setHidden(controls_.count() - 1, !(lib && lib->profileHasWhiteDie(profileId_))); // Hidden, and only read: the name an older config saved. controls_.addText("fixtureRef", fixtureRef_, sizeof(fixtureRef_)); controls_.setHidden(controls_.count() - 1, true); @@ -223,7 +236,9 @@ class DriverBase : public MoonModule { /// Whether `name` is one of the correction controls, for a driver's own prepare test. static bool isCorrectionControl(const char* name) { return std::strcmp(name, "fixture") == 0 || std::strcmp(name, "fixtureRef") == 0 || std::strcmp(name, "localBrightness") == 0 - || std::strcmp(name, "whiteMode") == 0 || std::strcmp(name, "curve") == 0; + || std::strcmp(name, "whiteMode") == 0 || std::strcmp(name, "curve") == 0 + || std::strcmp(name, "balanceRed") == 0 || std::strcmp(name, "balanceGreen") == 0 + || std::strcmp(name, "balanceBlue") == 0 || std::strcmp(name, "whiteLevel") == 0; } private: diff --git a/src/light/drivers/FixtureProfilesModule.h b/src/light/drivers/FixtureProfilesModule.h index fa505dc6a..520c66c20 100644 --- a/src/light/drivers/FixtureProfilesModule.h +++ b/src/light/drivers/FixtureProfilesModule.h @@ -64,6 +64,25 @@ class FixtureProfilesModule : public MoonModule, public ListSource { return 0; // a dangling id renders at the first profile; rebuildCorrection falls back to it too } + // Narrower than profileHasSynthChannel on purpose, since the white trim reaches the white dies alone. A profile with amber but no white would otherwise show a slider that changes nothing. + /// Whether the profile carries a white die, cold or warm, at either precision. + bool profileHasWhiteDie(uint32_t id) const { + const Profile* p = find(id); + if (!p) return false; + const uint8_t* r = roleAt(*p); + for (uint8_t c = 0; c < p->channelCount; c++) { + switch (static_cast(r[c])) { + case ChannelRole::White: + case ChannelRole::WarmWhite: + case ChannelRole::WhiteFine: + case ChannelRole::WarmWhiteFine: + return true; + default: break; + } + } + return false; + } + // Any channel apply() synthesizes from RGB counts, since one control governs them all. /// Whether the profile carries a channel the white mode would synthesize. bool profileHasSynthChannel(uint32_t id) const { diff --git a/test/unit/light/correction_presets.h b/test/unit/light/correction_presets.h index de6980625..8e24a640f 100644 --- a/test/unit/light/correction_presets.h +++ b/test/unit/light/correction_presets.h @@ -32,5 +32,13 @@ inline void rebuildFromPreset(mm::Correction& c, uint8_t brightness, PresetOrder } } +/// The same, leaving `curve` as the caller set it, for a test about how the curve composes with a trim. +inline void rebuildFromPresetKeepingCurve(mm::Correction& c, uint8_t brightness, PresetOrder order) { + const auto keep = c.curve; + rebuildFromPreset(c, brightness, order); + c.curve = keep; + c.rebuildBrightness(brightness); +} + /// @} } // namespace mm::test diff --git a/test/unit/light/unit_Correction.cpp b/test/unit/light/unit_Correction.cpp index 202d2a61a..9f897d22c 100644 --- a/test/unit/light/unit_Correction.cpp +++ b/test/unit/light/unit_Correction.cpp @@ -22,7 +22,7 @@ TEST_CASE("Correction brightness LUT: full brightness is identity") { Correction c; c.curve = Correction::Curve::Linear; // these pin roles and white math, not the curve mm::test::rebuildFromPreset(c, 255, mm::test::PresetOrder::RGB); - for (int v = 0; v < 256; v++) CHECK(c.briLut[v] == v); + for (int v = 0; v < 256; v++) CHECK(c.briLut[0][v] == v); } // At brightness=128, every entry is roughly halved using scale8 (255→128, 128→64, 2→1). @@ -30,10 +30,10 @@ TEST_CASE("Correction brightness LUT: half brightness halves each value (scale8) Correction c; c.curve = Correction::Curve::Linear; // these pin roles and white math, not the curve mm::test::rebuildFromPreset(c, 128, mm::test::PresetOrder::RGB); - CHECK(c.briLut[0] == 0); - CHECK(c.briLut[255] == 128); // (255*128)/255 = 128 - CHECK(c.briLut[128] == 64); // (128*128)/255 = 64 - CHECK(c.briLut[2] == 1); // (2*128)/255 = 1 + CHECK(c.briLut[0][0] == 0); + CHECK(c.briLut[0][255] == 128); // (255*128)/255 = 128 + CHECK(c.briLut[0][128] == 64); // (128*128)/255 = 64 + CHECK(c.briLut[0][2] == 1); // (2*128)/255 = 1 } // RGB preset at full brightness passes the source RGB through unchanged (3 output channels, no white). @@ -186,7 +186,7 @@ TEST_CASE("Correction roles array: arbitrary Custom wiring derives correct offse CHECK(c.offGreen == 2); CHECK(c.offRed == 3); CHECK(c.outChannels == 4); - CHECK(c.briLut[255] == 128); // LUT refreshed (brightness applied) + CHECK(c.briLut[0][255] == 128); // LUT refreshed (brightness applied) const uint8_t src[3] = {200, 100, 60}; // scaled: 100, 50, 30 → min = 30 uint8_t out[4] = {}; c.apply(src, out, 3); @@ -459,13 +459,13 @@ TEST_CASE("The CIE curve keeps the endpoints and bends the middle down") { c.curve = Correction::Curve::Cie; c.rebuildBrightness(255); - CHECK(c.briLut[0] == 0); // black stays black - CHECK(c.briLut[255] == 255); // and full stays full: the curve redistributes, it does not dim + CHECK(c.briLut[0][0] == 0); // black stays black + CHECK(c.briLut[0][255] == 255); // and full stays full: the curve redistributes, it does not dim // A mid control position is well under half output, which is the whole point: half the PERCEIVED brightness is a small fraction of the luminance. - CHECK(c.briLut[128] < 80); - CHECK(c.briLut[128] > 30); + CHECK(c.briLut[0][128] < 80); + CHECK(c.briLut[0][128] > 30); // Monotonic, or a gradient would band or reverse. - for (int v = 1; v < 256; v++) CHECK(c.briLut[v] >= c.briLut[v - 1]); + for (int v = 1; v < 256; v++) CHECK(c.briLut[0][v] >= c.briLut[0][v - 1]); } // The guard that stops a fade-out snapping to black. Every curve here crushes the low end at 8 bits, so without it the dimmest usable values are simply missing. @@ -475,7 +475,7 @@ TEST_CASE("A non-zero value never curves down to black") { mm::test::rebuildFromPreset(c, 255, mm::test::PresetOrder::RGB); c.curve = curve; c.rebuildBrightness(255); - for (int v = 1; v < 256; v++) CHECK(c.briLut[v] >= 1); + for (int v = 1; v < 256; v++) CHECK(c.briLut[0][v] >= 1); } } @@ -485,7 +485,7 @@ TEST_CASE("Linear is an exact identity at full brightness, for a device that cor mm::test::rebuildFromPreset(c, 255, mm::test::PresetOrder::RGB); c.curve = Correction::Curve::Linear; c.rebuildBrightness(255); - for (int v = 0; v < 256; v++) CHECK(c.briLut[v] == v); + for (int v = 0; v < 256; v++) CHECK(c.briLut[0][v] == v); } // Brightness is a LINEAR pre-scale and the curve is applied last. Turning the slider down must not change the shape of the curve, only how far up it reaches. @@ -495,11 +495,11 @@ TEST_CASE("Brightness scales before the curve, so black stays black and the top c.curve = Correction::Curve::Cie; c.rebuildBrightness(255); - const uint8_t fullTop = c.briLut[255]; + const uint8_t fullTop = c.briLut[0][255]; c.rebuildBrightness(128); - const uint8_t halfTop = c.briLut[255]; + const uint8_t halfTop = c.briLut[0][255]; - CHECK(c.briLut[0] == 0); // zero is zero at any brightness + CHECK(c.briLut[0][0] == 0); // zero is zero at any brightness CHECK(halfTop < fullTop); // the slider still reaches the output // Half the SLIDER is well under half the light, because the curve applies after the scale. CHECK(halfTop < fullTop / 2); @@ -522,11 +522,11 @@ TEST_CASE("RGBW white is derived in linear light, then curved once") { mm::test::rebuildFromPreset(ref, 255, mm::test::PresetOrder::RGB); ref.curve = Correction::Curve::Cie; ref.rebuildBrightness(255); - CHECK(out[3] == ref.briLut[100]); + CHECK(out[3] == ref.briLut[Correction::kWhite][100]); // and RGB carry the remainder (200-100, 150-100, 0), each curved. - CHECK(out[0] == ref.briLut[100]); - CHECK(out[1] == ref.briLut[50]); - CHECK(out[2] == ref.briLut[0]); + CHECK(out[0] == ref.briLut[0][100]); + CHECK(out[1] == ref.briLut[0][50]); + CHECK(out[2] == ref.briLut[0][0]); } namespace { @@ -564,7 +564,7 @@ TEST_CASE("At low brightness a 16-bit channel keeps every level the 8-bit table const uint8_t src[3] = {static_cast(v), 0, 0}; uint8_t out[6] = {}; c.apply(src, out, 3); - narrow.insert(c.briLut[v]); + narrow.insert(c.briLut[0][v]); wide.insert(out[0] * 256 + out[1]); } CHECK(narrow.size() <= 4); @@ -625,3 +625,83 @@ TEST_CASE("Dimmer fine is driven wide open with its coarse byte") { c.apply(src, out, 3); CHECK((out[0] == 255 && out[1] == 255)); } + +// --- White balance -------------------------------------------------------------------------- +// An LED's three dies are not equally efficient, and on an SK6812 the green runs hot, so a neutral triple renders green-cast. One trim per channel, folded into the table the hot path already reads. + +TEST_CASE("Correction white balance: trimming one channel leaves the others untouched") { + Correction c; + c.balGreen = 128; + mm::test::rebuildFromPreset(c, 255, mm::test::PresetOrder::RGB); + CHECK(c.briLut[0][255] == 255); // red untrimmed + CHECK(c.briLut[1][255] == 128); // green at half + CHECK(c.briLut[2][255] == 255); // blue untrimmed + + const uint8_t src[3] = {200, 200, 200}; // the triple a green-cast strip renders wrong + uint8_t out[3] = {}; + c.apply(src, out, 3); + CHECK(out[0] == 200); + CHECK(out[1] == 100); // pulled down to match the weaker channels + CHECK(out[2] == 200); +} + +// The trim corrects the RGB dies, so it must not reach the separate white phosphor. +TEST_CASE("Correction white balance: an RGB trim does not reach the white die") { + Correction c; + c.balBlue = 128; + mm::test::rebuildFromPreset(c, 255, mm::test::PresetOrder::RGBW); + const uint8_t src[3] = {200, 200, 200}; + uint8_t out[4] = {}; + c.apply(src, out, 3); + CHECK(out[0] == 200); // R + CHECK(out[1] == 200); // G + CHECK(out[2] == 100); // B trimmed + CHECK(out[3] == 200); // W from the RAW minimum, untrimmed +} + +// Two inputs to one fill rather than two stages, so the hot path stays a single lookup. +TEST_CASE("Correction: the curve and white balance compose into the one table") { + Correction c; + c.curve = Correction::Curve::Gamma22; + c.balBlue = 128; + mm::test::rebuildFromPresetKeepingCurve(c, 255, mm::test::PresetOrder::RGB); + CHECK(c.briLut[0][200] == 149); // red: curve only, (200/255)^2.2 * 255 + // Blue takes the trim as a PRE-scale, so the curve still lands last. + CHECK(c.briLut[2][200] < c.briLut[0][200]); + CHECK(c.briLut[2][200] > 0); +} + +// The white die is separate hardware the RGB trims say nothing about, so it carries its own. +TEST_CASE("Correction whiteLevel: trims the white die without touching RGB") { + const uint8_t src[3] = {200, 160, 120}; // white component = min = 120 + + Correction full; + mm::test::rebuildFromPreset(full, 255, mm::test::PresetOrder::RGBW); + uint8_t a[4] = {}; + full.apply(src, a, 3); + + Correction half; + half.whiteLevel = 128; + mm::test::rebuildFromPreset(half, 255, mm::test::PresetOrder::RGBW); + uint8_t b[4] = {}; + half.apply(src, b, 3); + + CHECK(b[3] < a[3]); // white trimmed + CHECK(b[3] == half.briLut[Correction::kWhite][120]); // through its own row, curve last + CHECK(b[0] == a[0]); // and RGB untouched + CHECK(b[1] == a[1]); + CHECK(b[2] == a[2]); +} + +// Zero has to mean dark. The never-black nudge is gated on the SCALE rather than the input, or a fully trimmed die would still emit its floor. +TEST_CASE("Correction whiteLevel: zero puts the white die out, not at its floor") { + Correction c; + c.whiteLevel = 0; + mm::test::rebuildFromPreset(c, 255, mm::test::PresetOrder::RGBW); + for (int v = 0; v < 256; v++) CHECK(c.briLut[Correction::kWhite][v] == 0); + const uint8_t src[3] = {200, 160, 120}; + uint8_t out[4] = {}; + c.apply(src, out, 3); + CHECK(out[3] == 0); + CHECK(out[0] > 0); // the colors are untouched by it +} diff --git a/test/unit/light/unit_Drivers_container.cpp b/test/unit/light/unit_Drivers_container.cpp index 043658bbe..d68790d13 100644 --- a/test/unit/light/unit_Drivers_container.cpp +++ b/test/unit/light/unit_Drivers_container.cpp @@ -137,19 +137,19 @@ TEST_CASE("Drivers::on gates the correction LUT without clobbering brightness") drivers.on = true; drivers.onControlChanged("brightness"); // rebuild LUT at full power CHECK(drivers.effectiveBrightness() == 200); - CHECK(drv.correctionForTest().briLut[255] == 200); // (255 * 200) / 255 == 200 + CHECK(drv.correctionForTest().briLut[0][255] == 200); // (255 * 200) / 255 == 200 // Turn off → LUT scales to black, but the brightness value is untouched. drivers.on = false; drivers.onControlChanged("on"); CHECK(drivers.brightness == 200); // value preserved CHECK(drivers.effectiveBrightness() == 0); - CHECK(drv.correctionForTest().briLut[255] == 0); // output black + CHECK(drv.correctionForTest().briLut[0][255] == 0); // output black // Turn back on → the exact level returns, no stored-value juggling. drivers.on = true; drivers.onControlChanged("on"); - CHECK(drv.correctionForTest().briLut[255] == 200); + CHECK(drv.correctionForTest().briLut[0][255] == 200); } // A per-driver localBrightness change re-scales that driver's correction LUT (global × local), as a global brightness change does. @@ -163,17 +163,17 @@ TEST_CASE("Drivers: a localBrightness change re-scales the driver's correction L // LINEAR, so the arithmetic below reads as the multiplication it is testing. What this pins is that BOTH sliders reach the LUT, which a perceptual curve would leave true but express as table lookups nobody can check by eye. The curve has its own tests. mm::test::setControlValue(drv, "curve", static_cast(mm::Correction::Curve::Linear)); drivers.setup(); // seeds the driver's correction (global 200, local 255) - CHECK(drv.correctionForTest().briLut[255] == 200); // global 200 × local 255/255 = 200 + CHECK(drv.correctionForTest().briLut[0][255] == 200); // global 200 × local 255/255 = 200 // Halve the driver's LOCAL brightness, its own control change must re-bake the LUT to global × local = 200 × 128/255 ≈ 100. This is the path the bug missed. mm::test::setControlValue(drv, "localBrightness", 128); drv.onControlChanged("localBrightness"); - CHECK(drv.correctionForTest().briLut[255] == 100); // (200 * 128) / 255 == 100 + CHECK(drv.correctionForTest().briLut[0][255] == 100); // (200 * 128) / 255 == 100 // And the global slider still composes on top: raising global to 255 with local 128 → 128. drivers.brightness = 255; drivers.onControlChanged("brightness"); - CHECK(drv.correctionForTest().briLut[255] == 128); // (255 * 128) / 255 == 128 + CHECK(drv.correctionForTest().briLut[0][255] == 128); // (255 * 128) / 255 == 128 } // Disabled child drivers don't tick: toggling `enabled` flips whether that driver's tick() runs.