GCC Code Coverage Report


Directory: ./
File: overlays/PixelOverlay.h
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 2 3 66.7%
Functions: 1 2 50.0%
Branches: 0 0 -%

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 // - PixelOverlayManager's data layout: plugins call the inline
17 // getModelNames(), which reads modelNames at a compiled-in offset.
18 // - Signatures of the methods plugins call by symbol (getModel(), ...).
19 // Any such change must bump FPP_PLUGIN_API_VERSION in Plugin.h. Without the
20 // bump, a plugin built against the previous header still loads and silently
21 // uses the old layout - FPP 10.2 shipped exactly that and crash-looped fppd.
22 // See .claude/PLUGIN-ABI.md for which plugins use what.
23
24 #include <atomic>
25 #include <cstdint>
26 #include <vector>
27 #include "fpp-json-fwd.h"
28 #include <condition_variable>
29 #include "fpphttp_types.h"
30 #include <list>
31 #include <map>
32 #include <memory>
33 #include <mutex>
34 #include <string>
35 #include <thread>
36
37 class PixelOverlayState;
38 class PixelOverlayModel;
39 class OverlayRange;
40
41 class PixelOverlayManager {
42 public:
43 static PixelOverlayManager INSTANCE;
44
45 HttpResponsePtr render_GET(const HttpRequestPtr& req);
46 HttpResponsePtr render_POST(const HttpRequestPtr& req);
47 HttpResponsePtr render_PUT(const HttpRequestPtr& req);
48 HttpResponsePtr render_HEAD(const HttpRequestPtr& req);
49
50 bool hasActiveOverlays();
51 void doOverlays(uint8_t* channels);
52 void modelStateChanged(PixelOverlayModel*, const PixelOverlayState& old, const PixelOverlayState& state);
53
54 void addModel(Json::Value config);
55 PixelOverlayModel* getModel(const std::string& name);
56 // A reference that keeps the model alive after modelsLock is released.
57 // removeAutoOverlayModel() can free a model at any time; anything that
58 // goes on using one after unlocking must hold this, not a raw pointer.
59 std::shared_ptr<PixelOverlayModel> getModelShared(const std::string& name);
60 // Non-blocking lookup for callers that may hold a model's effectLock (the
61 // effect update threads). modelsLock -> effectLock is the sanctioned
62 // order everywhere else; blocking on modelsLock from under an effectLock
63 // is the reverse edge and deadlocks against any HTTP/command path. This
64 // try-acquires modelsLock instead: returns false with model untouched if
65 // the lock is contended (caller retries on a later call), true with the
66 // lookup result (possibly nullptr for an unknown name) otherwise. A
67 // thread already holding modelsLock always succeeds (recursive).
68 bool tryGetModel(const std::string& name, PixelOverlayModel*& model);
69
70 void addModelListener(const std::string& name, const std::string& id, std::function<void(PixelOverlayModel*)> listener);
71 void removeModelListener(const std::string& name, const std::string& id);
72
73 void Initialize();
74
75 206 bool isAutoCreatePixelOverlayModels() const {
76 206 return autoCreate;
77 }
78 // colorOrder is ignored - the model's channel order follows from channelPerNode.
79 // The parameter is retained so plugins built against older headers still link.
80 void addAutoOverlayModel(const std::string& name,
81 uint32_t startChannel, uint32_t channelCount, uint32_t channelPerNode,
82 const std::string& orientation, const std::string& startLocation,
83 uint32_t strings, uint32_t strands, const std::string& colorOrder = "RGB");
84 void removeAutoOverlayModel(const std::string& name);
85
86 const std::string& mapFont(const std::string& f);
87 // Never throws: an unparseable colour logs a warning and maps to black.
88 // Colour args arrive off MQTT topics and hand-written presets.
89 static uint32_t mapColor(const std::string& c);
90 // As mapColor(), but reports whether c parsed instead of logging.
91 static bool parseColor(const std::string& c, uint32_t& color);
92
93 void addPeriodicUpdate(int32_t initialDelayMS, PixelOverlayModel* m);
94 // Takes the model out of the update schedule and stamps any update already
95 // in flight so it cannot requeue. Does NOT wait for a running update to
96 // finish, and must not: setRunningEffect() calls this holding the model's
97 // effectLock, while an in-flight entry is registered *before*
98 // updateRunningEffects() acquires that same lock -- so waiting here would
99 // block the holder of effectLock on a thread that cannot proceed without
100 // it. Waiting is also unnecessary from there: the caller holds effectLock,
101 // so nothing else is inside updateRunningEffects() on this model, and the
102 // stamp already prevents a stale requeue.
103 void removePeriodicUpdate(PixelOverlayModel* m);
104 // As above, but also waits until no other thread is inside
105 // updateRunningEffects() on the model. Only for a caller that is about to
106 // delete it (removeAutoOverlayModel()). MUST NOT be called while holding
107 // effectLock or modelsLock -- see the note on the definition.
108 void removePeriodicUpdateAndWait(PixelOverlayModel* m);
109 // FPP no longer calls this (a model's destructor did, by name, and so
110 // reset a replacement's submodel); kept for plugins.
111 void resetChildParent(const std::string& name);
112
113 Json::Value getModelsAsJson();
114
115 Json::Value getActiveOverlayEffects();
116
117 // The list itself: only safe to walk while holding modelsLock, since an
118 // output reload removes auto-created models from it on the main loop.
119 // Kept (inline, plugin ABI) for plugins; FPP's own threads that don't
120 // hold the lock use getModelNamesSnapshot().
121 ✗ const std::list<std::string>& getModelNames() const { return modelNames; };
122 std::vector<std::string> getModelNamesSnapshot();
123
124 // Bumped whenever the set of models changes -- a (re)load of
125 // model-overlays.json, an auto-created model appearing or going away, or a
126 // lazy xLights submodel being materialized on first reference. Lets
127 // /api/models answer a conditional request without serializing every model
128 // again. It deliberately does NOT cover a model's live state or running
129 // effect, so it must not be used for /api/overlays/models, which reports
130 // both.
131 uint64_t getModelsGeneration() const { return modelsGeneration; }
132
133 private:
134 class PixelOverlayModelHolder {
135 public:
136 // `owner` holds the model; `model` is owner.get(), kept for the many
137 // callers that only use it under modelsLock.
138 PixelOverlayModel* model = nullptr;
139 std::shared_ptr<PixelOverlayModel> owner;
140 std::map<std::string, std::function<void(PixelOverlayModel*)>> listeners;
141
142 void toJson(Json::Value& json) const;
143 };
144
145 PixelOverlayManager();
146 ~PixelOverlayManager();
147
148 void ConvertCMMFileToJSON();
149 void loadModelMap();
150 void RegisterCommands();
151 PixelOverlayModel* getModelLocked(const std::string& name);
152
153 // Lazy xLights submodel support. The submodel index (name -> serialized
154 // compact config) is parsed from config/xlights-submodels.json on first
155 // reference only; individual submodels are materialized into `models` on
156 // demand via getModelLocked(). See docs/PixelOverlaySubModels.md.
157 void loadSubModelIndex();
158 PixelOverlayModel* materializeSubModelLocked(const std::string& name);
159 std::map<std::string, std::string> subModelConfigs;
160 bool subModelIndexLoaded = false;
161
162 // activeModels.size() + activeRanges.size(), set by
163 // updateActiveCountLocked() and nothing else.
164 std::atomic_int numActive;
165 void updateActiveCountLocked();
166 std::list<PixelOverlayModel*> activeModels;
167 std::list<OverlayRange> activeRanges;
168 std::recursive_mutex activeModelsLock;
169
170 std::map<std::string, PixelOverlayModelHolder> models;
171 std::list<std::string> modelNames;
172 std::map<std::string, std::string> fonts;
173 bool fontsLoaded = false;
174 std::recursive_mutex modelsLock;
175 std::atomic<uint64_t> modelsGeneration{ 0 };
176
177 void doOverlayModelEffects();
178 std::thread* updateThread = nullptr;
179 bool threadKeepRunning = true;
180 std::mutex threadLock;
181 std::condition_variable threadCV;
182 std::map<uint64_t, std::list<PixelOverlayModel*>> updates;
183 std::list<PixelOverlayModel*> afterOverlayModels;
184
185 // Models currently inside updateRunningEffects(). Both callers (the
186 // FPP-OverlayME thread and doOverlays() on the channel output thread) take
187 // the model off `updates`/`afterOverlayModels` and then drop threadLock for
188 // the duration of the call, so removing it from those lists is not enough
189 // to make deleting it safe -- removeAutoOverlayModel() would free a model
190 // that a running effect is still writing into. Everything that enters the
191 // call registers here first.
192 //
193 // `removed` is what makes the registry an invariant rather than a race.
194 // Waiting for the entry to disappear is NOT sufficient: the in-flight
195 // thread erases its entry and then requeues the model into
196 // `updates`/`afterOverlayModels` while still holding threadLock, so the
197 // waiter -- which cannot wake until that lock is released -- observes an
198 // empty registry and a model that has just been put back. It then deletes
199 // a model the next tick will dereference. Both removal variants instead
200 // stamp every matching entry, and runEffectUpdateLocked() reports
201 // EFFECT_DONE for a stamped entry so the caller never requeues. Removal is
202 // then authoritative for a model that has already started, not only for one
203 // that has not -- which is also what lets setRunningEffect() strip the
204 // model safely without waiting at all.
205 //
206 // The stamp also fixes a pre-existing duplicate: setRunningEffect() removes
207 // and re-adds the model via addPeriodicUpdate(), and without the stamp the
208 // in-flight caller requeued it again on return, ticking it at twice its
209 // period until the next removal.
210 //
211 // `tid` scopes the wait in removePeriodicUpdateAndWait() to *other*
212 // threads. Nothing reaches that variant from inside an update today, so it
213 // is not load-bearing, but it keeps the predicate honest if one ever does.
214 struct InFlight {
215 PixelOverlayModel* m;
216 std::thread::id tid;
217 bool removed = false;
218 };
219 std::list<InFlight> inFlightModels;
220 std::condition_variable inFlightCV;
221 // threadLock must be held.
222 void StampAndStripLocked(PixelOverlayModel* m);
223 void runEffectUpdateLocked(PixelOverlayModel* m, std::unique_lock<std::mutex>& l,
224 int32_t* msOut);
225
226 void loadFonts();
227
228 bool autoCreate = true;
229
230 friend class OverlayCommand;
231 // try-locks modelsLock around its parent (PixelOverlayModelSub::setState).
232 // A friend declaration changes no layout.
233 friend class PixelOverlayModelSub;
234 };
235