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