GCC Code Coverage Report


Directory: ./
File: effects.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 208 285 73.0%
Functions: 20 23 87.0%
Branches: 222 470 47.2%

Line Branch Exec Source
1 /*
2 * This file is part of the Falcon Player (FPP) and is Copyright (C)
3 * 2013-2022 by the Falcon Player Developers.
4 *
5 * The Falcon Player (FPP) is free software, and is covered under
6 * multiple Open Source licenses. Please see the included 'LICENSES'
7 * file for descriptions of what files are covered by each license.
8 *
9 * This source file is covered under the LGPL v2.1 as described in the
10 * included LICENSE.LGPL file.
11 */
12
13 #include "fpp-pch.h"
14
15 #include "fpp-json.h"
16
17 #include "Warnings.h" // WarningHolder -- needed directly for NOPCH builds
18
19 #include <algorithm>
20 #include <array>
21 #include <atomic>
22 #include <cstdint>
23 #include <cstring>
24 #include <fnmatch.h>
25 #include <functional>
26 #include <list>
27 #include <memory>
28 #include <mutex>
29 #include <stdio.h>
30 #include <stdlib.h>
31 #include <string>
32 #include <utility>
33 #include <vector>
34
35 #include "Events.h"
36 #include "Sequence.h"
37 #include "common.h"
38 #include "log.h"
39 #include "settings.h"
40 #include "channeloutput/channeloutputthread.h"
41 #include "commands/Commands.h" // lines 58-58
42 #include "fseq/FSEQFile.h"
43
44 #include "effects.h"
45
46 #define MAX_EFFECTS 100
47
48 class FPPeffect {
49 public:
50 148 FPPeffect() :
51 148 fp(nullptr),
52 148 currentFrame(0) {}
53 38 ~FPPeffect() {
54
1/2
✓ Branch 0 taken 38 times.
✗ Branch 1 not taken.
38 if (fp)
55
1/2
✓ Branch 0 taken 38 times.
✗ Branch 1 not taken.
38 delete fp;
56 38 }
57
58 std::string name;
59 FSEQFile* fp;
60 int loop;
61 int background;
62 uint32_t currentFrame;
63 };
64
65 static int effectCount = 0;
66 // Changed by the settings listener while the output thread reads it.
67 static std::atomic_int pauseBackgroundEffects{ 0 };
68 static std::array<FPPeffect*, MAX_EFFECTS> effects;
69 static std::list<std::pair<uint32_t, uint32_t>> clearRanges;
70 static std::mutex effectsLock;
71 // effectIDs in the order OverlayEffects() composites them: oldest (re)started
72 // first, most-recently (re)started last. Composited in this order, so the
73 // last entry here is drawn last and wins on any channel two effects share.
74 static std::vector<int> effectRenderOrder;
75
76 /*
77 * Initialize effects constructs
78 */
79 1887 int InitEffects(void) {
80
3/6
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1887 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1887 times.
✗ Branch 5 not taken.
1887 std::string localFilename(FPP_DIR_EFFECT("/background.eseq"));
81
82
3/4
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 37 times.
✓ Branch 3 taken 1850 times.
1924 if ((getFPPmode() == REMOTE_MODE) &&
83
11/22
✓ Branch 0 taken 37 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 37 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 37 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 37 times.
✓ Branch 8 taken 37 times.
✓ Branch 9 taken 1850 times.
✓ Branch 10 taken 37 times.
✓ Branch 11 taken 1850 times.
✓ Branch 12 taken 37 times.
✓ Branch 13 taken 1850 times.
✗ Branch 14 not taken.
✓ Branch 15 taken 1887 times.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
✗ Branch 20 not taken.
✗ Branch 21 not taken.
1966 CheckForHostSpecificFile(getSetting("HostName").c_str(), localFilename)) {
84 ✗ localFilename = "background_";
85 ✗ localFilename += getSetting("HostName");
86
87 ✗ LogInfo(VB_EFFECT, "Automatically starting background effect "
88 "sequence %s\n",
89 localFilename.c_str());
90
91 ✗ StartEffect(localFilename.c_str(), 0, 1, true);
92
3/4
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 1886 times.
1887 } else if (FileExists(localFilename)) {
93
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogInfo(VB_EFFECT, "Automatically starting background effect sequence "
94 "background.eseq\n");
95
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 StartEffect("background", 0, 1, true);
96 }
97
98
1/2
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
1887 pauseBackgroundEffects = getSettingInt("pauseBackgroundEffects");
99
3/6
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1887 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1887 times.
✗ Branch 5 not taken.
9115 registerSettingsListener("effects", "pauseBackgroundEffects",
100 160 [](const std::string& value) {
101 6 LogDebug(VB_SETTING, "Setting pauseBackgroundEffects to %s\n", value.c_str());
102 6 pauseBackgroundEffects = getSettingInt("pauseBackgroundEffects");
103 6 });
104
105 std::function<void(const std::string&, const std::string&)>
106 1887 effect_callback = [](const std::string& topic_in,
107 const std::string& payload) {
108 ✗ LogDebug(VB_CONTROL, "System Callback for %s\n", topic_in.c_str());
109
110 ✗ if (0 == topic_in.compare(topic_in.length() - 5, 5, "/stop")) {
111 ✗ if (payload == "") {
112 ✗ StopAllEffects();
113 } else {
114 ✗ StopEffect(payload.c_str());
115 }
116 ✗ } else if (0 == topic_in.compare(topic_in.length() - 6, 6, "/start")) {
117 ✗ StartEffect(payload.c_str(), 0);
118 ✗ } else if (0 == topic_in.compare(topic_in.length() - 8, 8, "/startbg")) {
119 ✗ StartEffect(payload.c_str(), 0, 0, true);
120 }
121 1887 };
122
123
2/4
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1887 times.
✗ Branch 3 not taken.
1887 Events::AddCallback("/set/effect/#", effect_callback);
124
125 1887 return 1;
126 1887 }
127
128 /*
129 * Close effects constructs
130 */
131 1867 void CloseEffects(void) {
132 1867 }
133
134 /*
135 * Get next available effect ID
136 *
137 * Assumes effectsLock is held already
138 */
139 148 int GetNextEffectID(void) {
140 148 int i = -1;
141
142
1/2
✓ Branch 0 taken 5109 times.
✗ Branch 1 not taken.
5109 for (i = 0; i < MAX_EFFECTS; i++) {
143
2/2
✓ Branch 0 taken 148 times.
✓ Branch 1 taken 4961 times.
5109 if (!effects[i])
144 148 return i;
145 }
146
147 ✗ return -1;
148 }
149
150 /*
151 * Check to see if any effects are running
152 */
153 338955 int IsEffectRunning(void) {
154 338955 int result = 0;
155
1/2
✓ Branch 0 taken 338955 times.
✗ Branch 1 not taken.
338955 std::unique_lock<std::mutex> lock(effectsLock);
156 338955 result = effectCount;
157
2/2
✓ Branch 0 taken 62 times.
✓ Branch 1 taken 338893 times.
338955 if (!clearRanges.empty()) {
158 62 ++result;
159 }
160 338955 return result;
161 338955 }
162
163 /*
164 * Assumes effectsLock is already held. Moves effectID to the most-recent end
165 * of effectRenderOrder (the back of the vector, since OverlayEffects() walks
166 * it front-to-back and composites last = wins), inserting it if not already
167 * present. Used by both a fresh StartEffect() allocation and a
168 * RestartEffect() re-trigger of an existing slot, so that whichever effect
169 * started or restarted most recently visually wins regardless of its array
170 * slot.
171 */
172 149 static void BringEffectToFront(int effectID) {
173
1/2
✓ Branch 0 taken 149 times.
✗ Branch 1 not taken.
149 auto it = std::find(effectRenderOrder.begin(), effectRenderOrder.end(), effectID);
174
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 148 times.
149 if (it != effectRenderOrder.end()) {
175
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 effectRenderOrder.erase(it);
176 }
177
1/2
✓ Branch 0 taken 149 times.
✗ Branch 1 not taken.
149 effectRenderOrder.push_back(effectID);
178 149 }
179
180 /*
181 * Assumes effectsLock is already held. Removes effectID from the render order if
182 * present.
183 */
184 38 static void RemoveEffectFromRenderOrder(int effectID) {
185
1/2
✓ Branch 0 taken 38 times.
✗ Branch 1 not taken.
38 auto it = std::find(effectRenderOrder.begin(), effectRenderOrder.end(), effectID);
186
1/2
✓ Branch 0 taken 38 times.
✗ Branch 1 not taken.
38 if (it != effectRenderOrder.end()) {
187
1/2
✓ Branch 0 taken 38 times.
✗ Branch 1 not taken.
38 effectRenderOrder.erase(it);
188 }
189 38 }
190
191 198 int StartEffect(FSEQFile* fseq, const std::string& effectName, int loop, bool bg) {
192
1/2
✓ Branch 0 taken 198 times.
✗ Branch 1 not taken.
198 std::unique_lock<std::mutex> lock(effectsLock);
193
2/2
✓ Branch 0 taken 50 times.
✓ Branch 1 taken 148 times.
198 if (effectCount >= MAX_EFFECTS) {
194
1/2
✓ Branch 0 taken 50 times.
✗ Branch 1 not taken.
50 LogErr(VB_EFFECT, "Unable to start effect %s, maximum number of effects already running\n", effectName.c_str());
195 // This function takes ownership of fseq -- the effectID failure below
196 // deletes it, and so does StopEffect. Returning here without deleting
197 // leaked the whole FSEQFile, mapping and all, on every rejected start,
198 // which is exactly when a controller is already at the ceiling and
199 // retrying: one report leaked 48 of them before running out of address
200 // space to create the next read-ahead thread in.
201
1/2
✓ Branch 0 taken 50 times.
✗ Branch 1 not taken.
50 delete fseq;
202 50 return -1;
203 }
204 148 int effectID = -1;
205 148 int frameTime = 50;
206
207 148 frameTime = fseq->getStepTime();
208
1/2
✓ Branch 0 taken 148 times.
✗ Branch 1 not taken.
148 effectID = GetNextEffectID();
209
210
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 148 times.
148 if (effectID < 0) {
211 ✗ LogErr(VB_EFFECT, "Unable to start effect %s, unable to determine next effect ID\n", effectName.c_str());
212 ✗ delete fseq;
213 ✗ return effectID;
214 }
215
216
2/4
✓ Branch 0 taken 148 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 148 times.
148 effects[effectID] = new FPPeffect;
217
1/2
✓ Branch 0 taken 148 times.
✗ Branch 1 not taken.
148 effects[effectID]->name = effectName;
218 148 effects[effectID]->fp = fseq;
219 148 effects[effectID]->loop = loop;
220 148 effects[effectID]->background = bg;
221
1/2
✓ Branch 0 taken 148 times.
✗ Branch 1 not taken.
148 BringEffectToFront(effectID);
222
223 148 effectCount++;
224 148 int tmpec = effectCount;
225
1/2
✓ Branch 0 taken 148 times.
✗ Branch 1 not taken.
148 lock.unlock();
226
227
1/2
✓ Branch 0 taken 148 times.
✗ Branch 1 not taken.
148 StartChannelOutputThread();
228
229
7/8
✓ Branch 0 taken 148 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 143 times.
✓ Branch 3 taken 5 times.
✓ Branch 4 taken 33 times.
✓ Branch 5 taken 110 times.
✓ Branch 6 taken 33 times.
✓ Branch 7 taken 115 times.
148 if (!sequence->IsSequenceRunning() && tmpec == 1) {
230 // first effect running, no sequence running, set the refresh rate
231 // to the rate of the effect
232
1/2
✓ Branch 0 taken 33 times.
✗ Branch 1 not taken.
33 SetChannelOutputRefreshRate(1000 / frameTime);
233 }
234
235 148 return effectID;
236 198 }
237
238 157 int StartFSEQAsEffect(const std::string& fseqName, int loop, bool bg) {
239
1/2
✓ Branch 0 taken 157 times.
✗ Branch 1 not taken.
157 LogInfo(VB_EFFECT, "Starting FSEQ %s as effect\n", fseqName.c_str());
240
241
5/10
✓ Branch 0 taken 157 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 157 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 157 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 157 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 157 times.
✗ Branch 9 not taken.
157 std::string filename = FPP_DIR_SEQUENCE("/" + fseqName + ".fseq");
242
243
1/2
✓ Branch 0 taken 157 times.
✗ Branch 1 not taken.
157 FSEQFile* fseq = FSEQFile::openFSEQFile(filename);
244
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 157 times.
157 if (!fseq) {
245 ✗ LogErr(VB_EFFECT, "Unable to open effect: %s\n", filename.c_str());
246 ✗ WarningHolder::AddWarningTimeout(60, 49, "Effect could not be started: cannot open " + filename);
247 ✗ return -1;
248 }
249
1/2
✓ Branch 0 taken 157 times.
✗ Branch 1 not taken.
157 return StartEffect(fseq, fseqName, loop, bg);
250 157 }
251
252 /*
253 * Start a new effect offset at the specified channel number
254 */
255 47 int StartEffect(const std::string& effectName, int startChannel, int loop, bool bg) {
256
1/2
✓ Branch 0 taken 47 times.
✗ Branch 1 not taken.
47 LogInfo(VB_EFFECT, "Starting effect %s at channel %d\n", effectName.c_str(), startChannel);
257
258
5/10
✓ Branch 0 taken 47 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 47 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 47 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 47 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 47 times.
✗ Branch 9 not taken.
47 std::string filename = FPP_DIR_EFFECT("/" + effectName + ".eseq");
259
260
1/2
✓ Branch 0 taken 47 times.
✗ Branch 1 not taken.
47 FSEQFile* fseq = FSEQFile::openFSEQFile(filename);
261
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 41 times.
47 if (!fseq) {
262
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 LogErr(VB_EFFECT, "Unable to open effect: %s\n", filename.c_str());
263
3/6
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 6 times.
✗ Branch 5 not taken.
6 WarningHolder::AddWarningTimeout(60, 49, "Effect could not be started: cannot open " + filename);
264 6 return -1;
265 }
266
1/2
✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
41 V2FSEQFile* v2fseq = dynamic_cast<V2FSEQFile*>(fseq);
267
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 41 times.
41 if (!v2fseq) {
268 ✗ delete fseq;
269 ✗ LogErr(VB_EFFECT, "Effect file not a correct eseq file: %s\n", filename.c_str());
270 ✗ WarningHolder::AddWarningTimeout(60, 49, "Effect could not be started: " + filename + " is not a valid eseq file");
271 ✗ return -1;
272 }
273
274
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 41 times.
41 if (v2fseq->m_sparseRanges.size() == 0) {
275 ✗ LogErr(VB_EFFECT, "eseq file must have at least one model range.");
276 ✗ WarningHolder::AddWarningTimeout(60, 49, "Effect could not be started: " + filename + " has no model range");
277 ✗ delete fseq;
278 ✗ return -1;
279 }
280
281
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 40 times.
41 if (startChannel != 0) {
282 // This will need to change if/when we support multiple models per file
283 1 v2fseq->m_sparseRanges[0].first = startChannel - 1;
284 }
285
1/2
✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
41 return StartEffect(v2fseq, effectName, loop, bg);
286 47 }
287
288 /*
289 * Queue a channel range to be zeroed by OverlayEffects(). The ranges come
290 * from the effect file's header (or a caller-supplied start channel), so they
291 * are clamped to the channel buffer here: readFrame() already ignores a range
292 * past FPPD_MAX_CHANNELS, but the memset() that clears it would not. Assumes
293 * effectsLock is already held.
294 */
295 66 static void AddClearRange(uint32_t start, uint32_t len) {
296
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 62 times.
66 if (start >= FPPD_MAX_CHANNELS) {
297 4 return;
298 }
299 62 len = std::min(len, (uint32_t)FPPD_MAX_CHANNELS - start);
300
2/2
✓ Branch 0 taken 58 times.
✓ Branch 1 taken 4 times.
62 if (len) {
301
1/2
✓ Branch 0 taken 58 times.
✗ Branch 1 not taken.
58 clearRanges.push_back(std::pair<uint32_t, uint32_t>(start, len));
302 }
303 }
304
305 /*
306 * Helper function to stop an effect, assumes effectsLock is already held
307 */
308 38 void StopEffectHelper(int effectID) {
309 38 FPPeffect* e = NULL;
310 // Slots are validated by all callers, but effect IDs also arrive parsed
311 // from sequence tracks and command arguments -- never trust the index.
312
2/4
✓ Branch 0 taken 38 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 38 times.
38 if (effectID < 0 || effectID >= MAX_EFFECTS) {
313 ✗ return;
314 }
315 38 e = effects[effectID];
316
317
1/2
✓ Branch 0 taken 38 times.
✗ Branch 1 not taken.
38 if (e->fp) {
318
1/2
✓ Branch 0 taken 38 times.
✗ Branch 1 not taken.
38 V2FSEQFile* v2fseq = dynamic_cast<V2FSEQFile*>(e->fp);
319
5/6
✓ Branch 0 taken 38 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 33 times.
✓ Branch 3 taken 5 times.
✓ Branch 4 taken 33 times.
✓ Branch 5 taken 5 times.
38 if (v2fseq && v2fseq->m_sparseRanges.size() != 0) {
320
2/2
✓ Branch 0 taken 33 times.
✓ Branch 1 taken 33 times.
66 for (auto& a : v2fseq->m_sparseRanges) {
321
1/2
✓ Branch 0 taken 33 times.
✗ Branch 1 not taken.
33 AddClearRange(a.first, a.second);
322 }
323
2/2
✓ Branch 0 taken 28 times.
✓ Branch 1 taken 33 times.
61 for (auto& a : v2fseq->m_rangesToRead) {
324
1/2
✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
28 AddClearRange(a.first, a.second);
325 }
326 } else {
327 // not sparse and not eseq, entire range
328 5 AddClearRange(0, e->fp->getChannelCount());
329 }
330 }
331
1/2
✓ Branch 0 taken 38 times.
✗ Branch 1 not taken.
38 delete e;
332 38 effects[effectID] = NULL;
333 38 effectCount--;
334 38 RemoveEffectFromRenderOrder(effectID);
335 }
336
337 /*
338 * Stop all effects named effectName
339 */
340 24 int StopEffect(const std::string& effectName) {
341
1/2
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
24 LogDebug(VB_EFFECT, "StopEffect(%s)\n", effectName.c_str());
342
343
1/2
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
24 std::unique_lock<std::mutex> lock(effectsLock);
344
1/2
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
24 std::vector<std::string> names = split(effectName, ',');
345
346
2/2
✓ Branch 0 taken 25 times.
✓ Branch 1 taken 24 times.
49 for (int j = 0; j < names.size(); j++) {
347
2/2
✓ Branch 0 taken 2500 times.
✓ Branch 1 taken 25 times.
2525 for (int i = 0; i < MAX_EFFECTS; i++) {
348
2/2
✓ Branch 0 taken 27 times.
✓ Branch 1 taken 2473 times.
2500 if (effects[i]) {
349
4/4
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 19 times.
✓ Branch 2 taken 21 times.
✓ Branch 3 taken 6 times.
35 if ((effects[i]->name == names[j]) ||
350
3/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 6 times.
8 (!fnmatch(names[j].c_str(), effects[i]->name.c_str(), FNM_PATHNAME))) {
351
1/2
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
21 StopEffectHelper(i);
352 }
353 }
354 }
355 }
356
357
1/2
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
24 lock.unlock();
358
359
5/6
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✓ Branch 3 taken 18 times.
✓ Branch 4 taken 6 times.
✓ Branch 5 taken 18 times.
30 if ((!IsEffectRunning()) &&
360
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
6 (!sequence->IsSequenceRunning())) {
361
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 sequence->SendBlankingData();
362 }
363
364 24 return 1;
365 24 }
366
367 /*
368 * Stop a single effect
369 */
370 ✗ int StopEffect(int effectID) {
371 ✗ FPPeffect* e = NULL;
372
373 ✗ LogDebug(VB_EFFECT, "StopEffect(%d)\n", effectID);
374
375 ✗ std::unique_lock<std::mutex> lock(effectsLock);
376 ✗ if (effectID < 0 || effectID >= MAX_EFFECTS || !effects[effectID]) {
377 ✗ return 0;
378 }
379
380 ✗ StopEffectHelper(effectID);
381 ✗ lock.unlock();
382
383 ✗ if ((!IsEffectRunning()) &&
384 ✗ (!sequence->IsSequenceRunning())) {
385 ✗ sequence->SendBlankingData();
386 }
387
388 ✗ return 1;
389 ✗ }
390
391 /*
392 * Restart a single effect in place, i.e. jump it back to frame 0 without
393 * closing and reopening the underlying FSEQFile. This reuses the exact same
394 * mechanism OverlayEffect() already uses to wrap a looping effect back to
395 * the start (effects.cpp: e->currentFrame = 0), so it needs no new file
396 * open, no new read-ahead thread, and no teardown of the one already
397 * running - unlike a stop+start, it can't add to the concurrent-thread
398 * pressure that caused the crashes in issue #2815.
399 *
400 * Matches on both ID and name: effectID slots are recycled, so a caller
401 * that remembered an ID from an earlier point in time could otherwise end
402 * up restarting an unrelated effect that has since taken over that slot.
403 * Requiring the name to also match makes that mismatch a safe no-op.
404 */
405 1 int RestartEffect(int effectID, const std::string& effectName) {
406
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogDebug(VB_EFFECT, "RestartEffect(%d, %s)\n", effectID, effectName.c_str());
407
408
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 std::unique_lock<std::mutex> lock(effectsLock);
409
5/10
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 1 times.
✗ Branch 8 not taken.
✓ Branch 9 taken 1 times.
1 if (effectID < 0 || effectID >= MAX_EFFECTS || !effects[effectID] || effects[effectID]->name != effectName) {
410 ✗ return 0;
411 }
412
413 1 effects[effectID]->currentFrame = 0;
414
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 BringEffectToFront(effectID);
415 1 return 1;
416 1 }
417
418 /*
419 * Stop all effects
420 */
421 7 void StopAllEffects(void) {
422 int i;
423
424
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 LogDebug(VB_EFFECT, "Stopping all effects\n");
425
426
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 std::unique_lock<std::mutex> lock(effectsLock);
427
428
2/2
✓ Branch 0 taken 700 times.
✓ Branch 1 taken 7 times.
707 for (i = 0; i < MAX_EFFECTS; i++) {
429
2/2
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 693 times.
700 if (effects[i])
430
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 StopEffectHelper(i);
431 }
432
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 lock.unlock();
433
3/6
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 7 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 7 times.
7 if ((!IsEffectRunning()) &&
434 ✗ (!sequence->IsSequenceRunning()))
435 ✗ sequence->SendBlankingData();
436 7 }
437
438 /*
439 * Overlay a single effect onto raw channel data
440 */
441 2450 int OverlayEffect(int effectID, char* channelData) {
442 2450 FPPeffect* e = NULL;
443
4/8
✓ Branch 0 taken 2450 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2450 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 2450 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 2450 times.
2450 if (effectID < 0 || effectID >= MAX_EFFECTS || !effects[effectID]) {
444 ✗ LogErr(VB_EFFECT, "Invalid Effect ID %d\n", effectID);
445 ✗ return 0;
446 }
447
448 2450 e = effects[effectID];
449 2450 FSEQFile::FrameData* d = e->fp->getFrame(e->currentFrame);
450
4/4
✓ Branch 0 taken 25 times.
✓ Branch 1 taken 2425 times.
✓ Branch 2 taken 17 times.
✓ Branch 3 taken 8 times.
2450 if (d == nullptr && e->loop) {
451 17 e->currentFrame = 0;
452 17 d = e->fp->getFrame(e->currentFrame);
453 }
454 2450 e->currentFrame++;
455
2/2
✓ Branch 0 taken 2440 times.
✓ Branch 1 taken 10 times.
2450 if (d) {
456 2440 d->readFrame((uint8_t*)channelData, FPPD_MAX_CHANNELS);
457
1/2
✓ Branch 0 taken 2440 times.
✗ Branch 1 not taken.
2440 delete d;
458 2440 return 1;
459 } else {
460 10 StopEffectHelper(effectID);
461
2/2
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 10 times.
20 for (auto& rng : clearRanges) {
462 10 memset(&channelData[rng.first], 0, rng.second);
463 }
464 10 clearRanges.clear();
465 }
466
467 10 return 0;
468 }
469
470 /*
471 * Overlay current effects onto raw channel data
472 */
473 1033 int OverlayEffects(char* channelData) {
474 1033 int dataRead = 0;
475
476
1/2
✓ Branch 0 taken 1033 times.
✗ Branch 1 not taken.
1033 std::unique_lock<std::mutex> lock(effectsLock);
477
478 // for effects that have been stopped, we need to clear the data
479
2/2
✓ Branch 0 taken 42 times.
✓ Branch 1 taken 1033 times.
1075 for (auto& rng : clearRanges) {
480 42 memset(&channelData[rng.first], 0, rng.second);
481 }
482 1033 clearRanges.clear();
483
484
2/2
✓ Branch 0 taken 19 times.
✓ Branch 1 taken 1014 times.
1033 if (effectCount == 0) {
485 19 return 0;
486 }
487
488 1014 int skipBackground = 0;
489
6/8
✓ Branch 0 taken 20 times.
✓ Branch 1 taken 994 times.
✓ Branch 2 taken 20 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 20 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 20 times.
✓ Branch 7 taken 994 times.
1014 if (pauseBackgroundEffects && sequence->IsSequenceRunning()) {
490 20 skipBackground = 1;
491 }
492
493 // Snapshot into a reused scratch buffer (not a fresh local) so this
494 // doesn't malloc/free every frame: OverlayEffect() below can call
495 // StopEffectHelper() -> RemoveEffectFromRenderOrder() on natural EOF,
496 // mutating the live list mid-loop, so we still need a copy to iterate
497 // safely - just not a newly-allocated one each time. Safe as `static`
498 // even though OverlayEffects() can be entered from more than one thread
499 // (normally RunChannelOutputThread, but also directly from whatever
500 // thread calls Sequence::SendBlankingData() when the channel output
501 // thread isn't running) because the whole function body, including
502 // every access to `order`, runs under effectsLock - concurrent calls
503 // are serialized by the mutex, not by single-thread ownership.
504
3/4
✓ Branch 0 taken 23 times.
✓ Branch 1 taken 991 times.
✓ Branch 2 taken 23 times.
✗ Branch 3 not taken.
1014 static std::vector<int> order;
505
1/2
✓ Branch 0 taken 1014 times.
✗ Branch 1 not taken.
1014 order = effectRenderOrder;
506
2/2
✓ Branch 0 taken 2470 times.
✓ Branch 1 taken 1014 times.
3484 for (int id : order) {
507
1/2
✓ Branch 0 taken 2470 times.
✗ Branch 1 not taken.
2470 if (effects[id]) {
508
5/6
✓ Branch 0 taken 33 times.
✓ Branch 1 taken 2437 times.
✓ Branch 2 taken 33 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2450 times.
✓ Branch 5 taken 20 times.
2503 if ((!skipBackground) ||
509
2/2
✓ Branch 0 taken 13 times.
✓ Branch 1 taken 20 times.
33 (skipBackground && (!effects[id]->background))) {
510
1/2
✓ Branch 0 taken 2450 times.
✗ Branch 1 not taken.
2450 dataRead |= OverlayEffect(id, channelData);
511 }
512 }
513 }
514
515
1/2
✓ Branch 0 taken 1014 times.
✗ Branch 1 not taken.
1014 lock.unlock();
516
517 1028 if ((dataRead == 0) &&
518
7/8
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 1000 times.
✓ Branch 2 taken 14 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 7 times.
✓ Branch 5 taken 7 times.
✓ Branch 6 taken 7 times.
✓ Branch 7 taken 1007 times.
1021 (!IsEffectRunning()) &&
519
2/4
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
7 (!sequence->IsSequenceRunning())) {
520
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 sequence->SendBlankingData();
521 }
522
523 1014 return 1;
524 1033 }
525
526 243 Json::Value GetRunningEffectsJson() {
527
1/2
✓ Branch 0 taken 243 times.
✗ Branch 1 not taken.
243 Json::Value arr(Json::arrayValue);
528 int i;
529
1/2
✓ Branch 0 taken 243 times.
✗ Branch 1 not taken.
243 std::unique_lock<std::mutex> lock(effectsLock);
530
531
2/2
✓ Branch 0 taken 24300 times.
✓ Branch 1 taken 243 times.
24543 for (i = 0; i < MAX_EFFECTS; i++) {
532
2/2
✓ Branch 0 taken 559 times.
✓ Branch 1 taken 23741 times.
24300 if (effects[i]) {
533
1/2
✓ Branch 0 taken 559 times.
✗ Branch 1 not taken.
559 Json::Value obj;
534
2/4
✓ Branch 0 taken 559 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 559 times.
✗ Branch 3 not taken.
559 obj["id"] = i;
535
2/4
✓ Branch 0 taken 559 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 559 times.
✗ Branch 3 not taken.
559 obj["name"] = effects[i]->name;
536
1/2
✓ Branch 0 taken 559 times.
✗ Branch 1 not taken.
559 arr.append(obj);
537 559 }
538 }
539
540 243 return arr;
541 243 }
542
543 /*
544 * Get list of running effects and their IDs
545 *
546 * Format: [EFFECTID1,EFFECTNAME1[,EFFECTID2,EFFECTNAME2]...]
547 *
548 * NOTE: Caller is responsible for freeing string allocated
549 */
550 ✗ int GetRunningEffects(char* msg, char** result) {
551 ✗ int length = strlen(msg) + 2; // 1 for LF, 1 for NULL termination
552 ✗ int i = 0;
553
554 ✗ std::unique_lock<std::mutex> lock(effectsLock);
555
556 ✗ for (i = 0; i < MAX_EFFECTS; i++) {
557 ✗ if (effects[i]) {
558 // delimiters
559 ✗ length += 2;
560
561 // ID
562 ✗ length++;
563 ✗ if (i > 9)
564 ✗ length++;
565 ✗ if (i > 99)
566 ✗ length++;
567
568 // Name
569 ✗ length += strlen(effects[i]->name.c_str());
570 }
571 }
572
573 ✗ *result = (char*)malloc(length);
574 ✗ char* cptr = *result;
575 ✗ *cptr = '\0';
576
577 ✗ strcat(cptr, msg);
578 ✗ cptr += strlen(msg);
579
580 ✗ for (i = 0; i < MAX_EFFECTS; i++) {
581 ✗ if (effects[i]) {
582 ✗ strcat(cptr, ";");
583 ✗ cptr++;
584
585 ✗ cptr += snprintf(cptr, 4, "%d", i);
586
587 ✗ strcat(cptr, ",");
588 ✗ cptr++;
589
590 ✗ strcat(cptr, effects[i]->name.c_str());
591 ✗ cptr += strlen(effects[i]->name.c_str());
592 }
593 }
594
595 ✗ strcat(cptr, "\n");
596 ✗ return strlen(*result);
597 ✗ }
598