GCC Code Coverage Report


Directory: ./
File: overlays/PixelOverlayModel.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 706 976 72.3%
Functions: 55 62 88.7%
Branches: 731 1424 51.3%

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 <sys/stat.h>
16 #include <atomic>
17 #include <unordered_map>
18
19 #include "fpp-json.h"
20 #include "Warnings.h"
21
22 #if __has_include(<sys/posix_shm.h>)
23 #include <sys/posix_shm.h>
24 #else
25 #include <limits.h>
26 #define PSHMNAMLEN NAME_MAX
27 #endif
28
29 #include <Magick++/Blob.h>
30 #include <Magick++/Color.h>
31 #include <Magick++/Geometry.h>
32 #include <Magick++/Image.h>
33 #include <sys/mman.h>
34 #include <sys/time.h>
35 #include <algorithm>
36 #include <cstdint>
37 #include <errno.h>
38 #include <fcntl.h>
39 #include <limits.h>
40 #include <list>
41 #include <set>
42 #include <memory>
43 #include <mutex>
44 #include <stdio.h>
45 #include <stdlib.h>
46 #include <string.h>
47 #include <string>
48 #include <time.h>
49 #include <unistd.h>
50 #include <utility>
51 #include <vector>
52
53 #include "../Plugins.h"
54 #include "../Sequence.h"
55 #include "../commands/Commands.h"
56 #include "../common.h"
57 #include "../effects.h"
58 #include "../log.h"
59 #include "../settings.h"
60
61 #include "PixelOverlay.h"
62 #include "PixelOverlayEffects.h"
63 #include "PixelOverlayModel.h"
64 #include "PixelOverlayModelSub.h"
65 #include "../channeloutput/channeloutputthread.h"
66
67 // The channel order a model reads and writes. This is the order of fppd's
68 // channel data, not of the wire: setPixelValue() stores r, then g, then b,
69 // then w into consecutive channels, and the output driver is what permutes
70 // those into the hardware's colour order. It therefore follows entirely from
71 // how many channels a node occupies.
72 1433 static const char* ChannelDataOrder(int channelsPerNode) {
73
2/4
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✓ Branch 2 taken 1419 times.
✓ Branch 3 taken 14 times.
1433 switch (channelsPerNode) {
74 ✗ case 1:
75 ✗ return "W";
76 ✗ case 2:
77 ✗ return "RG";
78 1419 case 3:
79 1419 return "RGB";
80 14 default:
81 14 return "RGBW";
82 }
83 }
84
85 // Both of a model's buffers (the channel data and the overlay buffer) are
86 // allocated the same way and with the same fallbacks, so they share this.
87 //
88 // The buffer is shared memory when it can be, because external programs read
89 // it; when it cannot be, a private anonymous mapping is just as good to us.
90 // Each step is checked, and the caller is told which one won so it can release
91 // the buffer the way it was allocated -- munmap() on a malloc()ed pointer is
92 // not a no-op.
93 //
94 // Returns nullptr only if every allocator failed, which means the box is out of
95 // memory. Callers must handle that: a model with no buffer draws nothing, which
96 // is survivable, and the alternative here used to be a wild write through a
97 // null pointer.
98 1545 static uint8_t* createModelMemory(const std::string& dataName, uint32_t size, bool& mapped) {
99 1545 mapped = true;
100
2/2
✓ Branch 0 taken 232 times.
✓ Branch 1 taken 1313 times.
1545 if (size < 8) {
101 // cannot mmap 0 size which some models happen to have
102 232 size = 8;
103 }
104
105 1545 mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;
106 // No name: private memory, for a model that has given its names up.
107
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1545 times.
1545 int f = dataName.empty() ? -1 : shm_open(dataName.c_str(), O_RDWR | O_CREAT, mode);
108
2/2
✓ Branch 0 taken 1543 times.
✓ Branch 1 taken 2 times.
1545 if (f != -1) {
109 1543 int rc = ftruncate(f, size);
110
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1543 times.
1543 if (rc == -1) {
111 // if ftruncate fails, we need to completely reset
112 ✗ close(f);
113 ✗ shm_unlink(dataName.c_str());
114 ✗ f = shm_open(dataName.c_str(), O_RDWR | O_CREAT, mode);
115 ✗ if (f != -1 && ftruncate(f, size) == -1) {
116 ✗ close(f);
117 ✗ f = -1;
118 }
119 }
120 }
121
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1543 times.
1545 if (f == -1) {
122
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (!dataName.empty()) {
123 2 LogWarn(VB_CHANNELOUT, "Could not create shared memory block for %s: %s\n",
124 dataName.c_str(), FPPstrerror(errno));
125 }
126 } else {
127 1543 uint8_t* data = (uint8_t*)mmap(0, size, PROT_READ | PROT_WRITE, MAP_SHARED, f, 0);
128 1543 close(f);
129
1/2
✓ Branch 0 taken 1543 times.
✗ Branch 1 not taken.
1543 if (data != MAP_FAILED) {
130 1543 return data;
131 }
132 ✗ LogWarn(VB_CHANNELOUT, "Could not mmap shared memory for %s: %s\n",
133 dataName.c_str(), FPPstrerror(errno));
134 ✗ shm_unlink(dataName.c_str());
135 }
136
137 // Private memory instead: external programs lose their view of this model,
138 // but fppd itself works normally. MAP_ANON has to be paired with one of
139 // MAP_PRIVATE/MAP_SHARED -- on its own mmap() returns EINVAL, so for as
140 // long as that pairing was missing this fallback could not succeed and
141 // every failure here landed on the unchecked malloc() below.
142 2 uint8_t* data = (uint8_t*)mmap(0, size, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, -1, 0);
143
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (data != MAP_FAILED) {
144 2 return data;
145 }
146
147 ✗ mapped = false;
148 ✗ data = (uint8_t*)malloc(size);
149 ✗ if (data) {
150 // mmap() hands back zeroed pages and callers rely on that; malloc() does not.
151 ✗ memset(data, 0, size);
152 } else {
153 ✗ LogErr(VB_CHANNELOUT, "Out of memory allocating %u bytes for %s\n", size, dataName.c_str());
154 }
155 ✗ return data;
156 }
157
158 // Serializes the lazy overlay buffer allocation (see getOverlayBuffer()) and
159 // guards s_namesReleased.
160 static std::mutex s_overlayBufferAllocLock;
161 // Models that have given up their shm names (releaseSharedMemoryNames()). A
162 // member would change the plugin-visible layout. The destructor erases its
163 // entry, so a new model at the same address starts with its names.
164 static std::set<const PixelOverlayModel*> s_namesReleased;
165
166 // The shm name for a model buffer, shortened where the system's limit demands it.
167 2086 static std::string modelMemoryName(const std::string& prefix, const std::string& shortPrefix,
168 const std::string& name) {
169 if (PSHMNAMLEN > 48) {
170
1/2
✓ Branch 0 taken 2086 times.
✗ Branch 1 not taken.
2086 return prefix + name;
171 }
172 // system doesn't allow very long shared memory names, we'll use a shortened form
173 std::string n = shortPrefix + name;
174 if (n.size() > PSHMNAMLEN) {
175 n = n.substr(0, PSHMNAMLEN);
176 }
177 return n;
178 }
179
180 // Model definitions come from model-overlays.json, which POST /api/models
181 // (xLights' FPP Connect) writes unchecked, and from xlights-submodels.json.
182 // jsoncpp's asInt()/asString() throw on the wrong type, and nothing above the
183 // constructor catches - at boot that was a crash on every start. These read a
184 // field the way a hand-edited file might hold it (a number, or a number in a
185 // string) and report anything else instead of throwing.
186 11540 bool PixelOverlayModel::configInt(const Json::Value& c, const char* key, int dflt, int& out) {
187
5/6
✓ Branch 0 taken 7075 times.
✓ Branch 1 taken 4465 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 7075 times.
✓ Branch 4 taken 4465 times.
✓ Branch 5 taken 7075 times.
11540 if (!c.isMember(key) || c[key].isNull()) {
188 4465 out = dflt;
189 4465 return true;
190 }
191 7075 const Json::Value& v = c[key];
192
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 7075 times.
7075 if (v.isBool()) {
193 ✗ out = v.asBool() ? 1 : 0;
194 ✗ return true;
195 }
196
2/2
✓ Branch 0 taken 7074 times.
✓ Branch 1 taken 1 times.
7075 if (v.isNumeric()) {
197 7074 double d = v.asDouble();
198
2/4
✓ Branch 0 taken 7074 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 7074 times.
7074 if (d < INT_MIN || d > INT_MAX) {
199 ✗ return false;
200 }
201 7074 out = (int)d;
202 7074 return true;
203 }
204
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (v.isString()) {
205
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 std::string str = v.asString();
206
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (str.empty()) {
207 ✗ out = dflt;
208 ✗ return true;
209 }
210 1 char* end = nullptr;
211 1 errno = 0;
212 1 long l = strtol(str.c_str(), &end, 10);
213
2/8
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
1 if (errno || *end != '\0' || l < INT_MIN || l > INT_MAX) {
214 1 return false;
215 }
216 ✗ out = (int)l;
217 ✗ return true;
218 1 }
219 ✗ return false;
220 }
221 7245 bool PixelOverlayModel::configString(const Json::Value& c, const char* key, std::string& out) {
222
5/6
✓ Branch 0 taken 5644 times.
✓ Branch 1 taken 1601 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 5644 times.
✓ Branch 4 taken 1601 times.
✓ Branch 5 taken 5644 times.
7245 if (!c.isMember(key) || c[key].isNull()) {
223 1601 out.clear();
224 1601 return true;
225 }
226 5644 const Json::Value& v = c[key];
227
5/6
✓ Branch 0 taken 5643 times.
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 5643 times.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 5643 times.
5644 if (v.isObject() || v.isArray()) {
228 1 return false;
229 }
230
1/2
✓ Branch 0 taken 5643 times.
✗ Branch 1 not taken.
5643 out = v.asString();
231 5643 return true;
232 }
233
234 // Logs why and releases channelData, so isValid() is false and the manager
235 // drops the model. For constructors only.
236 7 void PixelOverlayModel::markInvalid(const std::string& why) {
237 7 LogErr(VB_CHANNELOUT, "Overlay model '%s' is not usable: %s\n", name.c_str(), why.c_str());
238
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 7 times.
7 if (channelData) {
239 ✗ if (channelDataMapped) {
240 ✗ munmap(channelData, std::max(channelCount, 8));
241 ✗ shm_unlink(modelMemoryName("/FPP-Model-Data-", "/FPPMD-", name).c_str());
242 } else {
243 ✗ free(channelData);
244 }
245 ✗ channelData = nullptr;
246 }
247 7 }
248
249 // Largest buffer a model may describe, in cells (pixels). Above this the
250 // channel map alone would run to hundreds of MB.
251 static constexpr int64_t MAX_MODEL_CELLS = 4096 * 1024;
252
253 1435 PixelOverlayModel::PixelOverlayModel(const Json::Value& c) :
254
1/2
✓ Branch 0 taken 1435 times.
✗ Branch 1 not taken.
1435 config(c),
255 1435 overlayBufferData(nullptr),
256 1435 channelData(nullptr),
257 2870 runningEffect(nullptr) {
258 // Any definition this constructor cannot represent safely leaves
259 // channelData null and returns: isValid() is then false and the manager
260 // drops the model, with the reason in the log. Nothing below may throw.
261
2/4
✓ Branch 0 taken 1435 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1435 times.
1435 if (!config.isObject()) {
262 ✗ markInvalid("its definition is not a JSON object");
263 7 return;
264 }
265 1435 std::string orientation;
266 1435 std::string startCorner;
267 1435 int strings = 0;
268 1435 int sps = 0;
269 1435 int cfgWidth = 0;
270 1435 int cfgHeight = 0;
271 1435 int pixelSize = 0;
272
3/6
✓ Branch 0 taken 1435 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1434 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1434 times.
✗ Branch 5 not taken.
2869 if (!configString(config, "Name", name) || !configString(config, "Type", type) ||
273
2/4
✓ Branch 0 taken 1434 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1434 times.
✗ Branch 3 not taken.
1434 !configString(config, "BufferStyle", bufferStyle) ||
274
8/12
✓ Branch 0 taken 1434 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 1434 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1434 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1434 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1434 times.
✓ Branch 10 taken 1 times.
✓ Branch 11 taken 1434 times.
2869 !configString(config, "Orientation", orientation) || !configString(config, "StartCorner", startCorner)) {
275
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 markInvalid("a text field (Name, Type, BufferStyle, Orientation or StartCorner) is not text");
276 1 return;
277 }
278
3/6
✓ Branch 0 taken 1434 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1434 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1434 times.
✗ Branch 5 not taken.
4706 replaceAll(name, "/", "_");
279
3/6
✓ Branch 0 taken 1434 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1433 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1433 times.
✗ Branch 5 not taken.
2867 if (!configInt(config, "StartChannel", 0, startChannel) || !configInt(config, "ChannelCount", 0, channelCount) ||
280
4/8
✓ Branch 0 taken 1433 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1433 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1433 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1433 times.
✗ Branch 7 not taken.
1433 !configInt(config, "StringCount", 0, strings) || !configInt(config, "StrandsPerString", 0, sps) ||
281
2/4
✓ Branch 0 taken 1433 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1433 times.
✗ Branch 3 not taken.
1433 !configInt(config, "ChannelCountPerNode", 3, channelsPerNode) ||
282
8/12
✓ Branch 0 taken 1433 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 1433 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1433 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1433 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1433 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1 times.
✓ Branch 11 taken 1433 times.
4300 !configInt(config, "Width", 0, cfgWidth) || !configInt(config, "Height", 0, cfgHeight) ||
283
2/4
✓ Branch 0 taken 1433 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1433 times.
1433 !configInt(config, "PixelSize", 0, pixelSize)) {
284
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 markInvalid("a numeric field is not a number");
285 1 return;
286 }
287 1433 startChannel--; // need to be 0 based
288
2/2
✓ Branch 0 taken 63 times.
✓ Branch 1 taken 1370 times.
1433 if (sps == 0) {
289 63 sps = 1;
290 }
291
292 // An empty or one-letter StartCorner used to index past the string.
293
4/4
✓ Branch 0 taken 1370 times.
✓ Branch 1 taken 63 times.
✓ Branch 2 taken 984 times.
✓ Branch 3 taken 386 times.
1433 bool TtoB = startCorner.size() < 1 || startCorner[0] == 'T';
294
3/4
✓ Branch 0 taken 1370 times.
✓ Branch 1 taken 63 times.
✓ Branch 2 taken 1370 times.
✗ Branch 3 not taken.
1433 bool LtoR = startCorner.size() < 2 || startCorner[1] == 'L';
295
296
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1433 times.
1433 if (channelsPerNode == 0) {
297 ✗ channelsPerNode = 1;
298 }
299
3/4
✓ Branch 0 taken 1433 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 60 times.
✓ Branch 3 taken 1373 times.
1433 if (type == "FB") {
300 // The framebuffer copies channelData as packed RGB, width*height*3
301 // bytes; any other node width under- or over-ran it.
302 60 channelsPerNode = 3;
303 }
304
2/4
✓ Branch 0 taken 1433 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1433 times.
1433 if (channelsPerNode < 1 || channelsPerNode > 32) {
305 ✗ markInvalid("ChannelCountPerNode " + std::to_string(channelsPerNode) + " is out of range (1-32)");
306 ✗ return;
307 }
308
2/2
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 1419 times.
1433 bytesPerPixel = (channelsPerNode >= 4) ? 4 : 3;
309
310 // A model addresses fppd's channel data, which is always R,G,B[,W] - the
311 // output driver is what applies the hardware wire order. So the order the
312 // model sees is fully determined by the node width and is derived here
313 // rather than stored: any ColorOrder in the saved config is overwritten.
314 // Exported read-only through the API so the UI can show it.
315
2/4
✓ Branch 0 taken 1433 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1433 times.
✗ Branch 3 not taken.
1433 config["ColorOrder"] = ChannelDataOrder(channelsPerNode);
316
317
3/4
✓ Branch 0 taken 1433 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 84 times.
✓ Branch 3 taken 1349 times.
1433 if (type != "Channel") {
318
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 83 times.
84 if (pixelSize > 1) {
319 1 width = cfgWidth / pixelSize;
320 1 height = cfgHeight / pixelSize;
321 } else {
322 83 width = cfgWidth;
323 83 height = cfgHeight;
324 }
325
3/6
✓ Branch 0 taken 84 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 84 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 84 times.
84 if (width < 1 || height < 1 || (int64_t)width * height > MAX_MODEL_CELLS) {
326 ✗ markInvalid("size " + std::to_string(width) + "x" + std::to_string(height) + " is out of range");
327 ✗ return;
328 }
329
2/4
✓ Branch 0 taken 84 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 84 times.
✗ Branch 3 not taken.
84 config["ChannelCount"] = channelCount = width * height * channelsPerNode;
330
2/4
✓ Branch 0 taken 84 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 84 times.
✗ Branch 3 not taken.
84 config["StringCount"] = strings = height;
331
2/4
✓ Branch 0 taken 84 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 84 times.
✗ Branch 3 not taken.
84 config["StrandsPerString"] = sps = 1;
332
2/4
✓ Branch 0 taken 84 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 84 times.
✗ Branch 3 not taken.
84 config["StartChannel"] = startChannel = 0;
333
3/6
✓ Branch 0 taken 84 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 84 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 84 times.
✗ Branch 5 not taken.
84 config["Orientation"] = orientation = "horizontal";
334
3/6
✓ Branch 0 taken 84 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 84 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 84 times.
✗ Branch 5 not taken.
84 config["StartCorner"] = startCorner = "TL";
335 84 TtoB = true;
336 84 LtoR = true;
337 } else {
338 // doOverlay() copies channelCount bytes to channels[startChannel], in
339 // a buffer that ends at FPPD_MAX_CHANNELS: StartChannel 0 wrote
340 // channels[-1], and a range past the end wrote past the buffer, every
341 // frame, on the channel output thread.
342
3/4
✓ Branch 0 taken 1347 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 1347 times.
✗ Branch 3 not taken.
1349 if (startChannel < 0 || channelCount < 1 ||
343
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1346 times.
1347 (int64_t)startChannel + channelCount > FPPD_MAX_CHANNELS) {
344
4/8
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 3 times.
✗ Branch 7 not taken.
9 markInvalid("channels " + std::to_string(startChannel + 1) + " + " + std::to_string(channelCount) +
345
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
12 " do not fit in 1-" + std::to_string(FPPD_MAX_CHANNELS));
346 3 return;
347 }
348
3/4
✓ Branch 0 taken 1344 times.
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 1344 times.
1346 if (strings < 0 || sps < 0) {
349
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 markInvalid("StringCount and StrandsPerString must not be negative");
350 2 return;
351 }
352 1344 int64_t h = (int64_t)strings * sps;
353
2/4
✓ Branch 0 taken 1344 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1344 times.
✗ Branch 3 not taken.
1344 height = (h < 1 || h > INT_MAX) ? 1 : (int)h;
354 1344 width = channelCount / channelsPerNode;
355 1344 width /= height;
356
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1344 times.
1344 if (width == 0) {
357 ✗ width = 1;
358 }
359 }
360
361
4/8
✓ Branch 0 taken 1428 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1428 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1428 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1024 times.
✗ Branch 7 not taken.
4688 channelData = createModelMemory(modelMemoryName("/FPP-Model-Data-", "/FPPMD-", name),
362
1/2
✓ Branch 0 taken 404 times.
✗ Branch 1 not taken.
1428 channelCount, channelDataMapped);
363
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1428 times.
1428 if (!channelData) {
364 ✗ WarningHolder::AddWarning(WARNING_ID_MODEL_MEMORY, MODEL_MEMORY_WARNING);
365 }
366
367
5/10
✓ Branch 0 taken 1428 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1428 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1428 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 1428 times.
✗ Branch 8 not taken.
✓ Branch 9 taken 1428 times.
1428 if (orientation == "V" || orientation == "vertical") {
368 ✗ channelMap.resize(width * height * bytesPerPixel);
369
370 ✗ std::swap(width, height);
371 ✗ for (int x = 0; x < width; x++) {
372 ✗ int segment = x % sps;
373 ✗ for (int y = 0; y < height; y++) {
374 // pos in the normal overlay buffer
375 ✗ int ppos = y * width + x;
376 // Relative Input Pixel first channel
377 ✗ int inCh = (ppos * bytesPerPixel);
378
379 // X position in output
380 ✗ int outX = (LtoR) ? x : width - x - 1;
381 // Y position in output
382 ✗ int outY = (TtoB == (segment % 2)) ? height - y - 1 : y;
383
384 // Relative Mapped Output Pixel first channel
385 ✗ int mpos = outX * height + outY;
386 ✗ int outCh = (mpos * channelsPerNode);
387
388 ✗ for (int cho = 0; cho < bytesPerPixel; cho++) {
389 ✗ if (cho < channelsPerNode) {
390 ✗ channelMap[inCh + cho] = outCh + cho;
391 } else {
392 ✗ channelMap[inCh + cho] = FPPD_OFF_CHANNEL;
393 }
394 }
395 }
396 }
397
7/10
✓ Branch 0 taken 1428 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1428 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1428 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✓ Branch 7 taken 1427 times.
✓ Branch 8 taken 1 times.
✓ Branch 9 taken 1427 times.
1428 } else if (orientation == "C" || orientation == "custom") {
398 // Two formats supported:
399 // "data": dense format "n,n,n;n,n,n|n,n,n;n,n,n" (comma=col, semi=row, pipe=layer)
400 // "compressedData": sparse format "node,row,col[,layer];..." (only non-empty cells)
401 1 std::string compressed;
402 1 std::string dense;
403
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 (!configString(config, "compressedData", compressed) || !configString(config, "data", dense)) {
404 ✗ markInvalid("custom model data is not text");
405 ✗ return;
406 }
407
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (!compressed.empty()) {
408 // Parse compressed format: "node,row,col[,layer];node,row,col[,layer];..."
409 ✗ std::vector<std::string> entries = split(compressed, ';');
410
411 struct NodeEntry {
412 int node, row, col, layer;
413 };
414 ✗ std::vector<NodeEntry> nodes;
415 ✗ nodes.reserve(entries.size());
416
417 ✗ int maxRow = 0, maxCol = 0, maxLayer = 0;
418 ✗ for (auto& entry : entries) {
419 ✗ std::vector<std::string> parts = split(entry, ',');
420 ✗ if (parts.size() >= 3) {
421 NodeEntry ne;
422 ✗ ne.node = std::atoi(parts[0].c_str());
423 ✗ ne.row = std::atoi(parts[1].c_str());
424 ✗ ne.col = std::atoi(parts[2].c_str());
425 ✗ ne.layer = (parts.size() >= 4) ? std::atoi(parts[3].c_str()) : 0;
426 // Negative row/col/layer indexed channelMap before its
427 // start (a heap write), and huge ones overflowed the int
428 // size below; no real model is 4096 cells on a side.
429 ✗ if (ne.node > 0 && ne.node <= FPPD_MAX_CHANNEL_NUM &&
430 ✗ ne.row >= 0 && ne.row < 4096 && ne.col >= 0 && ne.col < 4096 &&
431 ✗ ne.layer >= 0 && ne.layer < 4096) {
432 ✗ nodes.push_back(ne);
433 ✗ maxRow = std::max(maxRow, ne.row);
434 ✗ maxCol = std::max(maxCol, ne.col);
435 ✗ maxLayer = std::max(maxLayer, ne.layer);
436 }
437 }
438 ✗ }
439
440 ✗ width = maxCol + 1;
441 ✗ int64_t allLayers = (int64_t)(maxRow + 1) * (maxLayer + 1);
442 ✗ height = allLayers > INT_MAX ? INT_MAX : (int)allLayers;
443 ✗ if (width < 1) width = 1;
444 ✗ if (height < 1) height = 1;
445 ✗ if ((int64_t)width * height > (600 * 600)) {
446 // Limit to first layer only
447 ✗ height = maxRow + 1;
448 ✗ if (height < 1) height = 1;
449 }
450 ✗ if ((int64_t)width * height > MAX_MODEL_CELLS) {
451 ✗ markInvalid("custom model is " + std::to_string(width) + "x" + std::to_string(height) + ", too large");
452 ✗ return;
453 }
454
455 ✗ channelMap.resize(width * height * bytesPerPixel);
456 // Initialize all to FPPD_OFF_CHANNEL
457 ✗ std::fill(channelMap.begin(), channelMap.end(), FPPD_OFF_CHANNEL);
458
459 ✗ int layerHeight = maxRow + 1;
460 ✗ for (auto& ne : nodes) {
461 ✗ int y = ne.row + ne.layer * layerHeight;
462 ✗ if (y >= height || ne.col >= width) {
463 ✗ continue;
464 }
465 ✗ for (int cho = 0; cho < bytesPerPixel; cho++) {
466 ✗ if (cho < channelsPerNode) {
467 ✗ channelMap[(y * width * bytesPerPixel) + (ne.col * bytesPerPixel) + cho] = (ne.node - 1) * channelsPerNode + cho;
468 }
469 }
470 }
471 ✗ } else {
472 // Parse dense format: "n,n,n;n,n,n|n,n,n;n,n,n"
473
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 std::string customData = ",;,";
474
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 if (config.isMember("data")) {
475
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 customData = dense;
476 }
477 1 int endOfFirstLayer = 0;
478
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 std::vector<std::string> layers = split(customData, '|');
479 1 std::vector<std::vector<std::string>> allData;
480 1 width = 1;
481 1 height = 0; // counted from the rows below, not StringCount
482
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
2 for (auto& layer : layers) {
483
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 std::vector<std::string> lines = split(layer, ';');
484
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 times.
3 for (auto& l : lines) {
485
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 allData.push_back(split(l, ','));
486 2 width = std::max(width, (int)allData.back().size());
487 2 height++;
488 }
489
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (endOfFirstLayer == 0) {
490 1 endOfFirstLayer = allData.size();
491 }
492 1 lines.clear();
493
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if ((int64_t)width * height > (600 * 600)) {
494 ✗ height = endOfFirstLayer;
495 ✗ allData.erase(allData.begin() + endOfFirstLayer, allData.end());
496 ✗ break;
497 }
498
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 }
499
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (height < 1) {
500 ✗ height = 1;
501 }
502
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if ((int64_t)width * height > MAX_MODEL_CELLS) {
503 ✗ markInvalid("custom model is " + std::to_string(width) + "x" + std::to_string(height) + ", too large");
504 ✗ return;
505 }
506
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 channelMap.resize(width * height * bytesPerPixel);
507
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 times.
3 for (int y = 0; y < height; y++) {
508
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 2 times.
6 for (int x = 0; x < width; x++) {
509 4 int node = -1;
510
3/6
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
4 if (y < allData.size() && x < allData[y].size()) {
511 4 std::string& s = allData[y][x];
512
5/10
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 4 times.
✗ Branch 8 not taken.
✓ Branch 9 taken 4 times.
4 if (s.empty() || s.length() > 10 || s == " ") {
513 ✗ node = -1;
514 } else {
515 4 node = std::atoi(s.c_str());
516
2/8
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 4 times.
4 if (node == 0 && s != "0") {
517 ✗ node = -1;
518 }
519
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 if (node > FPPD_MAX_CHANNEL_NUM) {
520 ✗ node = -1;
521 }
522 }
523 }
524
525
2/2
✓ Branch 0 taken 12 times.
✓ Branch 1 taken 4 times.
16 for (int cho = 0; cho < bytesPerPixel; cho++) {
526
2/4
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
12 if (node != -1 && cho < channelsPerNode) {
527 12 channelMap[(y * width * bytesPerPixel) + (x * bytesPerPixel) + cho] = (node - 1) * channelsPerNode + cho;
528 } else {
529 ✗ channelMap[(y * width * bytesPerPixel) + (x * bytesPerPixel) + cho] = FPPD_OFF_CHANNEL;
530 }
531 }
532 }
533 }
534
3/6
✓ 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.
1 }
535
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 } else {
536
1/2
✓ Branch 0 taken 1427 times.
✗ Branch 1 not taken.
1427 channelMap.resize(width * height * bytesPerPixel);
537
2/2
✓ Branch 0 taken 10721 times.
✓ Branch 1 taken 1427 times.
12148 for (int y = 0; y < height; y++) {
538 10721 int segment = y % sps;
539
2/2
✓ Branch 0 taken 1310964 times.
✓ Branch 1 taken 10721 times.
1321685 for (int x = 0; x < width; x++) {
540 // pos in the normal overlay buffer
541 1310964 int ppos = y * width + x;
542 // Input Pixel first channel
543 1310964 int inCh = (ppos * bytesPerPixel);
544
545 // X position in output
546
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1310964 times.
1310964 int outX = (LtoR == (segment % 2)) ? width - x - 1 : x;
547 // Y position in output
548
2/2
✓ Branch 0 taken 1297297 times.
✓ Branch 1 taken 13667 times.
1310964 int outY = (TtoB) ? y : height - y - 1;
549
550 // Relative Mapped Output Pixel first channel
551 1310964 int mpos = outY * width + outX;
552 1310964 int outCh = (mpos * channelsPerNode);
553
554
2/2
✓ Branch 0 taken 3935280 times.
✓ Branch 1 taken 1310964 times.
5246244 for (int cho = 0; cho < bytesPerPixel; cho++) {
555
1/2
✓ Branch 0 taken 3935280 times.
✗ Branch 1 not taken.
3935280 if (cho < channelsPerNode) {
556 3935280 channelMap[inCh + cho] = outCh + cho;
557 } else {
558 ✗ channelMap[inCh + cho] = FPPD_OFF_CHANNEL;
559 }
560 }
561 }
562 }
563 }
564
565 // channelData is exactly channelCount bytes, but a custom model's node
566 // numbers come straight from the config and can address well past it.
567 // Every setData()/doOverlay() indexes channelData through channelMap, so
568 // enforce the bound once here rather than at each use.
569 1428 int outOfRange = 0;
570
2/2
✓ Branch 0 taken 3935292 times.
✓ Branch 1 taken 1428 times.
3936720 for (auto& ch : channelMap) {
571
3/4
✓ Branch 0 taken 3935292 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✓ Branch 3 taken 3935289 times.
3935292 if (ch != FPPD_OFF_CHANNEL && ch >= (uint32_t)channelCount) {
572 3 ch = FPPD_OFF_CHANNEL;
573 3 outOfRange++;
574 }
575 }
576
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1427 times.
1428 if (outOfRange) {
577
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogWarn(VB_CHANNELOUT, "Overlay model '%s': %d node channel(s) exceed the model's %d channels and were disabled\n",
578 name.c_str(), outOfRange, channelCount);
579 }
580
4/4
✓ Branch 0 taken 1428 times.
✓ Branch 1 taken 7 times.
✓ Branch 2 taken 1428 times.
✓ Branch 3 taken 7 times.
1515 }
581 2096 PixelOverlayModel::~PixelOverlayModel() {
582 {
583 // The running effect used to be left behind, so every output reload
584 // leaked whatever was running on an auto-created model (a WLED effect
585 // is a whole strip). Nothing else can reach it now: the effect thread
586 // was drained (removeAutoOverlayModel) or stopped (loadModelMap,
587 // shutdown) before the model is freed.
588 1068 std::unique_lock<std::recursive_mutex> l(effectLock);
589
2/2
✓ Branch 0 taken 20 times.
✓ Branch 1 taken 514 times.
1068 delete runningEffect;
590 1068 runningEffect = nullptr;
591 1068 }
592 // A retired model can be freed long after its replacement took its name,
593 // so nothing here may act on the name: the shm names were unlinked at
594 // retirement if it was retired. Nor does it reset its submodels: the
595 // manager has reset them by parent name at retirement, and a full reload
596 // or shutdown frees them along with it. Resetting them here, by
597 // name, disabled the replacement's submodel, and took modelsLock on
598 // whatever thread dropped the last reference -- the effect thread holds a
599 // submodel's effectLock there, the reverse of the sanctioned order.
600 bool namesReleased;
601 {
602 1068 std::lock_guard<std::mutex> l(s_overlayBufferAllocLock);
603 1068 namesReleased = s_namesReleased.erase(this) != 0;
604 1068 }
605
2/2
✓ Branch 0 taken 527 times.
✓ Branch 1 taken 7 times.
1068 if (channelData) {
606
1/2
✓ Branch 0 taken 527 times.
✗ Branch 1 not taken.
1054 if (channelDataMapped) {
607 1054 munmap(channelData, std::max(channelCount, 8));
608
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 527 times.
1054 if (!namesReleased) {
609 ✗ shm_unlink(modelMemoryName("/FPP-Model-Data-", "/FPPMD-", name).c_str());
610 }
611 } else {
612 ✗ free(channelData);
613 }
614 }
615
2/2
✓ Branch 0 taken 13 times.
✓ Branch 1 taken 521 times.
1068 if (overlayBufferData) {
616
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
26 if (overlayBufferMapped) {
617 26 munmap(overlayBufferData, width * height * bytesPerPixel + sizeof(OverlayBufferData));
618
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 13 times.
26 if (!namesReleased) {
619 ✗ shm_unlink(modelMemoryName("/FPP-Model-Overlay-Buffer-", "/FPPMB-", name).c_str());
620 }
621 } else {
622 ✗ free(overlayBufferData);
623 }
624 }
625 2096 }
626
627 528 void PixelOverlayModel::releaseSharedMemoryNames() {
628 // Under the allocation lock, so a buffer allocated concurrently either
629 // exists to be unlinked here or sees the release and stays private.
630
1/2
✓ Branch 0 taken 528 times.
✗ Branch 1 not taken.
528 std::lock_guard<std::mutex> l(s_overlayBufferAllocLock);
631
2/4
✓ Branch 0 taken 528 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 528 times.
528 if (!s_namesReleased.insert(this).second) {
632 ✗ return;
633 }
634
2/4
✓ Branch 0 taken 528 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 528 times.
✗ Branch 3 not taken.
528 if (channelData && channelDataMapped) {
635
4/8
✓ Branch 0 taken 528 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 528 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 528 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 528 times.
✗ Branch 7 not taken.
2240 shm_unlink(modelMemoryName("/FPP-Model-Data-", "/FPPMD-", name).c_str());
636 }
637
3/4
✓ Branch 0 taken 13 times.
✓ Branch 1 taken 515 times.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
528 if (overlayBufferData && overlayBufferMapped) {
638
4/8
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 13 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 13 times.
✗ Branch 7 not taken.
65 shm_unlink(modelMemoryName("/FPP-Model-Overlay-Buffer-", "/FPPMB-", name).c_str());
639 }
640
1/2
✓ Branch 0 taken 528 times.
✗ Branch 1 not taken.
528 }
641
642 528 bool PixelOverlayModel::isAutoCreated() {
643
3/6
✓ Branch 0 taken 528 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 528 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 528 times.
✗ Branch 5 not taken.
528 return config.isMember("autoCreated") && config["autoCreated"].isBool() && config["autoCreated"].asBool();
644 }
645
646 // The state is set from the API, commands and effect threads, and read on
647 // the output thread and by anything reporting it, not all under one lock.
648 // atomic_ref rather than an atomic member: this class's layout is compiled
649 // into plugins (see the header).
650 1682539 PixelOverlayState PixelOverlayModel::getState() const {
651 1682539 return std::atomic_ref<PixelOverlayState>(const_cast<PixelOverlayState&>(state)).load(std::memory_order_relaxed);
652 }
653 236038 bool PixelOverlayModel::needsOutput() const {
654
6/8
✓ Branch 0 taken 236038 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 124577 times.
✓ Branch 3 taken 111461 times.
✓ Branch 4 taken 124577 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 7231 times.
✓ Branch 7 taken 117346 times.
236038 return getState().getState() != PixelOverlayState::PixelState::Disabled || hasChildren();
655 }
656
657 324224 void PixelOverlayModel::setState(const PixelOverlayState& st) {
658
3/4
✓ Branch 0 taken 324224 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 102552 times.
✓ Branch 3 taken 221672 times.
324224 if (st == getState())
659 102552 return;
660
661 // Give the buffer a little time to flush before disabling -- but not on
662 // the output thread, which is what flushes it: an effect that ends by
663 // disabling its model after the next output (EFFECT_AFTER_NEXT_OUTPUT
664 // runs there) waited out the whole 100 ms, and every output stalled.
665
10/12
✓ Branch 0 taken 110420 times.
✓ Branch 1 taken 111252 times.
✓ Branch 2 taken 110420 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 521 times.
✓ Branch 5 taken 109899 times.
✓ Branch 6 taken 521 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 164 times.
✓ Branch 9 taken 357 times.
✓ Branch 10 taken 164 times.
✓ Branch 11 taken 221508 times.
221672 if (!st.getState() && needRefresh() && !IsChannelOutputThread()) {
666 164 int x = 0;
667
7/8
✓ Branch 0 taken 727 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 601 times.
✓ Branch 3 taken 126 times.
✓ Branch 4 taken 563 times.
✓ Branch 5 taken 38 times.
✓ Branch 6 taken 563 times.
✓ Branch 7 taken 164 times.
727 while (needRefresh() && (x++ < 10)) {
668
1/2
✓ Branch 0 taken 563 times.
✗ Branch 1 not taken.
563 usleep(10000);
669 }
670 }
671
672 221672 PixelOverlayState old = std::atomic_ref<PixelOverlayState>(state).exchange(st, std::memory_order_relaxed);
673
1/2
✓ Branch 0 taken 221672 times.
✗ Branch 1 not taken.
221672 PixelOverlayManager::INSTANCE.modelStateChanged(this, old, st);
674 }
675 // Copy-on-write publish of the child list. See the note on `children` in the
676 // header for why this must not take (or be called under) any overlay lock.
677 131708 void PixelOverlayModel::setChildState(const std::string& n, const PixelOverlayState& in, int ox, int oy, int w, int h) {
678 // flushChildren() indexes this model's channelMap with the child's
679 // rectangle on every frame, so the rectangle has to lie inside this model.
680 // A submodel saved against a parent whose geometry has since changed can
681 // point anywhere; refuse it here, where the list is written, the same way
682 // setData() refuses an out-of-bounds region.
683 131708 PixelOverlayState st = in;
684 // 64-bit sums: an XOffset near INT_MAX wrapped ox + w negative and passed.
685
8/12
✓ Branch 0 taken 75392 times.
✓ Branch 1 taken 56316 times.
✓ Branch 2 taken 75392 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 75392 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 75392 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 75392 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 2 times.
✓ Branch 11 taken 131706 times.
207100 if (st.getState() && (ox < 0 || oy < 0 || w < 0 || h < 0 ||
686
3/4
✓ Branch 0 taken 75390 times.
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 75390 times.
75392 (int64_t)ox + w > width || (int64_t)oy + h > height)) {
687
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 LogWarn(VB_CHANNELOUT, "Submodel '%s' (%dx%d @ %d,%d) lies outside its parent '%s' (%dx%d), ignoring it\n",
688 n.c_str(), w, h, ox, oy, name.c_str(), width, height);
689 2 st = PixelOverlayState(PixelOverlayState::Disabled);
690 }
691
692 131708 bool hadChildren = false;
693 131708 bool hasChildren = false;
694
695 {
696 // Leaf lock. Writers really are concurrent -- an HTTP/command thread
697 // holds modelsLock while an effect update thread holds only this
698 // model's effectLock -- so the read-copy-publish below has to be
699 // serialized or one update is lost. Nothing inside this scope calls
700 // out to code that takes another lock.
701
1/2
✓ Branch 0 taken 131708 times.
✗ Branch 1 not taken.
131708 std::lock_guard<std::mutex> lk(childrenWriteLock);
702
703
1/2
✓ Branch 0 taken 131708 times.
✗ Branch 1 not taken.
131708 auto cur = children.load();
704
4/4
✓ Branch 0 taken 131681 times.
✓ Branch 1 taken 27 times.
✓ Branch 2 taken 124490 times.
✓ Branch 3 taken 7191 times.
131708 hadChildren = cur && !cur->empty();
705
706
3/4
✓ Branch 0 taken 131681 times.
✓ Branch 1 taken 27 times.
✓ Branch 2 taken 131681 times.
✗ Branch 3 not taken.
131708 ChildModelStateList next = cur ? *cur : ChildModelStateList();
707 131708 bool changed = false;
708 131708 bool found = false;
709
710 131708 auto it = next.begin();
711
2/2
✓ Branch 0 taken 243857 times.
✓ Branch 1 taken 131708 times.
375565 while (it != next.end()) {
712
2/2
✓ Branch 0 taken 75376 times.
✓ Branch 1 taken 168481 times.
243857 if (it->name == n) {
713 75376 found = true;
714
2/2
✓ Branch 0 taken 56287 times.
✓ Branch 1 taken 19089 times.
75376 if (st.getState() == 0) {
715 56287 it = next.erase(it);
716 56287 changed = true;
717 } else {
718
2/4
✓ Branch 0 taken 18885 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 18885 times.
✗ Branch 3 not taken.
37974 if (it->state != st || it->xoffset != ox || it->yoffset != oy ||
719
6/8
✓ Branch 0 taken 18885 times.
✓ Branch 1 taken 204 times.
✓ Branch 2 taken 18885 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 18885 times.
✓ Branch 6 taken 204 times.
✓ Branch 7 taken 18885 times.
37974 it->width != w || it->height != h) {
720 204 it->state = st;
721 204 it->xoffset = ox;
722 204 it->yoffset = oy;
723 204 it->width = w;
724 204 it->height = h;
725 204 changed = true;
726 }
727 19089 ++it;
728 }
729 } else {
730 168481 ++it;
731 }
732 }
733
734
6/6
✓ Branch 0 taken 56332 times.
✓ Branch 1 taken 75376 times.
✓ Branch 2 taken 56301 times.
✓ Branch 3 taken 31 times.
✓ Branch 4 taken 56301 times.
✓ Branch 5 taken 75407 times.
131708 if (!found && st.getState()) {
735 56301 ChildModelState cms;
736
1/2
✓ Branch 0 taken 56301 times.
✗ Branch 1 not taken.
56301 cms.name = n;
737 56301 cms.state = st;
738 56301 cms.xoffset = ox;
739 56301 cms.yoffset = oy;
740 56301 cms.width = w;
741 56301 cms.height = h;
742
1/2
✓ Branch 0 taken 56301 times.
✗ Branch 1 not taken.
56301 next.push_back(cms);
743 56301 changed = true;
744 56301 }
745
746 131708 hasChildren = !next.empty();
747
2/2
✓ Branch 0 taken 18916 times.
✓ Branch 1 taken 112792 times.
131708 if (!changed) {
748 // Nothing to publish -- and with the list unchanged, hadChildren
749 // and hasChildren agree, so neither transition below could fire.
750 18916 return;
751 }
752
2/4
✓ Branch 0 taken 112792 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 112792 times.
✗ Branch 3 not taken.
112792 children.store(std::make_shared<const ChildModelStateList>(std::move(next)));
753
6/6
✓ Branch 0 taken 112792 times.
✓ Branch 1 taken 18916 times.
✓ Branch 2 taken 112792 times.
✓ Branch 3 taken 18916 times.
✓ Branch 4 taken 112792 times.
✓ Branch 5 taken 18916 times.
169540 }
754
755 // Deliberately outside childrenWriteLock: modelStateChanged() takes
756 // activeModelsLock, which the channel output thread holds while it is
757 // inside the readers of `children`.
758
4/4
✓ Branch 0 taken 105602 times.
✓ Branch 1 taken 7190 times.
✓ Branch 2 taken 7176 times.
✓ Branch 3 taken 98426 times.
112792 if (hadChildren && !hasChildren) {
759
2/4
✓ Branch 0 taken 7176 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7176 times.
✗ Branch 3 not taken.
7176 PixelOverlayManager::INSTANCE.modelStateChanged(this, PixelOverlayState::Enabled, getState());
760
3/4
✓ Branch 0 taken 7190 times.
✓ Branch 1 taken 98426 times.
✓ Branch 2 taken 7190 times.
✗ Branch 3 not taken.
105616 } else if (!hadChildren && hasChildren) {
761
2/4
✓ Branch 0 taken 7190 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7190 times.
✗ Branch 3 not taken.
7190 PixelOverlayManager::INSTANCE.modelStateChanged(this, getState(), PixelOverlayState::Enabled);
762 }
763 }
764
765 1614 bool PixelOverlayModel::flushChildren(uint8_t* dst) {
766 // Snapshot: the list this iterates cannot be mutated or freed by a
767 // concurrent setChildState(), which publishes a whole new list instead.
768
1/2
✓ Branch 0 taken 1614 times.
✗ Branch 1 not taken.
1614 auto snapshot = children.load();
769
3/6
✓ Branch 0 taken 1614 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1614 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 1614 times.
1614 if (!snapshot || snapshot->empty()) {
770 ✗ return false;
771 }
772
2/2
✓ Branch 0 taken 1947 times.
✓ Branch 1 taken 1614 times.
3561 for (auto& c : *snapshot) {
773 1947 int cst = c.state.getState();
774
2/2
✓ Branch 0 taken 39895 times.
✓ Branch 1 taken 1947 times.
41842 for (int y = 0; y < c.height; y++) {
775 39895 int yoff = (y + c.yoffset) * width * bytesPerPixel;
776
2/2
✓ Branch 0 taken 2250060 times.
✓ Branch 1 taken 39895 times.
2289955 for (int x = 0; x < c.width; x++) {
777 2250060 int off0 = (x + c.xoffset) * bytesPerPixel + yoff;
778
779
3/4
✓ Branch 0 taken 80756 times.
✓ Branch 1 taken 2168416 times.
✓ Branch 2 taken 888 times.
✗ Branch 3 not taken.
2250060 switch (cst) {
780 80756 case 1: // Opaque
781
2/2
✓ Branch 0 taken 242364 times.
✓ Branch 1 taken 80756 times.
323120 for (int ch = 0; ch < bytesPerPixel; ch++) {
782
1/2
✓ Branch 0 taken 242364 times.
✗ Branch 1 not taken.
242364 if (channelMap[off0 + ch] != FPPD_OFF_CHANNEL) {
783 242364 dst[channelMap[off0 + ch]] = channelData[channelMap[off0 + ch]];
784 }
785 }
786 80756 break;
787 2168416 case 2: // Transparent
788
2/2
✓ Branch 0 taken 6505248 times.
✓ Branch 1 taken 2168416 times.
8673664 for (int ch = 0; ch < bytesPerPixel; ch++) {
789
5/6
✓ Branch 0 taken 6505248 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2169632 times.
✓ Branch 3 taken 4335616 times.
✓ Branch 4 taken 2169632 times.
✓ Branch 5 taken 4335616 times.
6505248 if (channelMap[off0 + ch] != FPPD_OFF_CHANNEL && channelData[channelMap[off0 + ch]]) {
790 2169632 dst[channelMap[off0 + ch]] = channelData[channelMap[off0 + ch]];
791 }
792 }
793 2168416 break;
794 888 case 3: { // TransparentRGB
795 888 bool cp = false;
796
2/2
✓ Branch 0 taken 2664 times.
✓ Branch 1 taken 888 times.
3552 for (int ch = 0; ch < bytesPerPixel; ch++) {
797
3/4
✓ Branch 0 taken 2664 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1776 times.
✓ Branch 3 taken 888 times.
2664 cp |= (channelMap[off0 + ch] != FPPD_OFF_CHANNEL && channelData[channelMap[off0 + ch]]);
798 }
799
1/2
✓ Branch 0 taken 888 times.
✗ Branch 1 not taken.
888 if (cp) {
800
2/2
✓ Branch 0 taken 2664 times.
✓ Branch 1 taken 888 times.
3552 for (int ch = 0; ch < bytesPerPixel; ch++) {
801
5/6
✓ Branch 0 taken 2664 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1776 times.
✓ Branch 3 taken 888 times.
✓ Branch 4 taken 1776 times.
✓ Branch 5 taken 888 times.
2664 if (channelMap[off0 + ch] != FPPD_OFF_CHANNEL && channelData[channelMap[off0 + ch]]) {
802 1776 dst[channelMap[off0 + ch]] = channelData[channelMap[off0 + ch]];
803 }
804 }
805 }
806 888 break;
807 }
808 }
809 }
810 }
811 }
812 1614 return true;
813 1614 }
814
815 // A plain model owns one contiguous output range, so its channelData offset is
816 // simply relative to where that range starts (doOverlay() below memcpy's the
817 // whole thing to &channels[startChannel]).
818 432 uint32_t PixelOverlayModel::outputChannelForData(uint32_t dataOffset) const {
819 432 return static_cast<uint32_t>(startChannel) + dataOffset;
820 }
821 24 uint32_t PixelOverlayModel::outputChannelForCell(int x, int y) const {
822
4/8
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 24 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 24 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 24 times.
24 if (x < 0 || y < 0 || x >= width || y >= height) {
823 ✗ return FPPD_OFF_CHANNEL;
824 }
825 24 size_t off = ((size_t)y * width + x) * bytesPerPixel;
826
3/6
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 24 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 24 times.
24 if (off >= channelMap.size() || channelMap[off] == FPPD_OFF_CHANNEL) {
827 ✗ return FPPD_OFF_CHANNEL;
828 }
829 24 return outputChannelForData(channelMap[off]);
830 }
831
832 /*
833 * Build (once) the polar view of this model's buffer.
834 *
835 * The join runs buffer cell -> channelData offset -> absolute output channel ->
836 * position, because the virtual display map is keyed by absolute channel and
837 * every model type can already say which channel one of its buffer cells ends
838 * up on (outputChannelForData, overridden by the scattering subclasses).
839 *
840 * Any failure returns nullptr and is remembered, so a model with no geometry
841 * costs one attempt rather than one per effect start. A submodel whose parent
842 * can't be resolved yet is not an attempt: it has no channels to join.
843 */
844 23 const PolarBufferMap* PixelOverlayModel::getPolarMap(PolarMode mode) {
845
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 23 times.
23 if (mode == PolarMode::None) {
846 ✗ return nullptr;
847 }
848
1/2
✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
23 PixelOverlayModelSub* sub = dynamic_cast<PixelOverlayModelSub*>(this);
849
5/8
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 19 times.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 4 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 23 times.
23 if (sub && !sub->readyForPolarJoin()) {
850 ✗ return nullptr;
851 }
852 // Rebuild when the layout has been re-uploaded since the map was built.
853 struct stat vst;
854 23 long long layoutVersion = 0;
855
5/8
✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 23 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 23 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 21 times.
✓ Branch 7 taken 2 times.
46 if (stat(FPP_DIR_CONFIG("/virtualdisplaymap").c_str(), &vst) == 0) {
856 21 layoutVersion = (long long)vst.st_mtime;
857 }
858 23 auto key = std::make_pair(mode, layoutVersion);
859
860
1/2
✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
23 auto cached = polarMaps.find(key);
861
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 22 times.
23 if (cached != polarMaps.end()) {
862
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 return cached->second.valid() ? &cached->second : nullptr;
863 }
864
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
22 PolarBufferMap& polarMap = polarMaps[key];
865
866 22 std::vector<std::pair<uint32_t, std::pair<float, float>>> vdm;
867
3/4
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 20 times.
22 if (!LoadVirtualDisplayPositions(vdm)) {
868
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 LogDebug(VB_CHANNELOUT,
869 "No config/virtualdisplaymap; polar buffer mapping unavailable for %s\n",
870 name.c_str());
871 2 return nullptr;
872 }
873
1/2
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
20 std::unordered_map<uint32_t, std::pair<float, float>> pos(vdm.begin(), vdm.end());
874
875 20 std::vector<PolarPixel> pixels;
876
1/2
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
20 pixels.reserve(static_cast<size_t>(width) * height);
877
2/2
✓ Branch 0 taken 26 times.
✓ Branch 1 taken 20 times.
46 for (int y = 0; y < height; y++) {
878
2/2
✓ Branch 0 taken 488 times.
✓ Branch 1 taken 26 times.
514 for (int x = 0; x < width; x++) {
879 488 int off = (y * width + x) * bytesPerPixel;
880
3/6
✓ Branch 0 taken 488 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 488 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 488 times.
488 if (off < 0 || off >= (int)channelMap.size()) {
881 102 continue;
882 }
883 488 uint32_t cd = channelMap[off];
884
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 488 times.
488 if (cd == FPPD_OFF_CHANNEL) {
885 ✗ continue; // a hole in the buffer
886 }
887
1/2
✓ Branch 0 taken 488 times.
✗ Branch 1 not taken.
488 uint32_t ch = outputChannelForData(cd);
888
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 480 times.
488 if (ch == FPPD_OFF_CHANNEL) {
889 8 continue;
890 }
891
1/2
✓ Branch 0 taken 480 times.
✗ Branch 1 not taken.
480 auto it = pos.find(ch);
892
2/2
✓ Branch 0 taken 94 times.
✓ Branch 1 taken 386 times.
480 if (it == pos.end()) {
893 94 continue; // this node was never exported to the layout
894 }
895 772 pixels.push_back({ it->second.first, it->second.second,
896
1/2
✓ Branch 0 taken 386 times.
✗ Branch 1 not taken.
386 static_cast<uint32_t>(y * width + x) });
897 }
898 }
899
900
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 16 times.
20 if (pixels.size() < 2) {
901
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 LogDebug(VB_CHANNELOUT,
902 "Model %s has %zu positioned pixels; polar buffer mapping unavailable\n",
903 name.c_str(), pixels.size());
904 4 return nullptr;
905 }
906
907
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 polarMap = BuildPolarBufferMap(pixels, mode);
908
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 16 times.
16 if (!polarMap.valid()) {
909 ✗ LogDebug(VB_CHANNELOUT, "Could not build a polar buffer for %s\n", name.c_str());
910 ✗ return nullptr;
911 }
912
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 LogDebug(VB_CHANNELOUT, "Polar buffer for %s: %d x %d from %zu positioned pixels\n",
913 name.c_str(), polarMap.width, polarMap.height, pixels.size());
914 16 return &polarMap;
915 22 }
916
917 22917 void PixelOverlayModel::doOverlay(uint8_t* channels) {
918
1/2
✓ Branch 0 taken 22917 times.
✗ Branch 1 not taken.
22917 int st = getState().getState();
919 22917 uint8_t* dst = &channels[startChannel];
920
6/6
✓ Branch 0 taken 1617 times.
✓ Branch 1 taken 21300 times.
✓ Branch 2 taken 1614 times.
✓ Branch 3 taken 3 times.
✓ Branch 4 taken 1614 times.
✓ Branch 5 taken 21303 times.
22917 if (st == 0 && hasChildren()) {
921 // this model is disable, but we have children that are
922 // enabled. Thus, we need to apply their blending
923 // to the channel data.
924 1614 flushChildren(dst);
925 1614 dirtyBuffer = false;
926 1614 return;
927 }
928
2/2
✓ Branch 0 taken 326 times.
✓ Branch 1 taken 20628 times.
20954 if (((st == 2) || (st == 3)) &&
929
1/2
✓ Branch 0 taken 675 times.
✗ Branch 1 not taken.
675 (!IsEffectRunning()) &&
930
5/6
✓ Branch 0 taken 20954 times.
✓ Branch 1 taken 349 times.
✓ Branch 2 taken 21 times.
✓ Branch 3 taken 654 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 21303 times.
42627 (!sequence->IsSequenceRunning()) &&
931
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 21 times.
21 !PluginManager::INSTANCE.hasPlugins()) {
932 // there is nothing running that we would be overlaying so do a straight copy
933 ✗ st = 1;
934 }
935
936 21303 uint8_t* src = channelData;
937
4/4
✓ Branch 0 taken 20625 times.
✓ Branch 1 taken 349 times.
✓ Branch 2 taken 326 times.
✓ Branch 3 taken 3 times.
21303 switch (st) {
938 20625 case 1: // Active - Opaque
939 20625 memcpy(dst, src, channelCount);
940 20625 break;
941 349 case 2: // Active Transparent
942
2/2
✓ Branch 0 taken 52350 times.
✓ Branch 1 taken 349 times.
52699 for (int j = 0; j < channelCount; j++, src++, dst++) {
943
2/2
✓ Branch 0 taken 24008 times.
✓ Branch 1 taken 28342 times.
52350 if (*src) {
944 24008 *dst = *src;
945 }
946 }
947 349 break;
948 326 case 3: // Active Transparent RGB(W)
949
2/2
✓ Branch 0 taken 16300 times.
✓ Branch 1 taken 326 times.
16626 for (int j = 0; j < channelCount; j += channelsPerNode, src += channelsPerNode, dst += channelsPerNode) {
950 // A channel count that is not a whole number of nodes leaves a
951 // short last node; reading and writing a full node there ran past
952 // channelData and past the model's channels.
953 16300 const int n = std::min(channelsPerNode, channelCount - j);
954 16300 bool any = false;
955
2/2
✓ Branch 0 taken 48900 times.
✓ Branch 1 taken 16300 times.
65200 for (int k = 0; k < n; k++) {
956 48900 any |= src[k];
957 }
958
2/2
✓ Branch 0 taken 14322 times.
✓ Branch 1 taken 1978 times.
16300 if (any) {
959
2/2
✓ Branch 0 taken 42966 times.
✓ Branch 1 taken 14322 times.
57288 for (int k = 0; k < n; k++) {
960 42966 dst[k] = src[k];
961 }
962 }
963 }
964 326 break;
965 }
966
967 21303 dirtyBuffer = false;
968 }
969
970 98312 void PixelOverlayModel::setData(const uint8_t* data) {
971
2/2
✓ Branch 0 taken 90588434 times.
✓ Branch 1 taken 98312 times.
90686746 for (int c = 0; c < (width * height * bytesPerPixel); c++) {
972
2/2
✓ Branch 0 taken 90588431 times.
✓ Branch 1 taken 3 times.
90588434 if (channelMap[c] != FPPD_OFF_CHANNEL) {
973 90588431 channelData[channelMap[c]] = data[c];
974 }
975 }
976 98312 dirtyBuffer = true;
977 98312 }
978
979 48821 void PixelOverlayModel::setData(const uint8_t* data, int xOffset, int yOffset, int w, int h, const PixelOverlayState& st) {
980
1/2
✓ Branch 0 taken 48821 times.
✗ Branch 1 not taken.
48821 if (((int64_t)w + xOffset > width) ||
981
2/4
✓ Branch 0 taken 48821 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 48821 times.
✗ Branch 3 not taken.
48821 ((int64_t)h + yOffset > height) ||
982
3/6
✓ Branch 0 taken 48821 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 48821 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 48821 times.
48821 (xOffset < 0) || (yOffset < 0) || (w < 0) || (h < 0)) {
983 ✗ LogErr(VB_CHANNELOUT, "Error: region is out of bounds, model is %dx%d, region is %dx%d @ %d,%d\n",
984 width, height, w, h, xOffset, yOffset);
985 ✗ return;
986 }
987
988 144603 if ((w == width) &&
989
2/4
✓ Branch 0 taken 46961 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 46961 times.
✗ Branch 3 not taken.
46961 (h == height) &&
990
1/2
✓ Branch 0 taken 46961 times.
✗ Branch 1 not taken.
46961 (xOffset == 0) &&
991
5/6
✓ Branch 0 taken 46961 times.
✓ Branch 1 taken 1860 times.
✓ Branch 2 taken 46961 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 46961 times.
✓ Branch 5 taken 1860 times.
95782 (yOffset == 0) &&
992 46961 (st.getState() == 1)) {
993 46961 setData(data);
994 46961 return;
995 }
996
997 1860 int cst = st.getState();
998 1860 int rowWrap = (width - w) * bytesPerPixel;
999 1860 int s = 0;
1000 1860 int c = (yOffset * width * bytesPerPixel) + (xOffset * bytesPerPixel);
1001
2/2
✓ Branch 0 taken 3927 times.
✓ Branch 1 taken 1860 times.
5787 for (int yPos = 0; yPos < h; yPos++) {
1002
2/2
✓ Branch 0 taken 29542 times.
✓ Branch 1 taken 3927 times.
33469 for (int xPos = 0; xPos < w; xPos++) {
1003
3/4
✓ Branch 0 taken 8336 times.
✓ Branch 1 taken 21206 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 8336 times.
29542 if (cst == 1 || cst == 0) {
1004
2/2
✓ Branch 0 taken 63622 times.
✓ Branch 1 taken 21206 times.
84828 for (int i = 0; i < bytesPerPixel; i++, c++, s++) {
1005
1/2
✓ Branch 0 taken 63622 times.
✗ Branch 1 not taken.
63622 if (channelMap[c] != FPPD_OFF_CHANNEL) {
1006 63622 channelData[channelMap[c]] = data[s];
1007 }
1008 }
1009
2/2
✓ Branch 0 taken 4792 times.
✓ Branch 1 taken 3544 times.
29542 } else if (cst == 2) {
1010
2/2
✓ Branch 0 taken 14376 times.
✓ Branch 1 taken 4792 times.
19168 for (int i = 0; i < bytesPerPixel; i++, c++, s++) {
1011
5/6
✓ Branch 0 taken 9544 times.
✓ Branch 1 taken 4832 times.
✓ Branch 2 taken 9544 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 9544 times.
✓ Branch 5 taken 4832 times.
14376 if (data[s] && channelMap[c] != FPPD_OFF_CHANNEL) {
1012 9544 channelData[channelMap[c]] = data[s];
1013 }
1014 }
1015
1/2
✓ Branch 0 taken 3544 times.
✗ Branch 1 not taken.
3544 } else if (cst == 3) {
1016 3544 bool b = false;
1017
2/2
✓ Branch 0 taken 10632 times.
✓ Branch 1 taken 3544 times.
14176 for (int i = 0; i < bytesPerPixel; i++) {
1018 10632 b |= data[s + i];
1019 }
1020
1/2
✓ Branch 0 taken 3544 times.
✗ Branch 1 not taken.
3544 if (b) {
1021
2/2
✓ Branch 0 taken 10632 times.
✓ Branch 1 taken 3544 times.
14176 for (int i = 0; i < bytesPerPixel; i++, c++, s++) {
1022
1/2
✓ Branch 0 taken 10632 times.
✗ Branch 1 not taken.
10632 if (channelMap[c] != FPPD_OFF_CHANNEL) {
1023 10632 channelData[channelMap[c]] = data[s];
1024 }
1025 }
1026 } else {
1027 ✗ s += bytesPerPixel;
1028 ✗ c += bytesPerPixel;
1029 }
1030 }
1031 }
1032 3927 c += rowWrap;
1033 }
1034 1860 dirtyBuffer = true;
1035 }
1036
1037 // Clip a srcW x srcH rect placed at (xOffset, yOffset) against a w x h model.
1038 // On success sx/sy are the first source pixel to read, dx/dy where it lands,
1039 // and cw/ch the visible size. Returns false when nothing is visible.
1040 46969 static bool clipBlitRect(int srcW, int srcH, int xOffset, int yOffset,
1041 int w, int h,
1042 int& sx, int& sy, int& dx, int& dy, int& cw, int& ch) {
1043 46969 sx = 0;
1044 46969 sy = 0;
1045 46969 dx = xOffset;
1046 46969 dy = yOffset;
1047 46969 cw = srcW;
1048 46969 ch = srcH;
1049
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 46969 times.
46969 if (dx < 0) {
1050 ✗ sx = -dx;
1051 ✗ cw += dx;
1052 ✗ dx = 0;
1053 }
1054
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 46969 times.
46969 if (dy < 0) {
1055 ✗ sy = -dy;
1056 ✗ ch += dy;
1057 ✗ dy = 0;
1058 }
1059
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 46968 times.
46969 if (cw > (w - dx)) {
1060 1 cw = w - dx;
1061 }
1062
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 46968 times.
46969 if (ch > (h - dy)) {
1063 1 ch = h - dy;
1064 }
1065
2/4
✓ Branch 0 taken 46969 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 46969 times.
✗ Branch 3 not taken.
46969 return (cw > 0) && (ch > 0);
1066 }
1067
1068 // Expand one source pixel to the model's channel layout. srcBpp 1 is greyscale
1069 // (replicated to RGB), 3 is RGB, 4 is RGBW. When an RGBW source lands on an RGB
1070 // model the white is folded into RGB the same way getOverlayPixelValue() does,
1071 // so it dims rather than disappearing.
1072 24758692 static inline void convertPixel(const uint8_t* s, int srcBpp, uint8_t* d, int dstBpp) {
1073 24758692 uint8_t px[4] = { 0, 0, 0, 0 };
1074
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 24758692 times.
24758692 if (srcBpp == 1) {
1075 ✗ px[0] = px[1] = px[2] = s[0];
1076 } else {
1077 24758692 px[0] = s[0];
1078 24758692 px[1] = s[1];
1079 24758692 px[2] = s[2];
1080
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 24758684 times.
24758692 if (srcBpp > 3) {
1081 8 px[3] = s[3];
1082 }
1083 }
1084
3/4
✓ Branch 0 taken 24670314 times.
✓ Branch 1 taken 88378 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 24670314 times.
24758692 if (dstBpp < 4 && px[3]) {
1085 ✗ px[0] = std::min(px[0] + px[3], 255);
1086 ✗ px[1] = std::min(px[1] + px[3], 255);
1087 ✗ px[2] = std::min(px[2] + px[3], 255);
1088 }
1089
3/4
✓ Branch 0 taken 74364454 times.
✓ Branch 1 taken 24758692 times.
✓ Branch 2 taken 74364454 times.
✗ Branch 3 not taken.
99123146 for (int i = 0; i < dstBpp && i < 4; i++) {
1090 74364454 d[i] = px[i];
1091 }
1092 24758692 }
1093
1094 46963 bool PixelOverlayModel::blitData(const uint8_t* src, int srcW, int srcH, int srcBpp,
1095 int xOffset, int yOffset, const PixelOverlayState& st) {
1096
5/10
✓ Branch 0 taken 46963 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 46963 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 46963 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 46963 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 46963 times.
46963 if (!src || srcW <= 0 || srcH <= 0 || srcBpp < 1 || srcBpp > 4) {
1097 ✗ return false;
1098 }
1099 int sx, sy, dx, dy, cw, ch;
1100
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 46963 times.
46963 if (!clipBlitRect(srcW, srcH, xOffset, yOffset, width, height, sx, sy, dx, dy, cw, ch)) {
1101 ✗ return false;
1102 }
1103
1104 // setData()'s rect form wants a tightly packed cw x ch buffer in the
1105 // model's own pixel format, so convert into a temporary rather than
1106 // duplicating its channelMap walk here.
1107 46963 const int bpp = bytesPerPixel;
1108
1/2
✓ Branch 0 taken 46963 times.
✗ Branch 1 not taken.
46963 std::vector<uint8_t> rect((size_t)cw * ch * bpp, 0);
1109
2/2
✓ Branch 0 taken 771894 times.
✓ Branch 1 taken 46963 times.
818857 for (int y = 0; y < ch; y++) {
1110 771894 const uint8_t* s = src + (((size_t)(sy + y) * srcW) + sx) * srcBpp;
1111 771894 uint8_t* d = &rect[(size_t)y * cw * bpp];
1112
2/2
✓ Branch 0 taken 24758434 times.
✓ Branch 1 taken 771894 times.
25530328 for (int x = 0; x < cw; x++, s += srcBpp, d += bpp) {
1113
1/2
✓ Branch 0 taken 24758434 times.
✗ Branch 1 not taken.
24758434 convertPixel(s, srcBpp, d, bpp);
1114 }
1115 }
1116
1/2
✓ Branch 0 taken 46963 times.
✗ Branch 1 not taken.
46963 setData(rect.data(), dx, dy, cw, ch, st);
1117 46963 return true;
1118 46963 }
1119
1120 6 bool PixelOverlayModel::blitOverlayBuffer(const uint8_t* src, int srcW, int srcH, int srcBpp,
1121 int xOffset, int yOffset) {
1122
5/10
✓ 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.
✓ Branch 6 taken 6 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 6 times.
6 if (!src || srcW <= 0 || srcH <= 0 || srcBpp < 1 || srcBpp > 4) {
1123 ✗ return false;
1124 }
1125 int sx, sy, dx, dy, cw, ch;
1126
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (!clipBlitRect(srcW, srcH, xOffset, yOffset, width, height, sx, sy, dx, dy, cw, ch)) {
1127 ✗ return false;
1128 }
1129
1130 6 const int bpp = bytesPerPixel;
1131
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 uint8_t* buf = getOverlayBuffer();
1132
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (!buf) {
1133 ✗ return false;
1134 }
1135
2/2
✓ Branch 0 taken 27 times.
✓ Branch 1 taken 6 times.
33 for (int y = 0; y < ch; y++) {
1136 27 const uint8_t* s = src + (((size_t)(sy + y) * srcW) + sx) * srcBpp;
1137 27 uint8_t* d = buf + ((size_t)(dy + y) * width + dx) * bpp;
1138
2/2
✓ Branch 0 taken 258 times.
✓ Branch 1 taken 27 times.
285 for (int x = 0; x < cw; x++, s += srcBpp, d += bpp) {
1139
1/2
✓ Branch 0 taken 258 times.
✗ Branch 1 not taken.
258 convertPixel(s, srcBpp, d, bpp);
1140 }
1141 }
1142
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 setOverlayBufferDirty(true);
1143 6 return true;
1144 }
1145
1146 // Sets absolute (0-based) channels startChannel..endChannel of this model to
1147 // value; -1 for either end means the model's own first or last channel. No
1148 // caller in FPP; protected, so kept for plugin subclasses. It used to index channelMap - laid out by
1149 // buffer cell, not by channel - with channel numbers, compute the end from
1150 // the shadowed startChannel parameter, and run an omitted end to -1.
1151 3 void PixelOverlayModel::setValue(uint8_t value, int startChannel, int endChannel) {
1152
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 3 times.
3 if (!channelData || channelCount <= 0) {
1153 ✗ return;
1154 }
1155
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 times.
3 int64_t start = (startChannel == -1) ? 0 : (int64_t)startChannel - this->startChannel;
1156
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 2 times.
3 int64_t end = (endChannel == -1) ? channelCount - 1 : (int64_t)endChannel - this->startChannel;
1157 3 start = std::max<int64_t>(start, 0);
1158 3 end = std::min<int64_t>(end, channelCount - 1);
1159
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (start > end) {
1160 ✗ return;
1161 }
1162 3 memset(channelData + start, value, (size_t)(end - start + 1));
1163 3 dirtyBuffer = true;
1164 }
1165 505222 void PixelOverlayModel::setPixelValue(int x, int y, int r, int g, int b) {
1166 505222 setPixelValue(x, y, r, g, b, 0);
1167 505222 }
1168 542034 void PixelOverlayModel::setPixelValue(int x, int y, int r, int g, int b, int w) {
1169
4/8
✓ Branch 0 taken 542034 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 542034 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 542034 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 542034 times.
542034 if (y >= height || x >= width || x < 0 || y < 0) {
1170 ✗ return;
1171 }
1172 542034 int c = (y * width * bytesPerPixel) + x * bytesPerPixel;
1173 542034 int mapSize = width * height * bytesPerPixel;
1174
2/4
✓ Branch 0 taken 542034 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 542034 times.
542034 if (c >= mapSize || c < 0) {
1175 ✗ return;
1176 }
1177
1/2
✓ Branch 0 taken 542034 times.
✗ Branch 1 not taken.
542034 if (channelMap[c] != FPPD_OFF_CHANNEL) {
1178 542034 channelData[channelMap[c]] = r;
1179 }
1180 542034 c++;
1181
4/8
✓ Branch 0 taken 542034 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 542034 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 542034 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 542034 times.
✗ Branch 7 not taken.
542034 if (channelsPerNode > 1 && c < mapSize && channelMap[c] != FPPD_OFF_CHANNEL) {
1182 542034 channelData[channelMap[c]] = g;
1183 }
1184 542034 c++;
1185
4/8
✓ Branch 0 taken 542034 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 542034 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 542034 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 542034 times.
✗ Branch 7 not taken.
542034 if (channelsPerNode > 2 && c < mapSize && channelMap[c] != FPPD_OFF_CHANNEL) {
1186 542034 channelData[channelMap[c]] = b;
1187 }
1188 542034 c++;
1189
2/8
✗ Branch 0 not taken.
✓ Branch 1 taken 542034 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 542034 times.
542034 if (channelsPerNode > 3 && c < mapSize && channelMap[c] != FPPD_OFF_CHANNEL) {
1190 ✗ channelData[channelMap[c]] = w;
1191 }
1192 542034 dirtyBuffer = true;
1193 }
1194 ✗ void PixelOverlayModel::getPixelValue(int x, int y, int& r, int& g, int& b) {
1195 ✗ int w = 0;
1196 ✗ getPixelValue(x, y, r, g, b, w);
1197 ✗ if (w) {
1198 ✗ r = std::min(r + w, 255);
1199 ✗ g = std::min(g + w, 255);
1200 ✗ b = std::min(b + w, 255);
1201 }
1202 ✗ }
1203 ✗ void PixelOverlayModel::getPixelValue(int x, int y, int& r, int& g, int& b, int& w) {
1204 ✗ if (y >= height || x >= width || x < 0 || y < 0) {
1205 ✗ return;
1206 }
1207 ✗ int c = (y * width * bytesPerPixel) + x * bytesPerPixel;
1208 ✗ int mapSize = width * height * bytesPerPixel;
1209 ✗ if (c >= mapSize || c < 0) {
1210 ✗ return;
1211 }
1212 ✗ if (channelMap[c] != FPPD_OFF_CHANNEL) {
1213 ✗ r = channelData[channelMap[c]];
1214 }
1215 ✗ c++;
1216 ✗ if (channelsPerNode > 1 && c < mapSize && channelMap[c] != FPPD_OFF_CHANNEL) {
1217 ✗ g = channelData[channelMap[c]];
1218 }
1219 ✗ c++;
1220 ✗ if (channelsPerNode > 2 && c < mapSize && channelMap[c] != FPPD_OFF_CHANNEL) {
1221 ✗ b = channelData[channelMap[c]];
1222 }
1223 ✗ c++;
1224 ✗ if (channelsPerNode > 3 && c < mapSize && channelMap[c] != FPPD_OFF_CHANNEL) {
1225 ✗ w = channelData[channelMap[c]];
1226 }
1227 }
1228
1229 ✗ void PixelOverlayModel::setScaledData(uint8_t* data, int w, int h) {
1230 ✗ float ydiff = (float)h / (float)height;
1231 ✗ float xdiff = (float)w / (float)width;
1232
1233 ✗ float newy = 0.0f;
1234 ✗ float newx = 0.0f;
1235 int x, y;
1236
1237 ✗ for (y = 0, newy = 0.0f; y < height; y++, newy += ydiff) {
1238 ✗ int srcY = newy;
1239 ✗ for (x = 0, newx = 0.0f; x < width; x++, newx += xdiff) {
1240 ✗ int srcX = newx;
1241
1242 ✗ int idx = y * width * bytesPerPixel + x * bytesPerPixel;
1243 ✗ int srcidx = srcY * w * bytesPerPixel + srcX * bytesPerPixel;
1244
1245 ✗ for (int ch = 0; ch < bytesPerPixel; ch++) {
1246 ✗ if (ch < channelsPerNode && channelMap[idx] != FPPD_OFF_CHANNEL) {
1247 ✗ channelData[channelMap[idx]] = data[srcidx];
1248 }
1249 ✗ idx++;
1250 ✗ srcidx++;
1251 }
1252 }
1253 }
1254 ✗ dirtyBuffer = true;
1255 ✗ }
1256 62 void PixelOverlayModel::clearData() {
1257 62 memset(channelData, 0, channelCount);
1258 62 dirtyBuffer = true;
1259 62 }
1260 ✗ void PixelOverlayModel::fillData(int r, int g, int b) {
1261 ✗ fillData(r, g, b, 0);
1262 ✗ }
1263 1202 void PixelOverlayModel::fillData(int r, int g, int b, int w) {
1264
2/2
✓ Branch 0 taken 2796 times.
✓ Branch 1 taken 1202 times.
3998 for (int y = 0; y < height; y++) {
1265
2/2
✓ Branch 0 taken 36812 times.
✓ Branch 1 taken 2796 times.
39608 for (int x = 0; x < width; x++) {
1266 36812 setPixelValue(x, y, r, g, b, w);
1267 }
1268 }
1269 1202 }
1270
1271 162 void PixelOverlayModel::getDataJson(Json::Value& v, bool rle) {
1272 162 int bpp = bytesPerPixel;
1273
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 162 times.
162 if (rle) {
1274 ✗ std::vector<unsigned char> cur(bpp, 0);
1275 ✗ std::vector<unsigned char> last(bpp, 0);
1276 ✗ int count = 0;
1277 ✗ for (int c = 0; c < height * width * bpp; c += bpp) {
1278 ✗ for (int ch = 0; ch < bpp; ch++) {
1279 ✗ cur[ch] = (channelMap[c + ch] != FPPD_OFF_CHANNEL) ? channelData[channelMap[c + ch]] : 0;
1280 }
1281
1282 ✗ if (cur == last) {
1283 ✗ count++;
1284 } else {
1285 ✗ if (count) {
1286 ✗ v.append(count);
1287 ✗ for (int ch = 0; ch < bpp; ch++) {
1288 ✗ v.append(last[ch]);
1289 }
1290 }
1291 ✗ count = 1;
1292 ✗ last = cur;
1293 }
1294 }
1295
1296 ✗ if (count) {
1297 ✗ v.append(count);
1298 ✗ for (int ch = 0; ch < bpp; ch++) {
1299 ✗ v.append(cur[ch]);
1300 }
1301 }
1302 ✗ } else {
1303
2/2
✓ Branch 0 taken 314476 times.
✓ Branch 1 taken 162 times.
314638 for (int c = 0; c < height * width * bpp; c++) {
1304 314476 unsigned char i = 0;
1305
1/2
✓ Branch 0 taken 314476 times.
✗ Branch 1 not taken.
314476 if (channelMap[c] != FPPD_OFF_CHANNEL) {
1306 314476 i = channelData[channelMap[c]];
1307 }
1308
2/4
✓ Branch 0 taken 314476 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 314476 times.
✗ Branch 3 not taken.
314476 v.append(i);
1309 }
1310 }
1311 162 }
1312
1313 163 void PixelOverlayModel::getData(std::vector<uint8_t>& out) {
1314 163 const int bpp = bytesPerPixel;
1315 163 const size_t sz = (size_t)width * height * bpp;
1316 163 out.resize(sz);
1317
2/2
✓ Branch 0 taken 694096 times.
✓ Branch 1 taken 163 times.
694259 for (size_t c = 0; c < sz; c++) {
1318
1/2
✓ Branch 0 taken 694096 times.
✗ Branch 1 not taken.
694096 out[c] = (channelMap[c] != FPPD_OFF_CHANNEL) ? channelData[channelMap[c]] : 0;
1319 }
1320 163 }
1321
1322 114799 bool PixelOverlayModel::needRefresh() {
1323
3/4
✓ Branch 0 taken 112372 times.
✓ Branch 1 taken 2427 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 112372 times.
114799 return (dirtyBuffer || overlayBufferIsDirty());
1324 }
1325
1326 502 void PixelOverlayModel::setBufferIsDirty(bool dirty) {
1327 502 dirtyBuffer = dirty;
1328 502 }
1329
1330 // Created on first use, which can be an HTTP thread and an effect thread at
1331 // once: both saw null, both created it, and one mapping was lost - along with
1332 // whatever that thread had already drawn into it. Serialized here (allocation
1333 // happens once per model, so one lock for all of them is fine; a member would
1334 // change the plugin-visible layout), and published with release so the
1335 // channel output thread, which reads the pointer unlocked, never sees it
1336 // before its header is filled in.
1337 3211470 uint8_t* PixelOverlayModel::getOverlayBuffer() {
1338 3211470 OverlayBufferData* ob = __atomic_load_n(&overlayBufferData, __ATOMIC_ACQUIRE);
1339
2/2
✓ Branch 0 taken 119 times.
✓ Branch 1 taken 3211351 times.
3211470 if (!ob) {
1340
1/2
✓ Branch 0 taken 119 times.
✗ Branch 1 not taken.
119 std::lock_guard<std::mutex> l(s_overlayBufferAllocLock);
1341 119 ob = overlayBufferData;
1342
2/2
✓ Branch 0 taken 117 times.
✓ Branch 1 taken 2 times.
119 if (!ob) {
1343 117 uint32_t size = width * height * bytesPerPixel + sizeof(OverlayBufferData);
1344 // A retired model's name may be its replacement's by now.
1345
1/2
✓ Branch 0 taken 117 times.
✗ Branch 1 not taken.
117 std::string shmName = s_namesReleased.count(this) ? std::string()
1346
8/24
✗ Branch 0 not taken.
✓ Branch 1 taken 117 times.
✓ Branch 2 taken 117 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 117 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 117 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 117 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 117 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 117 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 117 times.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
✗ Branch 20 not taken.
✗ Branch 21 not taken.
✗ Branch 22 not taken.
✗ Branch 23 not taken.
468 : modelMemoryName("/FPP-Model-Overlay-Buffer-", "/FPPMB-", name);
1347
1/2
✓ Branch 0 taken 117 times.
✗ Branch 1 not taken.
117 ob = (OverlayBufferData*)createModelMemory(shmName, size, overlayBufferMapped);
1348
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 117 times.
117 if (!ob) {
1349 // Nothing left to hand back. Callers check, because the alternative
1350 // was dereferencing this null a few instructions from here.
1351 ✗ WarningHolder::AddWarning(WARNING_ID_MODEL_MEMORY, MODEL_MEMORY_WARNING);
1352 ✗ return nullptr;
1353 }
1354 117 ob->width = width;
1355 117 ob->height = height;
1356 117 ob->flags = (bytesPerPixel << 8); // store bytesPerPixel in bits 8-15
1357 117 __atomic_store_n(&overlayBufferData, ob, __ATOMIC_RELEASE);
1358
1/2
✓ Branch 0 taken 117 times.
✗ Branch 1 not taken.
117 }
1359
1/2
✓ Branch 0 taken 119 times.
✗ Branch 1 not taken.
119 }
1360 3211470 return ob->data;
1361 }
1362
1363 4604 void PixelOverlayModel::clearOverlayBuffer() {
1364
1/2
✓ Branch 0 taken 4604 times.
✗ Branch 1 not taken.
4604 if (uint8_t* buf = getOverlayBuffer()) {
1365 4604 memset(buf, 0, width * height * bytesPerPixel);
1366 }
1367 4604 }
1368 34787 void PixelOverlayModel::fillOverlayBuffer(int r, int g, int b) {
1369 34787 fillOverlayBuffer(r, g, b, 0);
1370 34787 }
1371 37770 void PixelOverlayModel::fillOverlayBuffer(int r, int g, int b, int w) {
1372 37770 uint8_t* data = getOverlayBuffer();
1373
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 37770 times.
37770 if (!data) {
1374 ✗ return;
1375 }
1376
2/2
✓ Branch 0 taken 2399852 times.
✓ Branch 1 taken 37770 times.
2437622 for (int p = 0; p < (width * height); p++) {
1377 2399852 data[0] = r;
1378 2399852 data[1] = g;
1379 2399852 data[2] = b;
1380
2/2
✓ Branch 0 taken 50 times.
✓ Branch 1 taken 2399802 times.
2399852 if (bytesPerPixel > 3) {
1381 50 data[3] = w;
1382 }
1383 2399852 data += bytesPerPixel;
1384 }
1385 }
1386
1387 ✗ void PixelOverlayModel::setOverlayPixelValue(int x, int y, int r, int g, int b) {
1388 ✗ setOverlayPixelValue(x, y, r, g, b, 0);
1389 ✗ }
1390 2373136 void PixelOverlayModel::setOverlayPixelValue(int x, int y, int r, int g, int b, int w) {
1391
4/8
✓ Branch 0 taken 2373136 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2373136 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2373136 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 2373136 times.
2373136 if (y >= height || x >= width || x < 0 || y < 0) {
1392 ✗ return;
1393 }
1394 2373136 int idx = y * width * bytesPerPixel + x * bytesPerPixel;
1395 2373136 uint8_t* buf = getOverlayBuffer();
1396
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2373136 times.
2373136 if (!buf) {
1397 ✗ return;
1398 }
1399 2373136 buf[idx++] = r;
1400 2373136 buf[idx++] = g;
1401 2373136 buf[idx++] = b;
1402
2/2
✓ Branch 0 taken 1320 times.
✓ Branch 1 taken 2371816 times.
2373136 if (bytesPerPixel > 3) {
1403 1320 buf[idx] = w;
1404 }
1405 }
1406 ✗ void PixelOverlayModel::getOverlayPixelValue(int x, int y, int& r, int& g, int& b) {
1407 ✗ int w = 0;
1408 ✗ getOverlayPixelValue(x, y, r, g, b, w);
1409 ✗ if (w) {
1410 ✗ r = std::min(r + w, 255);
1411 ✗ g = std::min(g + w, 255);
1412 ✗ b = std::min(b + w, 255);
1413 }
1414 ✗ }
1415 791198 void PixelOverlayModel::getOverlayPixelValue(int x, int y, int& r, int& g, int& b, int& w) {
1416
4/8
✓ Branch 0 taken 791198 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 791198 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 791198 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 791198 times.
791198 if (y >= height || x >= width || x < 0 || y < 0) {
1417 ✗ r = g = b = w = 0;
1418 ✗ return;
1419 }
1420 791198 int idx = y * width * bytesPerPixel + x * bytesPerPixel;
1421 791198 uint8_t* buf = getOverlayBuffer();
1422
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 791198 times.
791198 if (!buf) {
1423 ✗ r = g = b = w = 0;
1424 ✗ return;
1425 }
1426 791198 r = buf[idx++];
1427 791198 g = buf[idx++];
1428 791198 b = buf[idx++];
1429
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 791198 times.
791198 w = (bytesPerPixel > 3) ? buf[idx] : 0;
1430 }
1431
1432 // Neither of these allocates the overlay buffer. One that nobody has written
1433 // to is all zeros, and copying it out blanked whatever the model's channels
1434 // held: PUT .../fill with RGB filled the channels directly (the model had no
1435 // overlay buffer yet), then flushed, and the model went black.
1436 52161 void PixelOverlayModel::flushOverlayBuffer() {
1437 52161 OverlayBufferData* ob = __atomic_load_n(&overlayBufferData, __ATOMIC_ACQUIRE);
1438
2/2
✓ Branch 0 taken 653 times.
✓ Branch 1 taken 51508 times.
52161 if (!ob) {
1439 653 return;
1440 }
1441 51508 setData(ob->data);
1442 51508 ob->flags &= ~0x1;
1443 }
1444
1445 144808 bool PixelOverlayModel::overlayBufferIsDirty() {
1446 // Allocated on first use from any thread (getOverlayBuffer()), so read the
1447 // pointer the way it is published.
1448 144808 OverlayBufferData* ob = __atomic_load_n(&overlayBufferData, __ATOMIC_ACQUIRE);
1449
4/4
✓ Branch 0 taken 18010 times.
✓ Branch 1 taken 126798 times.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 18008 times.
144808 return ob && (ob->flags & 0x1);
1450 }
1451
1452 6 void PixelOverlayModel::setOverlayBufferDirty(bool dirty) {
1453 6 OverlayBufferData* ob = __atomic_load_n(&overlayBufferData, __ATOMIC_ACQUIRE);
1454
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (!ob) {
1455 ✗ return;
1456 }
1457
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 if (dirty)
1458 6 ob->flags |= 0x1;
1459 else
1460 ✗ ob->flags &= ~0x1;
1461 }
1462
1463 ✗ void PixelOverlayModel::setOverlayBufferScaledData(uint8_t* data, int w, int h) {
1464 ✗ float ydiff = (float)h / (float)height;
1465 ✗ float xdiff = (float)w / (float)width;
1466 ✗ uint8_t* buf = getOverlayBuffer();
1467 ✗ if (!buf) {
1468 ✗ return;
1469 }
1470
1471 ✗ float newy = 0.0f;
1472 ✗ float newx = 0.0f;
1473 int x, y;
1474
1475 ✗ for (y = 0, newy = 0.0f; y < height; y++, newy += ydiff) {
1476 ✗ int srcY = newy;
1477 ✗ for (x = 0, newx = 0.0f; x < width; x++, newx += xdiff) {
1478 ✗ int srcX = newx;
1479
1480 ✗ int idx = y * width * bytesPerPixel + x * bytesPerPixel;
1481 ✗ int srcidx = srcY * w * bytesPerPixel + srcX * bytesPerPixel;
1482
1483 ✗ for (int ch = 0; ch < bytesPerPixel; ch++) {
1484 ✗ buf[idx + ch] = data[srcidx + ch];
1485 }
1486 }
1487 }
1488 }
1489
1490 // A file name the image directory can hold: no directory part, nothing
1491 // hidden or relative ("..", ".foo"), no control characters. fppd runs as
1492 // root, so "../../boot/firmware/config.txt" overwrote that file with PNG data.
1493 3 bool PixelOverlayModel::isPlainImageFileName(const std::string& f) {
1494
5/6
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 2 times.
3 if (f.empty() || f[0] == '.') {
1495 1 return false;
1496 }
1497
2/2
✓ Branch 0 taken 20 times.
✓ Branch 1 taken 2 times.
22 for (unsigned char c : f) {
1498
4/8
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 20 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 20 times.
20 if (c == '/' || c == '\\' || c < 0x20 || c == 0x7F) {
1499 ✗ return false;
1500 }
1501 }
1502 2 return true;
1503 }
1504
1505 1 void PixelOverlayModel::saveOverlayAsImage(std::string filename) {
1506
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 (filename != "" && !isPlainImageFileName(filename)) {
1507 ✗ LogWarn(VB_CHANNELOUT, "Not saving model \"%s\" as \"%s\": the name must be a plain file name\n",
1508 name.c_str(), filename.c_str());
1509 ✗ return;
1510 }
1511
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
1 if (width <= 0 || height <= 0) {
1512 ✗ return;
1513 }
1514
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 Magick::Blob blob;
1515
3/6
✓ 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.
1 Magick::Image image(Magick::Geometry(width, height), Magick::Color("black"));
1516
1517
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
1 if (filename == "") {
1518 struct timeval tv;
1519 struct tm tm;
1520 char timeStr[32];
1521
1522 ✗ gettimeofday(&tv, nullptr);
1523 ✗ localtime_r(&tv.tv_sec, &tm);
1524
1525 ✗ snprintf(timeStr, sizeof(timeStr),
1526 "%4d%02d%02d_%02d%02d%02d",
1527 ✗ 1900 + tm.tm_year, tm.tm_mon + 1,
1528 tm.tm_mday, tm.tm_hour, tm.tm_min,
1529 tm.tm_sec);
1530
1531 ✗ filename = name + "-" + timeStr + ".png";
1532 }
1533
1534
4/8
✓ 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 taken 1 times.
✗ Branch 7 not taken.
1 filename = FPP_DIR_IMAGE("/") + filename;
1535
1536
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogDebug(VB_CHANNELOUT, "Saving %dx%d PixelOverlayModel image to: %s\n",
1537 width, height, filename.c_str());
1538
1539 // Always save as RGB (3 bytes per pixel) for image compatibility
1540 // For RGBW models, blend the W channel into RGB so white pixels render correctly
1541
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 std::vector<uint8_t> data(width * height * 3, 0);
1542
1543
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 1 times.
6 for (int y = 0; y < height; y++) {
1544
2/2
✓ Branch 0 taken 50 times.
✓ Branch 1 taken 5 times.
55 for (int x = 0; x < width; x++) {
1545 50 int mapIdx = (y * width + x) * bytesPerPixel;
1546 50 int imgIdx = (y * width + x) * 3;
1547
2/2
✓ Branch 0 taken 150 times.
✓ Branch 1 taken 50 times.
200 for (int ch = 0; ch < 3; ch++) {
1548
3/6
✓ Branch 0 taken 150 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 150 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 150 times.
✗ Branch 5 not taken.
150 if (ch < bytesPerPixel && channelMap[mapIdx + ch] != FPPD_OFF_CHANNEL) {
1549 150 data[imgIdx + ch] = channelData[channelMap[mapIdx + ch]];
1550 }
1551 }
1552
2/6
✗ Branch 0 not taken.
✓ Branch 1 taken 50 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 50 times.
50 if (bytesPerPixel > 3 && channelMap[mapIdx + 3] != FPPD_OFF_CHANNEL) {
1553 ✗ uint8_t w = channelData[channelMap[mapIdx + 3]];
1554 ✗ if (w) {
1555 ✗ for (int ch = 0; ch < 3; ch++) {
1556 ✗ int v = data[imgIdx + ch] + w;
1557 ✗ data[imgIdx + ch] = (v > 255) ? 255 : v;
1558 }
1559 }
1560 }
1561 }
1562 }
1563
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 blob.update(&data[0], width * height * 3);
1564
1565 try {
1566
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 image.magick("RGB");
1567
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 image.depth(8);
1568
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 image.quiet(true);
1569
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 image.read(blob, Magick::Geometry(width, height));
1570
1571
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 image.write(filename.c_str());
1572 ✗ } catch (const std::exception& e) {
1573 ✗ LogErr(VB_CHANNELOUT, "Could not save model \"%s\" to %s: %s\n", name.c_str(), filename.c_str(), e.what());
1574 ✗ return;
1575 ✗ }
1576
1577
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 SetFilePerms(filename);
1578
3/6
✓ 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.
1 }
1579
1580 62 void PixelOverlayModel::clear() {
1581
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 62 times.
62 if (__atomic_load_n(&overlayBufferData, __ATOMIC_ACQUIRE)) {
1582 ✗ clearOverlayBuffer();
1583 ✗ flushOverlayBuffer();
1584 } else {
1585 62 clearData();
1586 }
1587 62 }
1588 4184 void PixelOverlayModel::fill(int r, int g, int b) {
1589 4184 fill(r, g, b, 0);
1590 4184 }
1591 4184 void PixelOverlayModel::fill(int r, int g, int b, int w) {
1592
2/2
✓ Branch 0 taken 2982 times.
✓ Branch 1 taken 1202 times.
4184 if (__atomic_load_n(&overlayBufferData, __ATOMIC_ACQUIRE)) {
1593 2982 fillOverlayBuffer(r, g, b, w);
1594 2982 flushOverlayBuffer();
1595 } else {
1596 1202 fillData(r, g, b, w);
1597 }
1598 4184 }
1599
1600 2 int PixelOverlayModel::getStartChannel() const {
1601 2 return startChannel;
1602 }
1603 2 int PixelOverlayModel::getChannelCount() const {
1604 2 return channelCount;
1605 }
1606 971289 void PixelOverlayModel::toJson(Json::Value& result) {
1607 971289 result = config;
1608 971289 }
1609
1610 16596 int32_t PixelOverlayModel::updateRunningEffects() {
1611
1/2
✓ Branch 0 taken 16596 times.
✗ Branch 1 not taken.
16596 std::unique_lock<std::recursive_mutex> l(effectLock);
1612
2/2
✓ Branch 0 taken 16461 times.
✓ Branch 1 taken 135 times.
16596 if (runningEffect) {
1613
1/2
✓ Branch 0 taken 16461 times.
✗ Branch 1 not taken.
16461 int32_t v = runningEffect->update();
1614
2/2
✓ Branch 0 taken 382 times.
✓ Branch 1 taken 16079 times.
16461 if (v == 0) {
1615
1/2
✓ Branch 0 taken 382 times.
✗ Branch 1 not taken.
382 delete runningEffect;
1616 382 runningEffect = nullptr;
1617 }
1618 16461 return v;
1619 }
1620 135 return 0;
1621 16596 }
1622
1623 72028 void PixelOverlayModel::setRunningEffect(RunningEffect* ef, int32_t firstUpdateMS) {
1624
1/2
✓ Branch 0 taken 72028 times.
✗ Branch 1 not taken.
72028 std::unique_lock<std::recursive_mutex> l(effectLock);
1625
2/2
✓ Branch 0 taken 37234 times.
✓ Branch 1 taken 34794 times.
72028 if (runningEffect) {
1626
2/2
✓ Branch 0 taken 37044 times.
✓ Branch 1 taken 190 times.
37234 if (runningEffect != ef) {
1627
1/2
✓ Branch 0 taken 37044 times.
✗ Branch 1 not taken.
37044 delete runningEffect;
1628 }
1629
1/2
✓ Branch 0 taken 37234 times.
✗ Branch 1 not taken.
37234 PixelOverlayManager::INSTANCE.removePeriodicUpdate(this);
1630 }
1631 72028 runningEffect = ef;
1632
1/2
✓ Branch 0 taken 72028 times.
✗ Branch 1 not taken.
72028 PixelOverlayManager::INSTANCE.addPeriodicUpdate(firstUpdateMS, this);
1633 72028 }
1634
1635 125 void PixelOverlayModel::clearRunningEffect() {
1636
1/2
✓ Branch 0 taken 125 times.
✗ Branch 1 not taken.
125 std::unique_lock<std::recursive_mutex> l(effectLock);
1637
2/2
✓ Branch 0 taken 22 times.
✓ Branch 1 taken 103 times.
125 if (runningEffect) {
1638
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
22 delete runningEffect;
1639 22 runningEffect = nullptr;
1640
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
22 PixelOverlayManager::INSTANCE.removePeriodicUpdate(this);
1641 }
1642 125 }
1643
1644 37872 bool PixelOverlayModel::applyEffect(const std::string& autoState, const std::string& effect, const std::vector<std::string>& args) {
1645 37872 PixelOverlayEffect* pe = PixelOverlayEffect::GetPixelOverlayEffect(effect);
1646
2/2
✓ Branch 0 taken 37870 times.
✓ Branch 1 taken 2 times.
37872 if (pe) {
1647 // Every effect indexes args[] positionally with no bounds check of its
1648 // own, so a caller that supplies fewer than the effect declares walks
1649 // off the end -- "Overlay Model Effect,<model>,Enabled,Text" with no
1650 // further arguments is a one-line HTTP request that segfaults fppd in
1651 // TextEffect::apply's args[0]. Validate once here, at the single point
1652 // that dispatches to any effect, rather than bounds-checking every
1653 // effect body. The effect's own declared arg list is the authority,
1654 // and trailing args marked optional may legitimately be omitted.
1655 37870 size_t required = 0;
1656 37870 size_t idx = 0;
1657
2/2
✓ Branch 0 taken 19032 times.
✓ Branch 1 taken 37870 times.
56902 for (const auto& a : pe->args) {
1658 19032 ++idx;
1659
2/2
✓ Branch 0 taken 17080 times.
✓ Branch 1 taken 1952 times.
19032 if (!a.optional) {
1660 17080 required = idx;
1661 }
1662 }
1663
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 37866 times.
37870 if (args.size() < required) {
1664 4 LogErr(VB_CHANNELOUT,
1665 "Pixel Overlay effect '%s' needs at least %zu argument(s), %zu given -- ignoring\n",
1666 effect.c_str(), required, args.size());
1667 4 return false;
1668 }
1669 37866 return pe->apply(this, autoState, args);
1670 }
1671 2 LogWarn(VB_CHANNELOUT, "Pixel Overlay effect '%s' does not exist -- ignoring\n", effect.c_str());
1672 2 return false;
1673 }
1674