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