GCC Code Coverage Report


Directory: ./
File: overlays/PixelOverlayModel.h
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 35 36 97.2%
Functions: 17 17 100.0%
Branches: 25 34 73.5%

Line Branch Exec Source
1 #pragma once
2 /*
3 * This file is part of the Falcon Player (FPP) and is Copyright (C)
4 * 2013-2022 by the Falcon Player Developers.
5 *
6 * The Falcon Player (FPP) is free software, and is covered under
7 * multiple Open Source licenses. Please see the included 'LICENSES'
8 * file for descriptions of what files are covered by each license.
9 *
10 * This source file is covered under the LGPL v2.1 as described in the
11 * included LICENSE.LGPL file.
12 */
13
14 // PLUGIN ABI: native plugins compile against this header and are loaded with
15 // dlopen(), so parts of it are baked into binaries FPP did not build.
16 // - PixelOverlayModel's data layout: plugins call the inline getters
17 // (getName/getType/getWidth/getHeight/getSize/getRunningEffect/
18 // getRunningEffectMutex), which read members at compiled-in offsets.
19 // - PixelOverlayModel's vtable (plugins call setState() etc. virtually).
20 // - PixelOverlayState, which plugins construct and pass by value.
21 // Any such change must bump FPP_PLUGIN_API_VERSION in Plugin.h. Without the
22 // bump, a plugin built against the previous header still loads and silently
23 // uses the old layout - FPP 10.2 shipped exactly that and crash-looped fppd.
24 // See .claude/PLUGIN-ABI.md for which plugins use what.
25
26 #include <list>
27 #include <map>
28 #include "fpp-json.h"
29 #include <memory>
30 #include <mutex>
31 #include <thread>
32 #include <vector>
33
34 #include "../AtomicSharedPtr.h"
35 #include "PolarBufferMap.h"
36
37 // Raised when a model's channel data or overlay buffer cannot be allocated at
38 // all -- shared memory, an anonymous mapping and malloc() all failed, which
39 // means the box is out of memory. Deliberately permanent: nothing rechecks the
40 // allocation, and the model stays unusable until fppd restarts.
41 constexpr int WARNING_ID_MODEL_MEMORY = 64;
42 constexpr const char* MODEL_MEMORY_WARNING =
43 "Out of memory allocating pixel overlay model buffers - some models will not display";
44
45 class RunningEffect;
46
47 class PixelOverlayState {
48 public:
49 enum PixelState : uint8_t {
50 Disabled,
51 Enabled,
52 Transparent,
53 TransparentRGB
54 };
55
56 45834 PixelOverlayState() :
57 45834 state(PixelState::Disabled) {}
58 183834 constexpr PixelOverlayState(PixelState v) :
59 183834 state(v) {}
60 222149 constexpr PixelOverlayState(int v) :
61 222149 state((PixelState)v) {}
62 116887 PixelOverlayState(const std::string& v) {
63
8/10
✓ Branch 0 taken 116873 times.
✓ Branch 1 taken 14 times.
✓ Branch 2 taken 116873 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 116871 times.
✓ Branch 5 taken 2 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 116871 times.
✓ Branch 8 taken 16 times.
✓ Branch 9 taken 116871 times.
116887 if (v == "Disabled" || v == "false" || v == "False" || v == "0") {
64 16 state = PixelState::Disabled;
65
8/10
✓ Branch 0 taken 44326 times.
✓ Branch 1 taken 72545 times.
✓ Branch 2 taken 5 times.
✓ Branch 3 taken 44321 times.
✓ Branch 4 taken 5 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 5 times.
✓ Branch 8 taken 116866 times.
✓ Branch 9 taken 5 times.
116871 } else if (v == "Enabled" || v == "true" || v == "True" || v == "1") {
66 116866 state = PixelState::Enabled;
67
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 2 times.
5 } else if (v == "Transparent") {
68 3 state = PixelState::Transparent;
69
2/6
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
2 } else if (v == "TransparentRGB" || v == "Transparent RGB") {
70 2 state = PixelState::TransparentRGB;
71 } else {
72 ✗ state = PixelState::Disabled;
73 }
74 116887 }
75
76 1957690 PixelState getState() const { return state; }
77 328398 bool operator==(PixelOverlayState a) const { return state == a.state; }
78 19089 bool operator!=(PixelOverlayState a) const { return state != a.state; }
79
80 private:
81 PixelState state;
82 };
83
84 class PixelOverlayModel {
85 public:
86 PixelOverlayModel(const Json::Value& config);
87 virtual ~PixelOverlayModel();
88
89 912841 const std::string& getName() const { return name; };
90 55724 const std::string& getType() const { return type; };
91
92 // How this model's BUFFER is laid out, when it is not the plain
93 // wiring-order rectangle. Read from the model definition's "BufferStyle"
94 // and reported in toJson(), so a producer (an xLights bridge script, a
95 // plugin) can declare a model whose axes already mean something and the UI
96 // can group and label it accordingly rather than filing it under whatever
97 // file it happened to arrive in. Empty for an ordinary model.
98 //
99 // Known values: "" (ordinary) and "polar" (x/y are radius and angle).
100 const std::string& getBufferStyle() const { return bufferStyle; }
101
102 4360106 int getWidth() const { return width; }
103 4351191 int getHeight() const { return height; }
104 5884 void getSize(int& w, int& h) const {
105 5884 w = width;
106 5884 h = height;
107 5884 }
108
109 PixelOverlayState getState() const;
110 // For the manager, when it retires this model (removeAutoOverlayModel()):
111 // gives up the shared memory names, which a replacement of the same name
112 // is about to take. The model may live on for a while in a reference
113 // holder's hands, and its destructor then leaves the names alone; a
114 // buffer it allocates after this is private memory.
115 void releaseSharedMemoryNames();
116 // The absolute output channel buffer cell (x, y)'s first byte lands on,
117 // or FPPD_OFF_CHANNEL for a hole or a cell outside the model.
118 uint32_t outputChannelForCell(int x, int y) const;
119 // Whether the channel output thread has to draw this model: its own state
120 // is enabled, or a submodel is enabled within it. The manager's active
121 // list is kept in step with this, not with individual state transitions.
122 bool needsOutput() const;
123 virtual void setState(const PixelOverlayState& state);
124
125 virtual void doOverlay(uint8_t* channels);
126
127 // A polar (radius x angle) view of this model's buffer, for the Radial/
128 // Angular buffer mappings. Built on first use from the model's geometry in
129 // config/virtualdisplaymap and cached; the result is owned by the model.
130 //
131 // Returns nullptr whenever a polar layout cannot be built -- no layout file
132 // (a player that has never used xLights), this model absent from it, or too
133 // little geometry. Callers MUST fall back to the ordinary buffer mapping,
134 // never treat it as an error.
135 //
136 // Not thread safe against itself: call it from the command path that starts
137 // an effect, never from the channel output thread.
138 const PolarBufferMap* getPolarMap(PolarMode mode);
139
140 int getStartChannel() const;
141 int getChannelCount() const;
142
143 void toJson(Json::Value& v);
144 void getDataJson(Json::Value& v, bool rle = false);
145 // Binary equivalent of getDataJson(): the current pixel data as a flat
146 // width*height*bytesPerPixel buffer.
147 void getData(std::vector<uint8_t>& out);
148
149 // Access to the channelData. The channelData is a minimal array of bytes
150 // If the model is a "custom" model or using singleChannel nodes or similar,
151 // then the channelData will be significantly smaller than WxHxBPP
152 2427725 int getBytesPerPixel() const { return bytesPerPixel; }
153 void saveOverlayAsImage(std::string filename = "");
154 // True for a name saveOverlayAsImage() accepts: no directory part,
155 // nothing hidden or relative.
156 static bool isPlainImageFileName(const std::string& f);
157 virtual void setData(const uint8_t* data); // full pixel data, width*height*bytesPerPixel
158 virtual void setData(const uint8_t* data, int xOffset, int yOffset, int w, int h, const PixelOverlayState& st = PixelOverlayState(PixelOverlayState::Enabled));
159
160 // Blit externally supplied pixel data onto the model. src is srcW*srcH
161 // pixels of srcBpp bytes each (1 = greyscale, 3 = RGB, 4 = RGBW); it is
162 // converted to this model's bytesPerPixel and CLIPPED to the model bounds,
163 // so a rect hanging off an edge draws the part that is visible instead of
164 // being rejected the way setData()'s rect form is. That is what lets a
165 // caller slide a sprite off the edge by reposting it at moving offsets.
166 // Returns false only when nothing landed inside the model at all.
167 //
168 // blitData() writes the output channel data (honoring st as the per-blit
169 // blend); blitOverlayBuffer() writes the mmapped overlay image buffer and
170 // marks it dirty, so the write composites into the same persistent canvas
171 // external mmap clients use and is flushed on the next output pass.
172 bool blitData(const uint8_t* src, int srcW, int srcH, int srcBpp,
173 int xOffset, int yOffset, const PixelOverlayState& st);
174 bool blitOverlayBuffer(const uint8_t* src, int srcW, int srcH, int srcBpp,
175 int xOffset, int yOffset);
176
177 void setScaledData(uint8_t* data, int w, int h);
178 void setPixelValue(int x, int y, int r, int g, int b);
179 void setPixelValue(int x, int y, int r, int g, int b, int w);
180 void getPixelValue(int x, int y, int& r, int& g, int& b);
181 void getPixelValue(int x, int y, int& r, int& g, int& b, int& w);
182 void clearData();
183 void fillData(int r, int g, int b);
184 void fillData(int r, int g, int b, int w);
185 void setBufferIsDirty(bool dirty = true);
186 bool needRefresh();
187
188 // False when the model's channel data could not be allocated at all. Such
189 // a model has no storage behind any of its accessors, so the manager drops
190 // it rather than registering one that cannot be indexed.
191 1519 bool isValid() const { return channelData != nullptr; }
192
193 // The overlay buffer is a full continuous width*height*bytesPerPixel buffer
194 // that can be used to construct the frame as a full RGB(W) image prior to
195 // flushing to the channelData. The overlay buffer is also mmapped so
196 // external programs can have easy access to it.
197 uint8_t* getOverlayBuffer();
198 void setOverlayBufferDirty(bool dirty = true);
199 bool overlayBufferIsDirty();
200 void clearOverlayBuffer();
201 void setOverlayBufferScaledData(uint8_t* data, int w, int h);
202 void fillOverlayBuffer(int r, int g, int b);
203 void fillOverlayBuffer(int r, int g, int b, int w);
204 void setOverlayPixelValue(int x, int y, int r, int g, int b);
205 void setOverlayPixelValue(int x, int y, int r, int g, int b, int w);
206 void getOverlayPixelValue(int x, int y, int& r, int& g, int& b);
207 void getOverlayPixelValue(int x, int y, int& r, int& g, int& b, int& w);
208 void flushOverlayBuffer();
209
210 // Operate on both the overlay buffer (if mapped) and the channelData
211 void clear();
212 void fill(int r, int g, int b);
213 void fill(int r, int g, int b, int w);
214
215 bool applyEffect(const std::string& autoState, const std::string& effect, const std::vector<std::string>& args);
216 void setRunningEffect(RunningEffect* r, int32_t firstUpdateMS);
217
218 // Retire the running effect WITHOUT clearing what it last drew. This is
219 // the "nothing takes over" case setRunningEffect() cannot express, and the
220 // Text effect needs it: a still, centred message writes its pixels
221 // directly, so an effect left repainting from an earlier run (a scroll, or
222 // an animated colour mode) would paint straight back over it.
223 void clearRunningEffect();
224
225 1417794 std::recursive_mutex& getRunningEffectMutex() { return effectLock; }
226 1421612 RunningEffect* getRunningEffect() const { return runningEffect; } // make sure you have the mutex locked
227 int32_t updateRunningEffects();
228
229 void setChildState(const std::string& n, const PixelOverlayState& state, int ox, int oy, int w, int h);
230 bool isAutoCreated();
231
232 protected:
233 // Constructor helpers: read a config field without throwing (a number,
234 // or a number in a string; false for anything else), and reject the
235 // definition. See the constructor.
236 static bool configInt(const Json::Value& c, const char* key, int dflt, int& out);
237 static bool configString(const Json::Value& c, const char* key, std::string& out);
238 void markInvalid(const std::string& why);
239
240 void setValue(uint8_t v, int startChannel = -1, int endChannel = -1);
241 bool flushChildren(uint8_t* dst);
242
243 // The 0-based ABSOLUTE output channel a channelData offset ends up on, or
244 // FPPD_OFF_CHANNEL. A plain model owns a contiguous range so it is just an
245 // addition; a submodel or group scatters, so those override this. Used only
246 // to join buffer cells to their position in the virtual display map.
247 virtual uint32_t outputChannelForData(uint32_t dataOffset) const;
248
249 // Cached polar views, keyed by mode AND the layout file's mtime.
250 //
251 // One entry per key, never replaced or erased: a running effect holds a
252 // bare pointer into this for its lifetime, so rebuilding in place would
253 // dangle it. std::map node addresses are stable across inserts, which is
254 // the property relied on. An entry with valid() == false records
255 // "tried, cannot" so a model with no geometry costs one attempt.
256 //
257 // The mtime in the key is what makes a re-upload from xLights take effect:
258 // a model that already built a map would otherwise keep its stale layout
259 // until fppd restarted. A superseded entry is left behind rather than
260 // erased (a few KB, only when the layout actually changes mid-run) because
261 // an effect may still be pointing at it.
262 std::map<std::pair<PolarMode, long long>, PolarBufferMap> polarMaps;
263
264 Json::Value config;
265 std::string name;
266 std::string type;
267 int width, height;
268 std::string bufferStyle;
269 PixelOverlayState state;
270 int startChannel;
271 int channelCount;
272 int channelsPerNode;
273 int bytesPerPixel; // 3 for RGB, 4 for RGBW (overlay buffer stride)
274
275 std::vector<uint32_t> channelMap;
276 uint8_t* channelData;
277
278 volatile bool dirtyBuffer = false;
279
280 struct OverlayBufferData {
281 uint32_t width;
282 uint32_t height;
283 uint32_t flags; // bit 0: dirty, bits 8-15: bytesPerPixel (0 means 3 for backward compat)
284 uint8_t data[4];
285 } __attribute__((__packed__));
286 OverlayBufferData* overlayBufferData;
287
288 // How each buffer was allocated, so the destructor releases it the same
289 // way. munmap() on a malloc()ed pointer either fails outright or, if the
290 // allocation happened to be page aligned, unmaps memory the allocator still
291 // believes it owns.
292 bool overlayBufferMapped = true;
293 bool channelDataMapped = true;
294
295 std::recursive_mutex effectLock;
296 RunningEffect* runningEffect;
297
298 class ChildModelState {
299 public:
300 std::string name;
301 PixelOverlayState state = PixelOverlayState::Disabled;
302 int xoffset = 0;
303 int yoffset = 0;
304 int width = 0;
305 int height = 0;
306 };
307
308 // The child list is copy-on-write, published through an AtomicSharedPtr,
309 // and DELIBERATELY not guarded by any of the overlay locks.
310 //
311 // Readers (flushChildren(), doOverlay(), the FB subclass) run on the
312 // channel output thread, which reaches them from doOverlays() holding
313 // activeModelsLock. The writer, setChildState(), is reached from
314 // PixelOverlayModelSub::setState() -- an HTTP/command thread under
315 // modelsLock, or an effect update thread under that model's effectLock.
316 // Guarding `children` with a lock that either side already holds is what
317 // makes this area deadlock: activeModelsLock -> X on the reader against
318 // modelsLock/effectLock -> X on the writer closes a cycle with the
319 // existing modelsLock -> activeModelsLock and modelsLock -> effectLock
320 // orders (see PixelOverlayModelSub::foundParent()).
321 //
322 // Copy-on-write sidesteps the ordering question entirely: a reader takes a
323 // snapshot and iterates a list nobody can mutate or free, so it needs no
324 // lock at all. Previously readers walked the live std::list while
325 // setChildState() erased nodes out from under them, which faulted in
326 // flushChildren() on a freed node's garbage width/height.
327 //
328 // childrenWriteLock serializes only the read-copy-publish sequence in
329 // setChildState() -- writers are genuinely concurrent (HTTP vs. effect
330 // thread), so without it one update can be lost. It is a LEAF lock: it is
331 // held across nothing but list copying and the store, never across a call
332 // that could reach another lock (notably not modelStateChanged(), which
333 // takes activeModelsLock). Keep it that way and it cannot participate in
334 // a cycle.
335 typedef std::list<ChildModelState> ChildModelStateList;
336 AtomicSharedPtr<const ChildModelStateList> children;
337 std::mutex childrenWriteLock;
338
339 // A null snapshot means "no children" -- models without submodels never
340 // allocate a list.
341 129545 bool hasChildren() const {
342
1/2
✓ Branch 0 taken 129545 times.
✗ Branch 1 not taken.
129545 auto snap = children.load();
343
4/4
✓ Branch 0 taken 15954 times.
✓ Branch 1 taken 113591 times.
✓ Branch 2 taken 8845 times.
✓ Branch 3 taken 7109 times.
259090 return snap && !snap->empty();
344 129545 }
345 };
346