| 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 GPL v2 as described in the | ||
| 10 | * included LICENSE.GPL file. | ||
| 11 | */ | ||
| 12 | |||
| 13 | #include "fpp-pch.h" | ||
| 14 | |||
| 15 | #include "fpp-json.h" | ||
| 16 | |||
| 17 | #include <fcntl.h> | ||
| 18 | #include <signal.h> | ||
| 19 | #include <stdint.h> | ||
| 20 | |||
| 21 | #include "../Sequence.h" | ||
| 22 | #include "../log.h" | ||
| 23 | |||
| 24 | #include "ModelPixelStrings.h" | ||
| 25 | #include "ChannelOutputSetup.h" | ||
| 26 | #include "overlays/PixelOverlay.h" | ||
| 27 | |||
| 28 | ///////////////////////////////////////////////////////////////////////////// | ||
| 29 | |||
| 30 | #include "Plugin.h" | ||
| 31 | class ModelPixelStringsPlugin : public FPPPlugins::Plugin, public FPPPlugins::ChannelOutputPlugin { | ||
| 32 | public: | ||
| 33 | 21 | ModelPixelStringsPlugin() : | |
| 34 |
2/4✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 21 times.
✗ Branch 3 not taken.
|
63 | FPPPlugins::Plugin("ModelPixelStrings") { |
| 35 | 21 | } | |
| 36 | 23 | virtual ChannelOutput* createChannelOutput(unsigned int startChannel, unsigned int channelCount) override { | |
| 37 |
2/6✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 23 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
|
46 | return new ModelPixelStringsOutput(startChannel, channelCount); |
| 38 | } | ||
| 39 | }; | ||
| 40 | |||
| 41 | extern "C" { | ||
| 42 | 21 | FPPPlugins::Plugin* createPlugin() { | |
| 43 |
2/6✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 21 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
|
42 | return new ModelPixelStringsPlugin(); |
| 44 | } | ||
| 45 | } | ||
| 46 | /* | ||
| 47 | * | ||
| 48 | */ | ||
| 49 | 23 | ModelPixelStringsOutput::ModelPixelStringsOutput(unsigned int startChannel, unsigned int channelCount) : | |
| 50 | 23 | ThreadedChannelOutput(startChannel, channelCount) { | |
| 51 |
1/2✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
|
23 | LogDebug(VB_CHANNELOUT, "ModelPixelStringsOutput::ModelPixelStringsOutput(%u, %u)\n", |
| 52 | startChannel, channelCount); | ||
| 53 | 23 | } | |
| 54 | |||
| 55 | /* | ||
| 56 | * | ||
| 57 | */ | ||
| 58 | 92 | ModelPixelStringsOutput::~ModelPixelStringsOutput() { | |
| 59 | 46 | LogDebug(VB_CHANNELOUT, "ModelPixelStringsOutput::~ModelPixelStringsOutput()\n"); | |
| 60 | // a failed Init() is deleted without Close() | ||
| 61 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 21 times.
|
46 | if (!m_modelListenerName.empty()) { |
| 62 | 4 | PixelOverlayManager::INSTANCE.removeModelListener(modelName, m_modelListenerName); | |
| 63 | } | ||
| 64 | |||
| 65 |
2/2✓ Branch 0 taken 53 times.
✓ Branch 1 taken 23 times.
|
152 | for (int s = 0; s < strings.size(); s++) |
| 66 |
1/2✓ Branch 0 taken 53 times.
✗ Branch 1 not taken.
|
106 | delete strings[s]; |
| 67 | |||
| 68 |
2/2✓ Branch 0 taken 20 times.
✓ Branch 1 taken 3 times.
|
46 | if (buffer) |
| 69 | 40 | free(buffer); | |
| 70 | |||
| 71 |
2/2✓ Branch 0 taken 20 times.
✓ Branch 1 taken 3 times.
|
46 | if (prepBuffer) |
| 72 | 40 | free(prepBuffer); | |
| 73 | 92 | } | |
| 74 | |||
| 75 | /* | ||
| 76 | * | ||
| 77 | */ | ||
| 78 | 23 | int ModelPixelStringsOutput::Init(Json::Value config) { | |
| 79 | 23 | LogDebug(VB_CHANNELOUT, "ModelPixelStringsOutput::Init(JSON)\n"); | |
| 80 | |||
| 81 |
1/2✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
|
23 | if (config.isMember("modelName")) |
| 82 |
2/4✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 23 times.
✗ Branch 3 not taken.
|
23 | modelName = config["modelName"].asString(); |
| 83 | |||
| 84 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 23 times.
|
23 | if (modelName == "") { |
| 85 | ✗ | LogErr(VB_CHANNELOUT, "Empty Pixel Overlay Model name\n"); | |
| 86 | ✗ | AddOutputConfigWarning(35, "ModelPixelStringsOutput: Empty Pixel Overlay Model name"); | |
| 87 | ✗ | return 0; | |
| 88 | } | ||
| 89 | |||
| 90 | 23 | model = PixelOverlayManager::INSTANCE.getModel(modelName); | |
| 91 | |||
| 92 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 22 times.
|
23 | if (!model) { |
| 93 | 1 | LogErr(VB_CHANNELOUT, "Invalid Pixel Overlay Model: '%s'\n", modelName.c_str()); | |
| 94 |
2/4✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
|
1 | AddOutputConfigWarning(35, "ModelPixelStringsOutput: Invalid Pixel Overlay Model: " + modelName); |
| 95 | 1 | return 0; | |
| 96 | } | ||
| 97 | 22 | m_modelWidth = model->getWidth(); | |
| 98 | 22 | m_modelHeight = model->getHeight(); | |
| 99 | 22 | m_modelBpp = model->getBytesPerPixel(); | |
| 100 |
2/4✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 22 times.
✗ Branch 3 not taken.
|
22 | m_modelListenerName = "ModelPixelStrings" + std::to_string((uintptr_t)this); |
| 101 |
1/2✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
|
22 | PixelOverlayManager::INSTANCE.addModelListener(modelName, m_modelListenerName, |
| 102 | 22 | [this](PixelOverlayModel* m) { | |
| 103 | { | ||
| 104 |
1/2✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
|
6 | std::lock_guard<std::mutex> lock(m_modelLock); |
| 105 | // only follow a replacement the buffers still fit | ||
| 106 |
5/8✓ Branch 0 taken 3 times.
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 6 times.
|
9 | if (m && (m->getWidth() != m_modelWidth || m->getHeight() != m_modelHeight || |
| 107 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
|
3 | m->getBytesPerPixel() != m_modelBpp)) { |
| 108 | ✗ | LogWarn(VB_CHANNELOUT, "ModelPixelStrings: model '%s' changed shape; detaching\n", modelName.c_str()); | |
| 109 | ✗ | m = nullptr; | |
| 110 | } | ||
| 111 | 6 | model = m; | |
| 112 | 6 | } | |
| 113 | // A re-created model starts disabled, and a disabled model's data | ||
| 114 | // is never overlaid onto the channels: enable the replacement as | ||
| 115 | // Init() enabled the original, or the strings stop showing after | ||
| 116 | // the owning output's next reload. Outside m_modelLock, which | ||
| 117 | // SendData holds; setState() takes activeModelsLock, which goes | ||
| 118 | // after modelsLock (held by our caller) and nowhere before it. | ||
| 119 |
2/2✓ Branch 0 taken 3 times.
✓ Branch 1 taken 3 times.
|
6 | if (m) { |
| 120 |
1/2✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
|
3 | m->setState(PixelOverlayState(PixelOverlayState::PixelState::Enabled)); |
| 121 | } | ||
| 122 | 6 | }); | |
| 123 | |||
| 124 |
1/2✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
|
22 | model->setState(PixelOverlayState(PixelOverlayState::PixelState::Enabled)); |
| 125 | |||
| 126 |
2/2✓ Branch 0 taken 60 times.
✓ Branch 1 taken 20 times.
|
80 | for (int i = 0; i < config["outputs"].size(); i++) { |
| 127 |
3/6✓ Branch 0 taken 60 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 60 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 60 times.
✗ Branch 5 not taken.
|
60 | Json::Value s = config["outputs"][i]; |
| 128 |
3/8✓ Branch 0 taken 60 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 60 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 60 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
|
60 | PixelString* newString = new PixelString(); |
| 129 | |||
| 130 |
4/6✓ Branch 0 taken 60 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 60 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 58 times.
|
60 | if (!newString->Init(s)) { |
| 131 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | delete newString; |
| 132 | 2 | return 0; | |
| 133 | } | ||
| 134 | |||
| 135 | // PrepData writes each string as a row of 8 bit RGB pixels, three | ||
| 136 | // output bytes per model pixel. Anything else (RGBW or single colour | ||
| 137 | // nodes, 16 bit protocols, smart receiver markers) would walk the | ||
| 138 | // string's maps out of step and past their ends. | ||
| 139 |
3/4✓ Branch 0 taken 58 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 55 times.
✓ Branch 3 taken 3 times.
|
58 | bool rgbOnly = newString->m_bytesPerChannel == 1 && (newString->m_outputBytes % 3) == 0; |
| 140 |
2/2✓ Branch 0 taken 59 times.
✓ Branch 1 taken 58 times.
|
117 | for (auto& vs : newString->m_virtualStrings) { |
| 141 |
6/8✓ Branch 0 taken 59 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 59 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✓ Branch 5 taken 56 times.
✓ Branch 6 taken 3 times.
✓ Branch 7 taken 56 times.
|
59 | if (vs.pixelCount && vs.channelsPerNode() != 3) { |
| 142 | 3 | rgbOnly = false; | |
| 143 | } | ||
| 144 | } | ||
| 145 |
2/2✓ Branch 0 taken 4 times.
✓ Branch 1 taken 54 times.
|
58 | if (!rgbOnly) { |
| 146 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | LogErr(VB_CHANNELOUT, "ERROR, ModelPixelStrings only supports 8 bit RGB strings\n"); |
| 147 |
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 | AddOutputConfigWarning(35, "ModelPixelStringsOutput: model " + modelName + " has a string that is not 8 bit RGB"); |
| 148 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | delete newString; |
| 149 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 54 times.
|
54 | } else if ((newString->m_outputBytes / 3) > model->getWidth()) { |
| 150 | ✗ | LogErr(VB_CHANNELOUT, "ERROR, string length of %d is too long for model width of %d\n", | |
| 151 | (newString->m_outputBytes / 3), model->getWidth()); | ||
| 152 | ✗ | delete newString; | |
| 153 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 53 times.
|
54 | } else if (strings.size() >= (size_t)model->getHeight()) { |
| 154 | // PrepData lays one string per model row, so a string past the last | ||
| 155 | // row would write off the end of the buffer. | ||
| 156 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | LogErr(VB_CHANNELOUT, "ERROR, more strings configured than the model height of %d\n", |
| 157 | model->getHeight()); | ||
| 158 |
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 | AddOutputConfigWarning(35, "ModelPixelStringsOutput: model " + modelName + " has fewer rows than the configured strings"); |
| 159 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | delete newString; |
| 160 | } else { | ||
| 161 |
1/2✓ Branch 0 taken 53 times.
✗ Branch 1 not taken.
|
53 | strings.push_back(newString); |
| 162 | } | ||
| 163 |
2/2✓ Branch 0 taken 58 times.
✓ Branch 1 taken 2 times.
|
60 | } |
| 164 | |||
| 165 | 20 | LogDebug(VB_CHANNELOUT, " Found %d strings of pixels\n", strings.size()); | |
| 166 | |||
| 167 | // setData() reads width*height*bytesPerPixel while PrepData writes 3 bytes | ||
| 168 | // per pixel, so the allocation has to cover whichever is larger. Zeroed | ||
| 169 | // because only the rows that have a string are ever written. | ||
| 170 | 20 | size_t bufSize = (size_t)model->getWidth() * model->getHeight() * | |
| 171 | 20 | std::max(3, model->getBytesPerPixel()); | |
| 172 | 20 | buffer = (unsigned char*)calloc(1, bufSize); | |
| 173 | 20 | prepBuffer = (unsigned char*)calloc(1, bufSize); | |
| 174 |
2/4✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 20 times.
|
20 | if (!buffer || !prepBuffer) { |
| 175 | ✗ | LogErr(VB_CHANNELOUT, "ERROR, could not allocate %zu bytes for model '%s'\n", | |
| 176 | bufSize, modelName.c_str()); | ||
| 177 | ✗ | AddOutputConfigWarning(35, "ModelPixelStringsOutput: could not allocate buffers for model " + modelName); | |
| 178 | ✗ | return 0; | |
| 179 | } | ||
| 180 | |||
| 181 |
2/4✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20 times.
✗ Branch 3 not taken.
|
20 | return ThreadedChannelOutput::Init(config); |
| 182 | } | ||
| 183 | |||
| 184 | /* | ||
| 185 | * | ||
| 186 | */ | ||
| 187 | 20 | int ModelPixelStringsOutput::Close(void) { | |
| 188 | 20 | LogDebug(VB_CHANNELOUT, "ModelPixelStringsOutput::Close()\n"); | |
| 189 |
1/2✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
|
20 | if (!m_modelListenerName.empty()) { |
| 190 | 20 | PixelOverlayManager::INSTANCE.removeModelListener(modelName, m_modelListenerName); | |
| 191 | 20 | m_modelListenerName.clear(); | |
| 192 | } | ||
| 193 | |||
| 194 | 20 | return ThreadedChannelOutput::Close(); | |
| 195 | } | ||
| 196 | |||
| 197 | 43 | void ModelPixelStringsOutput::GetRequiredChannelRanges(const std::function<void(int, int)>& addRange) { | |
| 198 | 43 | int min = FPPD_MAX_CHANNELS; | |
| 199 | 43 | int max = 0; | |
| 200 | |||
| 201 | 43 | PixelString* ps = NULL; | |
| 202 |
2/2✓ Branch 0 taken 109 times.
✓ Branch 1 taken 43 times.
|
152 | for (int s = 0; s < strings.size(); s++) { |
| 203 | 109 | ps = strings[s]; | |
| 204 | 109 | int inCh = 0; | |
| 205 |
2/2✓ Branch 0 taken 2454 times.
✓ Branch 1 taken 109 times.
|
2563 | for (int p = 0; p < ps->m_outputBytes; p++) { |
| 206 | 2454 | int ch = ps->m_outputMap[inCh++]; | |
| 207 |
2/2✓ Branch 0 taken 2430 times.
✓ Branch 1 taken 24 times.
|
2454 | if (ch < (FPPD_MAX_CHANNELS - 3)) { |
| 208 | 2430 | min = std::min(min, ch); | |
| 209 | 2430 | max = std::max(max, ch); | |
| 210 | } | ||
| 211 | } | ||
| 212 | } | ||
| 213 | |||
| 214 |
2/2✓ Branch 0 taken 37 times.
✓ Branch 1 taken 6 times.
|
43 | if (min < max) |
| 215 |
1/2✓ Branch 0 taken 37 times.
✗ Branch 1 not taken.
|
37 | addRange(min, max); |
| 216 | 43 | } | |
| 217 | |||
| 218 | /* | ||
| 219 | * | ||
| 220 | */ | ||
| 221 | 634 | void ModelPixelStringsOutput::PrepData(unsigned char* channelData) { | |
| 222 |
1/2✓ Branch 0 taken 634 times.
✗ Branch 1 not taken.
|
634 | LogExcess(VB_CHANNELOUT, "ModelPixelStringsOutput::PrepData(%p)\n", channelData); |
| 223 | |||
| 224 |
1/2✓ Branch 0 taken 634 times.
✗ Branch 1 not taken.
|
634 | std::lock_guard<std::mutex> modelLock(m_modelLock); |
| 225 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 634 times.
|
634 | if (!model) { |
| 226 | ✗ | return; | |
| 227 | } | ||
| 228 | 634 | unsigned char* d = prepBuffer; | |
| 229 | 634 | int bpp = m_modelBpp; | |
| 230 | 634 | int stride = m_modelWidth * bpp; | |
| 231 | |||
| 232 | 634 | PixelString* ps = NULL; | |
| 233 | 634 | int inCh = 0; | |
| 234 | |||
| 235 |
2/2✓ Branch 0 taken 1142 times.
✓ Branch 1 taken 634 times.
|
1776 | for (int s = 0; s < strings.size(); s++) { |
| 236 | 1142 | ps = strings[s]; | |
| 237 | 1142 | d = prepBuffer + (s * stride); | |
| 238 | 1142 | inCh = 0; | |
| 239 | |||
| 240 | // Only the first 3 (RGB) bytes of each pixel are written; on an RGBW | ||
| 241 | // model the 4th byte is left as-is, which the calloc'd buffers keep | ||
| 242 | // at 0. Advancing by bpp (not a fixed 3) keeps rows aligned to the | ||
| 243 | // model's real layout instead of RGB-packing them. | ||
| 244 |
2/2✓ Branch 0 taken 10418 times.
✓ Branch 1 taken 1142 times.
|
11560 | for (int p = 0, pix = 0; p < ps->m_outputBytes; pix++) { |
| 245 | 10418 | d[0] = ps->m_brightnessMaps[p++][channelData[ps->m_outputMap[inCh++]]]; | |
| 246 | 10418 | d[1] = ps->m_brightnessMaps[p++][channelData[ps->m_outputMap[inCh++]]]; | |
| 247 | 10418 | d[2] = ps->m_brightnessMaps[p++][channelData[ps->m_outputMap[inCh++]]]; | |
| 248 | 10418 | d += bpp; | |
| 249 | } | ||
| 250 | } | ||
| 251 | |||
| 252 | // Publish the finished frame. The worker touches `buffer` only under this | ||
| 253 | // lock, so what it hands to the model is never the frame being filled. | ||
| 254 |
1/2✓ Branch 0 taken 634 times.
✗ Branch 1 not taken.
|
634 | std::lock_guard<std::mutex> lock(bufferLock); |
| 255 | 634 | std::swap(buffer, prepBuffer); | |
| 256 |
1/2✓ Branch 0 taken 634 times.
✗ Branch 1 not taken.
|
634 | } |
| 257 | |||
| 258 | 617 | int ModelPixelStringsOutput::RawSendData(unsigned char* channelData) { | |
| 259 | // model before buffer, the same order as PrepData | ||
| 260 |
1/2✓ Branch 0 taken 617 times.
✗ Branch 1 not taken.
|
617 | std::lock_guard<std::mutex> modelLock(m_modelLock); |
| 261 |
1/2✓ Branch 0 taken 617 times.
✗ Branch 1 not taken.
|
617 | std::lock_guard<std::mutex> lock(bufferLock); |
| 262 |
1/2✓ Branch 0 taken 617 times.
✗ Branch 1 not taken.
|
617 | if (model) { |
| 263 |
1/2✓ Branch 0 taken 617 times.
✗ Branch 1 not taken.
|
617 | model->setData(buffer); |
| 264 | } | ||
| 265 | |||
| 266 | 617 | return m_channelCount; | |
| 267 | 617 | } | |
| 268 | |||
| 269 | /* | ||
| 270 | * | ||
| 271 | */ | ||
| 272 | 40 | void ModelPixelStringsOutput::DumpConfig(void) { | |
| 273 | 40 | LogDebug(VB_CHANNELOUT, "ModelPixelStringsOutput::DumpConfig()\n"); | |
| 274 | |||
| 275 |
2/2✓ Branch 0 taken 106 times.
✓ Branch 1 taken 40 times.
|
146 | for (int i = 0; i < strings.size(); i++) { |
| 276 | 106 | LogDebug(VB_CHANNELOUT, " String #%d\n", i); | |
| 277 | 106 | strings[i]->DumpConfig(); | |
| 278 | } | ||
| 279 | |||
| 280 | 40 | ThreadedChannelOutput::DumpConfig(); | |
| 281 | 40 | } | |
| 282 |