GCC Code Coverage Report


Directory: ./
File: channeloutput/FBMatrix.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 151 163 92.6%
Functions: 12 12 100.0%
Branches: 137 266 51.5%

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 "../common.h"
18 #include "../log.h"
19
20 #include "FBMatrix.h"
21 #include "ChannelOutputSetup.h"
22 #include "overlays/PixelOverlay.h"
23
24 #include "../Plugin.h"
25
26 class FBMatrixPlugin : public FPPPlugins::Plugin, public FPPPlugins::ChannelOutputPlugin {
27 public:
28 9 FBMatrixPlugin() :
29
2/4
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✗ Branch 3 not taken.
27 FPPPlugins::Plugin("FBMatrix") {
30 9 }
31 11 virtual ChannelOutput* createChannelOutput(unsigned int startChannel, unsigned int channelCount) override {
32
2/6
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 11 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
22 return new FBMatrixOutput(startChannel, channelCount);
33 }
34 };
35
36 extern "C" {
37 9 FPPPlugins::Plugin* createPlugin() {
38
2/6
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 9 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
18 return new FBMatrixPlugin();
39 }
40 }
41
42 /*
43 *
44 */
45 11 FBMatrixOutput::FBMatrixOutput(unsigned int startChannel,
46 11 unsigned int channelCount) :
47 11 ChannelOutput(startChannel, channelCount) {
48
1/2
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
11 LogDebug(VB_CHANNELOUT, "FBMatrixOutput::FBMatrixOutput(%u, %u)\n",
49 startChannel, channelCount);
50 11 }
51
52 /*
53 *
54 */
55 44 FBMatrixOutput::~FBMatrixOutput() {
56 22 LogDebug(VB_CHANNELOUT, "FBMatrixOutput::~FBMatrixOutput()\n");
57
58 // Close() normally does this, but Init() can return failure after the
59 // listener is registered, and a failed output is deleted without a Close().
60 // A listener left pointing at a freed FBMatrixOutput is the very bug this
61 // is here to prevent, one level up.
62
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 9 times.
22 if (!m_modelListenerName.empty()) {
63 4 PixelOverlayManager::INSTANCE.removeModelListener(modelName, m_modelListenerName);
64 4 m_modelListenerName.clear();
65 }
66
67
2/2
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 2 times.
22 if (buffer)
68
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
18 delete[] buffer;
69 44 }
70
71 11 int FBMatrixOutput::Init(Json::Value config) {
72 11 LogDebug(VB_CHANNELOUT, "FBMatrixOutput::Init()\n");
73
74
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 8 times.
11 if (config.isMember("modelName"))
75
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 modelName = config["modelName"].asString();
76
77 11 LogDebug(VB_CHANNELOUT, "FBMatrix::Init: modelName='%s', empty=%d\n", modelName.c_str(), modelName == "");
78
79
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 3 times.
11 if (modelName == "") {
80 8 int width = 0;
81 8 int height = 0;
82 8 std::string device;
83
84
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 if ((config.isMember("width")) &&
85
4/8
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 8 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 8 times.
✗ Branch 7 not taken.
16 (config.isMember("height")) &&
86
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
8 (config.isMember("device"))) {
87
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
8 width = config["width"].asInt();
88
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
8 height = config["height"].asInt();
89
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
8 device = config["device"].asString();
90
91
3/6
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 8 times.
✗ Branch 5 not taken.
24 replaceStart(device, "/dev/");
92
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 modelName = "FB - ";
93
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 modelName += device;
94
95
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
8 LogDebug(VB_CHANNELOUT, "FBMatrix: Checking for existing model '%s', hasModel=%d\n",
96 modelName.c_str(), PixelOverlayManager::INSTANCE.getModel(modelName) != nullptr);
97
98 // Delete existing model if it exists, so we can recreate with PixelSize
99
3/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 7 times.
8 if (PixelOverlayManager::INSTANCE.getModel(modelName)) {
100
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogDebug(VB_CHANNELOUT, "FBMatrix: Removing existing model '%s' to recreate with PixelSize\n", modelName.c_str());
101
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 PixelOverlayManager::INSTANCE.removeAutoOverlayModel(modelName);
102 }
103
104
5/10
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 8 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 8 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 8 times.
✗ Branch 9 not taken.
8 if (width && height && (device != "")) {
105
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 Json::Value val;
106
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
8 val["Name"] = modelName;
107
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
8 val["Type"] = "FB";
108
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
8 val["Width"] = width;
109
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
8 val["Height"] = height;
110
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
8 val["Device"] = device;
111
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
8 val["autoCreated"] = true;
112
113 // Pass through pixelSize if specified in config
114
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 8 times.
8 if (config.isMember("pixelSize")) {
115 ✗ int ps = config["pixelSize"].asInt();
116 ✗ val["PixelSize"] = ps;
117 ✗ LogDebug(VB_CHANNELOUT, "FBMatrix: Setting PixelSize=%d for model '%s'\n", ps, modelName.c_str());
118 } else {
119
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 LogDebug(VB_CHANNELOUT, "FBMatrix: No pixelSize in config for model '%s'\n", modelName.c_str());
120 }
121
122
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
8 PixelOverlayManager::INSTANCE.addModel(val);
123
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 m_autoCreatedFBModelName = modelName;
124 8 } else {
125 ✗ LogErr(VB_CHANNELOUT, "Empty Pixel Overlay Model name\n");
126 ✗ AddOutputConfigWarning(35, "VirtualMatrix: Empty Pixel Overlay Model name");
127 ✗ return 0;
128 }
129 } else {
130 ✗ LogErr(VB_CHANNELOUT, "Empty Pixel Overlay Model name\n");
131 ✗ AddOutputConfigWarning(35, "VirtualMatrix: Empty Pixel Overlay Model name");
132 ✗ return 0;
133 }
134
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 }
135
136 // Track the model rather than caching a bare pointer: an auto-created FB
137 // model is destroyed by removeAutoOverlayModel() whenever the outputs are
138 // reloaded, and the channel output thread can be inside PrepData()/
139 // SendData() at that moment.
140
2/4
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 11 times.
✗ Branch 3 not taken.
11 m_modelListenerName = "FBMatrix" + std::to_string((uintptr_t)this);
141
1/2
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
11 PixelOverlayManager::INSTANCE.addModelListener(modelName, m_modelListenerName,
142 11 [this](PixelOverlayModel* m) {
143
1/2
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
11 std::lock_guard<std::mutex> lock(m_modelLock);
144 // buffer is sized for the model we saw in Init(), and SendData()
145 // hands it to model->setData(), which copies the *model's*
146 // width * height worth of bytes out of it. A model re-created
147 // under the same name with other dimensions (a second
148 // VirtualMatrix on the same device, whose Init() replaces
149 // "FB - <device>") would turn that into a heap over-read, so only
150 // follow a replacement that still matches this output.
151
7/8
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 6 times.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 1 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 4 times.
✓ Branch 6 taken 1 times.
✓ Branch 7 taken 10 times.
11 if (m && (m->getWidth() != width || m->getHeight() != height)) {
152
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogWarn(VB_CHANNELOUT, "FBMatrix: model '%s' is now %dx%d, this output is %dx%d; detaching\n",
153 modelName.c_str(), m->getWidth(), m->getHeight(), width, height);
154 1 m = nullptr;
155 }
156 11 model = m;
157 11 });
158 11 model = PixelOverlayManager::INSTANCE.getModel(modelName);
159
160
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 10 times.
11 if (!model) {
161 1 LogErr(VB_CHANNELOUT, "Invalid Pixel Overlay Model: '%s'\n", modelName.c_str());
162
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 AddOutputConfigWarning(35, "VirtualMatrix: Invalid Pixel Overlay Model: " + modelName);
163 1 return 0;
164 }
165
166 // buffer is 3 byte RGB and SendData() hands it to setData(), which reads
167 // the model's own bytes per pixel (4 for an RGBW submodel)
168
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 9 times.
10 if (model->getBytesPerPixel() != 3) {
169 1 LogErr(VB_CHANNELOUT, "Pixel Overlay Model '%s' is not RGB\n", modelName.c_str());
170
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, "VirtualMatrix: model " + modelName + " is not an RGB model");
171 1 model = nullptr;
172 1 return 0;
173 }
174
175 9 width = model->getWidth();
176 9 height = model->getHeight();
177
178 // Calculate channel count from the model size
179 9 m_channelCount = width * height * 3;
180
181 9 inverted = config["invert"].asInt();
182 9 flipHorizontal = config["flipHorizontal"].asInt();
183
184
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (m_channelCount != (width * height * 3)) {
185 ✗ LogErr(VB_CHANNELOUT, "Error, channel count is incorrect\n");
186 ✗ AddOutputConfigWarning(35, "VirtualMatrix: Invalid Pixel Overlay Model: " + modelName);
187 ✗ return 0;
188 }
189
190 9 buffer = new unsigned char[width * height * 3];
191
192
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 if (PixelOverlayManager::INSTANCE.isAutoCreatePixelOverlayModels()) {
193
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 std::string dd = "VirtualMatrix";
194
3/4
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 8 times.
9 if (config.isMember("description")) {
195
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 dd = config["description"].asString();
196 }
197
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 std::string desc = dd;
198 9 int count = 0;
199
3/4
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 9 times.
10 while (PixelOverlayManager::INSTANCE.getModel(desc) != nullptr) {
200 1 count++;
201
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 desc = dd + "-" + std::to_string(count);
202 }
203
4/8
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 9 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 9 times.
✗ Branch 7 not taken.
45 PixelOverlayManager::INSTANCE.addAutoOverlayModel(desc,
204 m_startChannel, m_channelCount, 3,
205 "H", "TL",
206 9 height, 1);
207
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 m_autoCreatedModelName = desc;
208 9 }
209
2/4
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✗ Branch 3 not taken.
9 return ChannelOutput::Init(config);
210 }
211
212 /*
213 *
214 */
215 9 int FBMatrixOutput::Close(void) {
216 9 LogDebug(VB_CHANNELOUT, "FBMatrixOutput::Close()\n");
217
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 if (!m_modelListenerName.empty()) {
218 9 PixelOverlayManager::INSTANCE.removeModelListener(modelName, m_modelListenerName);
219 9 m_modelListenerName.clear();
220 }
221 {
222
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 std::lock_guard<std::mutex> lock(m_modelLock);
223 9 model = nullptr;
224 9 }
225
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 if (!m_autoCreatedModelName.empty()) {
226 9 PixelOverlayManager::INSTANCE.removeAutoOverlayModel(m_autoCreatedModelName);
227 }
228
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 1 times.
9 if (!m_autoCreatedFBModelName.empty()) {
229 8 PixelOverlayManager::INSTANCE.removeAutoOverlayModel(m_autoCreatedFBModelName);
230 }
231 9 return ChannelOutput::Close();
232 }
233
234 1202 void FBMatrixOutput::PrepData(unsigned char* channelData) {
235
1/2
✓ Branch 0 taken 1202 times.
✗ Branch 1 not taken.
1202 std::lock_guard<std::mutex> lock(m_modelLock);
236
3/4
✓ Branch 0 taken 1135 times.
✓ Branch 1 taken 67 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 1135 times.
1202 if (!model || !buffer) {
237 67 return;
238 }
239 1135 channelData += m_startChannel;
240
241
1/2
✓ Branch 0 taken 1135 times.
✗ Branch 1 not taken.
1135 LogExcess(VB_CHANNELOUT, "FBMatrixOutput::SendData(%p)\n",
242 channelData);
243
244 1135 int stride = width * 3;
245
246
2/2
✓ Branch 0 taken 41760 times.
✓ Branch 1 taken 1135 times.
42895 for (int y = 0; y < height; y++) {
247 41760 unsigned char* src = channelData + (y * stride);
248 // Vertical flip: write source row y into the mirrored destination row
249
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 41760 times.
41760 int dstRow = inverted ? (height - 1 - y) : y;
250 41760 unsigned char* dst = buffer + (dstRow * stride);
251
252
2/2
✓ Branch 0 taken 608 times.
✓ Branch 1 taken 41152 times.
41760 if (flipHorizontal) {
253 // Horizontal flip: reverse the pixel order within the row
254 608 unsigned char* sp = src;
255 608 unsigned char* dp = dst + ((width - 1) * 3);
256
2/2
✓ Branch 0 taken 38912 times.
✓ Branch 1 taken 608 times.
39520 for (int x = 0; x < width; x++) {
257 38912 dp[0] = sp[0];
258 38912 dp[1] = sp[1];
259 38912 dp[2] = sp[2];
260 38912 sp += 3;
261 38912 dp -= 3;
262 }
263 } else {
264 41152 memcpy(dst, src, stride);
265 }
266 }
267
2/2
✓ Branch 0 taken 1135 times.
✓ Branch 1 taken 67 times.
1202 }
268
269 1188 int FBMatrixOutput::SendData(unsigned char* channelData) {
270
1/2
✓ Branch 0 taken 1188 times.
✗ Branch 1 not taken.
1188 std::lock_guard<std::mutex> lock(m_modelLock);
271
3/4
✓ Branch 0 taken 1123 times.
✓ Branch 1 taken 65 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 1123 times.
1188 if (!model || !buffer) {
272 65 return m_channelCount;
273 }
274
2/4
✓ Branch 0 taken 1123 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1123 times.
✗ Branch 3 not taken.
1123 if (model->getState() == 0) {
275 // Pass-through mode: the model isn't being driven by an active
276 // overlay/effect, so push the current sequence channels into it
277 // and draw to the framebuffer right here.
278
1/2
✓ Branch 0 taken 1123 times.
✗ Branch 1 not taken.
1123 model->setData(buffer);
279 // doOverlay() takes the whole sequence buffer: a submodel's parent
280 // writes at its own absolute channels, and channelData here is this
281 // output's slice, starting m_startChannel in. (A framebuffer model
282 // ignores the argument and draws its own data.)
283
1/2
✓ Branch 0 taken 1123 times.
✗ Branch 1 not taken.
1123 model->doOverlay(channelData - m_startChannel);
284 }
285 // else: the model is acting as an overlay (e.g., a Pixel Overlay
286 // effect is writing into channelData via flushOverlayBuffer). Don't
287 // call setData here — that would overwrite the effect's data with
288 // m_seqData every frame and leave the framebuffer mostly black.
289 // doOverlays() in ProcessSequenceData() drives the framebuffer in
290 // that case via PixelOverlayModelFB::doOverlay().
291 1123 return m_channelCount;
292 1188 }
293
294 22 void FBMatrixOutput::GetRequiredChannelRanges(const std::function<void(int, int)>& addRange) {
295 22 addRange(m_startChannel, m_startChannel + (width * height * 3) - 1);
296 22 }
297
298 /*
299 *
300 */
301 9 void FBMatrixOutput::DumpConfig(void) {
302 9 LogDebug(VB_CHANNELOUT, "VirtualMatrixOutput::DumpConfig()\n");
303 9 LogDebug(VB_CHANNELOUT, " model : %s\n", modelName.c_str());
304 9 LogDebug(VB_CHANNELOUT, " width : %d\n", width);
305 9 LogDebug(VB_CHANNELOUT, " height : %d\n", height);
306 9 LogDebug(VB_CHANNELOUT, " flipV : %d\n", inverted);
307 9 LogDebug(VB_CHANNELOUT, " flipH : %d\n", flipHorizontal);
308 9 }
309