| 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 |