GCC Code Coverage Report


Directory: ./
File: overlays/PixelOverlayModelSub.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 208 228 91.2%
Functions: 12 13 92.3%
Branches: 197 330 59.7%

Line Branch Exec Source
1 /*
2 * This file is part of the Falcon Player (FPP) and is Copyright (C)
3 * 2013-2022 by the Falcon Player Developers.
4 *
5 * The Falcon Player (FPP) is free software, and is covered under
6 * multiple Open Source licenses. Please see the included 'LICENSES'
7 * file for descriptions of what files are covered by each license.
8 *
9 * This source file is covered under the LGPL v2.1 as described in the
10 * included LICENSE.LGPL file.
11 */
12
13 #include "fpp-pch.h"
14
15 #include "fpp-json.h"
16
17 #include <algorithm>
18 #include <cstring>
19
20 #include "../Sequence.h" // FPPD_OFF_CHANNEL
21 #include "../log.h"
22 #include "../common_mini.h"
23
24 #include "PixelOverlay.h"
25 #include "PixelOverlayModelSub.h"
26
27 24 PixelOverlayModelSub::PixelOverlayModelSub(const Json::Value& c) :
28 24 PixelOverlayModel(c) {
29
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 24 times.
24 if (!isValid()) {
30 // The base class rejected the definition; the manager drops it.
31 ✗ return;
32 }
33 // Same type tolerance as the base class: these come from the same
34 // unchecked files, and asInt()/asString() throw.
35 24 std::string parentName, subType, grid;
36 24 int parentStart = 0;
37
3/6
✓ 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.
48 if (!configInt(config, "XOffset", 0, xOffset) || !configInt(config, "YOffset", 0, yOffset) ||
38
2/4
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 24 times.
✗ Branch 3 not taken.
24 !configInt(config, "ParentStartChannel", 0, parentStart) ||
39
6/12
✓ 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 taken 24 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 24 times.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✓ Branch 11 taken 24 times.
72 !configString(config, "Parent", parentName) || !configString(config, "SubType", subType) ||
40
2/4
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 24 times.
24 !configString(config, "Grid", grid)) {
41 ✗ markInvalid("XOffset, YOffset, ParentStartChannel, Parent, SubType or Grid has the wrong type");
42 ✗ return;
43 }
44
45
3/4
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✓ Branch 3 taken 19 times.
24 if (config.isMember("SubType")) {
46
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 std::string subType = config["SubType"].asString();
47
3/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✓ Branch 3 taken 2 times.
5 if (subType == "grid") {
48 3 gridMode = true;
49
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 buildGrid();
50
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 } else if (subType == "channelgrid") {
51 2 gridMode = true;
52 2 channelGridMode = true;
53
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 buildChannelGrid();
54 }
55 5 }
56
3/6
✓ 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.
24 }
57
58 // Channel-grid (model group) build: each grid cell is a '&'-separated list of
59 // 1-based absolute start channels (the member pixels binned into that cell). We
60 // index outputCells by the channelData slot the base assigned to each cell, so
61 // the scatter reads the right buffer color.
62 2 void PixelOverlayModelSub::buildChannelGrid() {
63 2 const int cpn = channelsPerNode;
64
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 outputCells.assign((size_t)width * height, std::vector<uint32_t>());
65
66
5/14
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✓ Branch 11 taken 2 times.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
2 std::string grid = config.isMember("Grid") ? config["Grid"].asString() : "";
67
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 std::vector<std::string> rows = split(grid, ';');
68
69
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 2 times.
6 for (int y = 0; y < height; y++) {
70 4 std::vector<std::string> cells;
71
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 if (y < (int)rows.size()) {
72
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 cells = split(rows[y], ',');
73 }
74
2/2
✓ Branch 0 taken 12 times.
✓ Branch 1 taken 4 times.
16 for (int x = 0; x < width; x++) {
75
6/6
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 8 times.
✓ Branch 4 taken 4 times.
✓ Branch 5 taken 8 times.
12 if (x >= (int)cells.size() || cells[x].empty()) {
76 4 continue; // hole
77 }
78 8 int buf = (y * width + x) * bytesPerPixel;
79
3/6
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 8 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 8 times.
8 if (buf < 0 || buf >= (int)channelMap.size()) {
80 ✗ continue;
81 }
82 8 uint32_t cd = channelMap[buf];
83
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (cd == FPPD_OFF_CHANNEL) {
84 ✗ continue;
85 }
86 8 int slot = cd / cpn;
87
3/6
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 8 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 8 times.
8 if (slot < 0 || slot >= (int)outputCells.size()) {
88 ✗ continue;
89 }
90
3/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✓ Branch 3 taken 8 times.
22 for (auto& chStr : split(cells[x], '&')) {
91
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 14 times.
14 if (chStr.empty()) {
92 ✗ continue;
93 }
94 14 int ch1 = std::atoi(chStr.c_str()); // 1-based absolute start channel
95 // Same bound as buildGrid(): scattered to on the output thread.
96
2/4
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✗ Branch 3 not taken.
14 if (ch1 > 0 && (int64_t)ch1 - 1 + cpn <= FPPD_MAX_CHANNELS) {
97
1/2
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
14 outputCells[slot].push_back((uint32_t)(ch1 - 1));
98 }
99 8 }
100 }
101 4 }
102 2 }
103
104 // Build outputMap: for each populated grid cell, resolve the parent node number
105 // to the absolute (0-based) output channel base. The base class has already
106 // built a dense width*height buffer + channelMap, so we index outputMap by the
107 // channelData slot the base assigned to each cell, making this robust to the
108 // base orientation mapping.
109 3 void PixelOverlayModelSub::buildGrid() {
110 3 int parentStart0 = 0;
111
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 configInt(config, "ParentStartChannel", 0, parentStart0); // type checked in the constructor
112 3 parentStart0--;
113
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (parentStart0 < 0) {
114 ✗ parentStart0 = 0;
115 }
116 3 const int cpn = channelsPerNode;
117
118
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 outputMap.assign((size_t)width * height, FPPD_OFF_CHANNEL);
119
120
5/14
✓ 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.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✓ Branch 11 taken 3 times.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
3 std::string grid = config.isMember("Grid") ? config["Grid"].asString() : "";
121
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 std::vector<std::string> rows = split(grid, ';');
122
123
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 3 times.
11 for (int y = 0; y < height; y++) {
124 8 std::vector<std::string> cells;
125
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 if (y < (int)rows.size()) {
126
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 cells = split(rows[y], ',');
127 }
128
2/2
✓ Branch 0 taken 40 times.
✓ Branch 1 taken 8 times.
48 for (int x = 0; x < width; x++) {
129 40 int node = -1;
130
6/6
✓ Branch 0 taken 34 times.
✓ Branch 1 taken 6 times.
✓ Branch 2 taken 32 times.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 32 times.
✓ Branch 5 taken 8 times.
40 if (x < (int)cells.size() && !cells[x].empty()) {
131 32 node = std::atoi(cells[x].c_str());
132 }
133
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 32 times.
40 if (node <= 0) {
134 8 continue; // hole
135 }
136 32 int buf = (y * width + x) * bytesPerPixel;
137
3/6
✓ Branch 0 taken 32 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 32 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 32 times.
32 if (buf < 0 || buf >= (int)channelMap.size()) {
138 ✗ continue;
139 }
140 32 uint32_t cd = channelMap[buf];
141
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 32 times.
32 if (cd == FPPD_OFF_CHANNEL) {
142 ✗ continue;
143 }
144 32 int slot = cd / cpn;
145 // doGridOverlay() writes cpn channels at this base on the output
146 // thread, so it has to lie inside the channel buffer; a stale or
147 // corrupt grid could name any node.
148 32 int64_t ob = (int64_t)parentStart0 + (int64_t)(node - 1) * cpn;
149
4/8
✓ Branch 0 taken 32 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 32 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 32 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 32 times.
✗ Branch 7 not taken.
32 if (slot >= 0 && slot < (int)outputMap.size() && ob + cpn <= FPPD_MAX_CHANNELS) {
150 32 outputMap[slot] = (uint32_t)ob;
151 }
152 }
153 8 }
154 3 }
155
156 ✗ PixelOverlayModelSub::~PixelOverlayModelSub() {
157 ✗ }
158
159 // Resolve the parent model, caching the pointer.
160 //
161 // MUST NOT be called from the channel output thread. The output thread reaches
162 // doOverlay() from PixelOverlayManager::doOverlays() with activeModelsLock
163 // already held, and every HTTP handler takes modelsLock before
164 // activeModelsLock (via setState() -> modelStateChanged()), so a lookup from
165 // that side would invert them. Callers are setState() below and
166 // readyForPolarJoin() (effect start), and the resolution itself only
167 // try-locks modelsLock: setState() can also run on the effect update threads
168 // (an effect stopping calls model->setState() under this model's
169 // effectLock), where BLOCKING on modelsLock is the reverse of the
170 // HTTP/command paths' modelsLock -> effectLock order and deadlocks outright --
171 // the same shape 37c8f92a8 fixed one lock over. The try-acquire never blocks:
172 // from the manager paths (which already hold modelsLock recursively) it always
173 // succeeds, and a contended effect-side resolve just stays unresolved until
174 // the next setState().
175 147230 bool PixelOverlayModelSub::foundParent() {
176 {
177
1/2
✓ Branch 0 taken 147230 times.
✗ Branch 1 not taken.
147230 std::lock_guard<std::mutex> l(parentLock);
178
2/2
✓ Branch 0 taken 131681 times.
✓ Branch 1 taken 15549 times.
147230 if (parent)
179 131681 return true;
180
2/2
✓ Branch 0 taken 15549 times.
✓ Branch 1 taken 131681 times.
147230 }
181
182 15549 PixelOverlayManager& mgr = PixelOverlayManager::INSTANCE;
183 15549 std::unique_lock<std::recursive_mutex> lock(mgr.modelsLock, std::try_to_lock);
184
2/2
✓ Branch 0 taken 56 times.
✓ Branch 1 taken 15493 times.
15549 if (!lock.owns_lock()) {
185 // modelsLock contended and not ours; do not log -- the parent may be
186 // perfectly valid, we just can't look it up without risking deadlock.
187 56 return false;
188 }
189
3/6
✓ Branch 0 taken 15493 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15493 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 15493 times.
✗ Branch 5 not taken.
15493 PixelOverlayModel* p = mgr.getModelLocked(config["Parent"].asString());
190
191 // Refuse a parent chain that leads back here: a submodel naming itself
192 // (the Parent picker lists every model, this one included) or a loop of
193 // them. doOverlay() calls parent->setData() then parent->doOverlay(), and
194 // with this model somewhere up the chain that never returned - the channel
195 // output thread recursed until its stack overflowed. Grid submodels end
196 // the walk: they scatter to output channels and never call their parent.
197 15493 constexpr int MAX_SUBMODEL_DEPTH = 16;
198 15493 PixelOverlayModel* a = p;
199
2/2
✓ Branch 0 taken 34 times.
✓ Branch 1 taken 15461 times.
15495 for (int depth = 0; a; depth++) {
200
3/4
✓ Branch 0 taken 31 times.
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 31 times.
34 if (a == this || depth >= MAX_SUBMODEL_DEPTH) {
201
3/6
✓ 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.
3 LogErr(VB_CHANNELOUT, "Submodel \"%s\" cannot use \"%s\" as its parent: the parent chain leads back to it\n",
202 name.c_str(), config["Parent"].asString().c_str());
203 3 return false;
204 }
205
1/2
✓ Branch 0 taken 31 times.
✗ Branch 1 not taken.
31 PixelOverlayModelSub* s = dynamic_cast<PixelOverlayModelSub*>(a);
206
3/4
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 29 times.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
31 if (!s || s->gridMode) {
207 break;
208 }
209
3/6
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
2 a = mgr.getModelLocked(s->config["Parent"].asString());
210 }
211
212
2/2
✓ Branch 0 taken 29 times.
✓ Branch 1 taken 15461 times.
15490 if (p) {
213 // Under modelsLock, so a reload can't retire it (and resetParent()
214 // this model) between the lookup and here.
215
1/2
✓ Branch 0 taken 29 times.
✗ Branch 1 not taken.
29 std::lock_guard<std::mutex> l(parentLock);
216 29 parent = p;
217
1/2
✓ Branch 0 taken 29 times.
✗ Branch 1 not taken.
29 parentRef = mgr.models[p->getName()].owner;
218 29 return true;
219 29 }
220
221
3/6
✓ Branch 0 taken 15461 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15461 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 15461 times.
✗ Branch 5 not taken.
15461 LogErr(VB_CHANNELOUT, "Unable to find parent \"%s\" for submodel \"%s\"\n",
222 config["Parent"].asString().c_str(), name.c_str());
223 15461 return false;
224 15549 }
225 147246 void PixelOverlayModelSub::setState(const PixelOverlayState& st) {
226
2/2
✓ Branch 0 taken 18 times.
✓ Branch 1 taken 147228 times.
147246 if (gridMode) {
227 // Grid submodels scatter directly to the parent's output channels; they
228 // never dereference `parent`, so they need no resolution at all, and
229 // they do not register as a rectangular child of the parent.
230
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 PixelOverlayModel::setState(st);
231 18 return;
232 }
233
234 // Resolve BEFORE the base class publishes this model into the manager's
235 // active list. From the moment it lands there the channel output thread can
236 // call doOverlay() on it, and resolving from that side takes modelsLock
237 // while doOverlays() holds activeModelsLock -- the inversion described on
238 // foundParent(). Doing it here is free: setState() runs under modelsLock
239 // on the manager's paths, so this is a recursive re-acquire there.
240 //
241 // Not every path holds modelsLock, though: an effect that disables its
242 // model when it finishes (Stop Effects, Text/Image with disableWhenDone,
243 // WLED autoDisable) gets here from the effect or output thread, while an
244 // output reload's removeAutoOverlayModel() can retire the parent and
245 // resetParent() this model. So the parent is used through a share of its
246 // ownership taken here: it stays alive for the call even if retired
247 // meanwhile (a retired model can't be made active again), and the call
248 // needs no modelsLock, so it is never dropped or delayed because a
249 // command or reload happens to hold that lock.
250
1/2
✓ Branch 0 taken 147228 times.
✗ Branch 1 not taken.
147228 foundParent();
251 147228 std::shared_ptr<PixelOverlayModel> p;
252 {
253
1/2
✓ Branch 0 taken 147228 times.
✗ Branch 1 not taken.
147228 std::lock_guard<std::mutex> l(parentLock);
254 147228 p = parentRef.lock();
255 147228 }
256
257
1/2
✓ Branch 0 taken 147228 times.
✗ Branch 1 not taken.
147228 PixelOverlayModel::setState(st);
258
259
2/2
✓ Branch 0 taken 131708 times.
✓ Branch 1 taken 15520 times.
147228 if (p) {
260
1/2
✓ Branch 0 taken 131708 times.
✗ Branch 1 not taken.
131708 p->setChildState(name, st, xOffset, yOffset, width, height);
261 }
262 147228 }
263 19 void PixelOverlayModelSub::resetParent() {
264 // Disable first: that takes this model out of the active list under
265 // activeModelsLock, which waits out a doOverlay() already reading
266 // `parent` on the channel output thread. Only then is it safe to clear.
267
1/2
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
19 PixelOverlayModel::setState(PixelOverlayState(PixelOverlayState::Disabled));
268
1/2
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
19 std::lock_guard<std::mutex> l(parentLock);
269 19 parent = nullptr;
270 19 parentRef.reset();
271 19 }
272 19 std::string PixelOverlayModelSub::getParentName() const {
273 19 return config["Parent"].asString();
274 }
275
276 80 uint32_t PixelOverlayModelSub::outputChannelForData(uint32_t dataOffset) const {
277
2/2
✓ Branch 0 taken 24 times.
✓ Branch 1 taken 56 times.
80 if (!gridMode) {
278 // A rectangular submodel draws through its parent, so its cells land
279 // wherever the parent's do at the offset cell; the base class's
280 // startChannel arithmetic is meaningless here (startChannel is forced
281 // to 0 for a submodel), and joined the polar layout against channels
282 // 0..N. channelData is node-major at channelsPerNode.
283
2/4
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 24 times.
24 if (!parent || channelsPerNode <= 0) {
284 ✗ return FPPD_OFF_CHANNEL;
285 }
286 24 int node = dataOffset / channelsPerNode;
287 24 int k = dataOffset % channelsPerNode;
288 24 uint32_t ch = parent->outputChannelForCell(node % width + xOffset, node / width + yOffset);
289
1/2
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
24 return ch == FPPD_OFF_CHANNEL ? ch : ch + k;
290 }
291 56 int slot = dataOffset / channelsPerNode;
292
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 56 times.
56 if (channelGridMode) {
293 // A cell may feed several member pixels; they are one buffer cell and
294 // therefore one position, so the first is the representative one.
295 ✗ if (slot < 0 || slot >= (int)outputCells.size() || outputCells[slot].empty()) {
296 ✗ return FPPD_OFF_CHANNEL;
297 }
298 ✗ return outputCells[slot][0];
299 }
300
3/6
✓ Branch 0 taken 56 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 56 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 56 times.
56 if (slot < 0 || slot >= (int)outputMap.size()) {
301 ✗ return FPPD_OFF_CHANNEL;
302 }
303 56 return outputMap[slot];
304 }
305
306 4832 void PixelOverlayModelSub::doOverlay(uint8_t* channels) {
307
2/4
✓ Branch 0 taken 4832 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 4832 times.
4832 if (PixelOverlayModel::overlayBufferIsDirty())
308 ✗ flushOverlayBuffer();
309
310
2/2
✓ Branch 0 taken 117 times.
✓ Branch 1 taken 4715 times.
4832 if (gridMode) {
311
1/2
✓ Branch 0 taken 117 times.
✗ Branch 1 not taken.
117 doGridOverlay(channels);
312 2974 return;
313 }
314
315 // Deliberately the cached pointer, NOT foundParent(): this runs on the
316 // channel output thread with activeModelsLock held, where taking modelsLock
317 // deadlocks (see foundParent()). setState() has already resolved it by the
318 // time we can be reached. Null here means the parent genuinely does not
319 // exist -- a submodel left pointing at a renamed or deleted model -- and
320 // retrying every frame would both spam the log and hold that deadlock
321 // window open permanently rather than for a single lookup. setState()
322 // reports the failure once instead.
323
2/2
✓ Branch 0 taken 987 times.
✓ Branch 1 taken 3728 times.
4715 if (!parent)
324 987 return;
325 // A submodel that no longer fits its parent (the parent's geometry changed
326 // after the submodel was saved) can't be drawn: setChildState() warned
327 // about it once. Asking the parent anyway logged "region is out of
328 // bounds" on every frame an effect touched it.
329
1/2
✓ Branch 0 taken 3728 times.
✗ Branch 1 not taken.
3728 if (xOffset < 0 || yOffset < 0 ||
330
5/6
✓ Branch 0 taken 3728 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3652 times.
✓ Branch 3 taken 76 times.
✓ Branch 4 taken 76 times.
✓ Branch 5 taken 3652 times.
11108 (int64_t)xOffset + width > parent->getWidth() ||
331
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3652 times.
3652 (int64_t)yOffset + height > parent->getHeight()) {
332 76 dirtyBuffer = false;
333 76 return;
334 }
335
336
1/2
✓ Branch 0 taken 3652 times.
✗ Branch 1 not taken.
3652 dirtyBuffer = dirtyBuffer | parent->needRefresh();
337
2/2
✓ Branch 0 taken 1794 times.
✓ Branch 1 taken 1858 times.
3652 if (!dirtyBuffer)
338 1794 return;
339
340 // channelData is packed at this model's node width, but the parent reads
341 // the region at its own bytes per pixel. A UI-made submodel is 3 channels
342 // per node, so on an RGBW parent that read ran a third past the end of
343 // channelData (and shifted every colour). Repack when they differ; this
344 // runs only on the channel output thread, hence the thread_local.
345 1858 const uint8_t* data = channelData;
346 1858 const int pb = parent->getBytesPerPixel();
347
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1857 times.
1858 if (pb != channelsPerNode) {
348
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 static thread_local std::vector<uint8_t> repacked;
349 1 const size_t px = (size_t)width * height;
350 1 const int n = std::min(pb, channelsPerNode);
351
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 repacked.assign(px * pb, 0);
352
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 1 times.
5 for (size_t p = 0; p < px; p++) {
353 4 memcpy(&repacked[p * pb], &channelData[p * channelsPerNode], n);
354 }
355 1 data = repacked.data();
356 }
357
1/2
✓ Branch 0 taken 1858 times.
✗ Branch 1 not taken.
1858 parent->setData(data, xOffset, yOffset, width, height, state);
358
1/2
✓ Branch 0 taken 1858 times.
✗ Branch 1 not taken.
1858 parent->doOverlay(channels);
359 1858 dirtyBuffer = false;
360 }
361
362 // Scatter each populated grid cell's color from channelData (dense, indexed by
363 // the base class buffer slot) to the parent's absolute output channel, honoring
364 // the overlay state. Unlike a normal model's doOverlay() this never does a
365 // bulk copy, so sibling pixels that are not part of the submodel are untouched.
366 // Scatter one node's color (src, cpn bytes) to one output channel base (dst),
367 // honoring the overlay state.
368 1698 static inline void scatterNode(uint8_t* dst, const uint8_t* src, int cpn, int st) {
369
3/4
✓ Branch 0 taken 1522 times.
✓ Branch 1 taken 88 times.
✓ Branch 2 taken 88 times.
✗ Branch 3 not taken.
1698 switch (st) {
370 1522 case 1: // Opaque
371 1522 memcpy(dst, src, cpn);
372 1522 break;
373 88 case 2: // Transparent (per-channel)
374
2/2
✓ Branch 0 taken 264 times.
✓ Branch 1 taken 88 times.
352 for (int k = 0; k < cpn; k++) {
375
2/2
✓ Branch 0 taken 56 times.
✓ Branch 1 taken 208 times.
264 if (src[k]) {
376 56 dst[k] = src[k];
377 }
378 }
379 88 break;
380 88 case 3: { // Transparent RGB(W) (per-node)
381 88 bool any = false;
382
2/2
✓ Branch 0 taken 264 times.
✓ Branch 1 taken 88 times.
352 for (int k = 0; k < cpn; k++) {
383 264 any |= src[k];
384 }
385
2/2
✓ Branch 0 taken 56 times.
✓ Branch 1 taken 32 times.
88 if (any) {
386
2/2
✓ Branch 0 taken 168 times.
✓ Branch 1 taken 56 times.
224 for (int k = 0; k < cpn; k++) {
387 168 dst[k] = src[k];
388 }
389 }
390 88 break;
391 }
392 }
393 1698 }
394
395 117 void PixelOverlayModelSub::doGridOverlay(uint8_t* channels) {
396
1/2
✓ Branch 0 taken 117 times.
✗ Branch 1 not taken.
117 int st = getState().getState();
397
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 117 times.
117 if (st == 0) {
398 ✗ dirtyBuffer = false;
399 ✗ return;
400 }
401 117 const int cpn = channelsPerNode;
402
403
2/2
✓ Branch 0 taken 30 times.
✓ Branch 1 taken 87 times.
117 if (channelGridMode) {
404 // Model group: each cell may map to multiple member channels.
405 30 const int slots = (int)outputCells.size();
406
2/2
✓ Branch 0 taken 180 times.
✓ Branch 1 taken 30 times.
210 for (int slot = 0; slot < slots; slot++) {
407 180 const std::vector<uint32_t>& chans = outputCells[slot];
408
2/2
✓ Branch 0 taken 60 times.
✓ Branch 1 taken 120 times.
180 if (chans.empty()) {
409 60 continue;
410 }
411 120 uint8_t* src = &channelData[slot * cpn];
412
2/2
✓ Branch 0 taken 210 times.
✓ Branch 1 taken 120 times.
330 for (uint32_t ob : chans) {
413 210 scatterNode(&channels[ob], src, cpn, st);
414 }
415 }
416 30 dirtyBuffer = false;
417 30 return;
418 }
419
420 87 const int slots = (int)outputMap.size();
421
2/2
✓ Branch 0 taken 1776 times.
✓ Branch 1 taken 87 times.
1863 for (int slot = 0; slot < slots; slot++) {
422 1776 uint32_t ob = outputMap[slot];
423
2/2
✓ Branch 0 taken 288 times.
✓ Branch 1 taken 1488 times.
1776 if (ob == FPPD_OFF_CHANNEL) {
424 288 continue;
425 }
426 1488 scatterNode(&channels[ob], &channelData[slot * cpn], cpn, st);
427 }
428 87 dirtyBuffer = false;
429 }
430
431 33893 void PixelOverlayModelSub::setData(const uint8_t* data) {
432 // Don't call foundParent() here to avoid potential deadlock
433 // when called from flushOverlayBuffer() while holding runningEffectMutex.
434 // The parent will be resolved in doOverlay() before data is needed.
435 33893 PixelOverlayModel::setData(data);
436
437 33893 dirtyBuffer = true;
438 33893 }
439