GCC Code Coverage Report


Directory: ./
File: channeloutput/ModelPixelStrings.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 131 142 92.3%
Functions: 12 12 100.0%
Branches: 109 192 56.8%

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