GCC Code Coverage Report


Directory: ./
File: channeloutput/VirtualDisplay.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 122 152 80.3%
Functions: 9 9 100.0%
Branches: 115 256 44.9%

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
18 #include <sys/ioctl.h>
19 #include <sys/mman.h>
20 #include <cmath>
21 #include <fcntl.h>
22
23 #include "../common.h"
24 #include "../log.h"
25
26 #include "VirtualDisplay.h"
27 #include "ChannelOutputSetup.h"
28 #include "overlays/PixelOverlay.h"
29
30 #include "Plugin.h"
31 class VirtualDisplayPlugin : public FPPPlugins::Plugin, public FPPPlugins::ChannelOutputPlugin {
32 public:
33 6 VirtualDisplayPlugin() :
34
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
18 FPPPlugins::Plugin("VirtualDisplay") {
35 6 }
36 17 virtual ChannelOutput* createChannelOutput(unsigned int startChannel, unsigned int channelCount) override {
37
2/6
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 17 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
34 return new VirtualDisplayOutput(startChannel, channelCount);
38 }
39 };
40
41 extern "C" {
42 6 FPPPlugins::Plugin* createPlugin() {
43
2/6
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 6 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
12 return new VirtualDisplayPlugin();
44 }
45 }
46
47 /*
48 *
49 */
50 17 VirtualDisplayOutput::VirtualDisplayOutput(unsigned int startChannel,
51 17 unsigned int channelCount) :
52 17 VirtualDisplayBaseOutput(startChannel, channelCount) {
53
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
17 LogDebug(VB_CHANNELOUT, "VirtualDisplayOutput::VirtualDisplayOutput(%u, %u)\n",
54 startChannel, channelCount);
55 17 }
56
57 /*
58 *
59 */
60 68 VirtualDisplayOutput::~VirtualDisplayOutput() {
61 34 LogDebug(VB_CHANNELOUT, "VirtualDisplayOutput::~VirtualDisplayOutput()\n");
62
63 34 Close();
64 68 }
65
66 /*
67 *
68 */
69 17 int VirtualDisplayOutput::Init(Json::Value config) {
70 17 LogDebug(VB_CHANNELOUT, "VirtualDisplayOutput::Init()\n");
71
72 // Check for old config names
73
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 17 times.
17 if (config.isMember("ModelName"))
74 ✗ m_modelName = config["ModelName"].asString();
75
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 17 times.
17 if (config.isMember("PixelSize"))
76 ✗ m_pixelSize = config["PixelSize"].asInt();
77
78 // Check for new config names
79
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
17 if (config.isMember("modelName"))
80
2/4
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
17 m_modelName = config["modelName"].asString();
81
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
17 if (config.isMember("pixelSize"))
82 17 m_pixelSize = config["pixelSize"].asInt();
83
84
2/2
✓ Branch 0 taken 12 times.
✓ Branch 1 taken 5 times.
17 if (m_modelName == "") {
85 12 int width = 0;
86 12 int height = 0;
87 12 std::string device;
88
89
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 if ((config.isMember("width")) &&
90
4/8
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 12 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 12 times.
✗ Branch 7 not taken.
24 (config.isMember("height")) &&
91
2/4
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
12 (config.isMember("device"))) {
92
2/4
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
12 width = config["width"].asInt();
93
2/4
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
12 height = config["height"].asInt();
94
2/4
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
12 device = config["device"].asString();
95
96
3/6
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 12 times.
✗ Branch 5 not taken.
36 replaceStart(device, "/dev/");
97
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 m_modelName = "FB - ";
98
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 m_modelName += device;
99
100
7/14
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 12 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 12 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 12 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 12 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 12 times.
✗ Branch 13 not taken.
12 if (width && height && (device != "") && !PixelOverlayManager::INSTANCE.getModel(m_modelName)) {
101
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 Json::Value val;
102
2/4
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
12 val["Name"] = m_modelName;
103
2/4
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
12 val["Type"] = "FB";
104
2/4
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
12 val["Width"] = width;
105
2/4
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
12 val["Height"] = height;
106
2/4
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
12 val["PixelSize"] = 1;
107
2/4
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
12 val["Device"] = device;
108
2/4
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
12 val["autoCreated"] = true;
109
110
2/4
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
12 PixelOverlayManager::INSTANCE.addModel(val);
111 12 modelCreated = true;
112 12 } else {
113 ✗ LogErr(VB_CHANNELOUT, "Empty Pixel Overlay Model name\n");
114 ✗ AddOutputConfigWarning(35, "Virtual Matrix output has an empty Pixel Overlay Model name");
115 ✗ return 0;
116 }
117 } else {
118 ✗ LogErr(VB_CHANNELOUT, "Empty Pixel Overlay Model name\n");
119 ✗ AddOutputConfigWarning(35, "Virtual Matrix output has an empty Pixel Overlay Model name");
120 ✗ return 0;
121 }
122
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 }
123
124
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 17 times.
17 if (m_modelName == "") {
125 ✗ LogErr(VB_CHANNELOUT, "Empty Pixel Overlay Model name\n");
126 ✗ AddOutputConfigWarning(35, "Virtual Matrix output has an empty Pixel Overlay Model name");
127 ✗ return 0;
128 }
129
130 17 m_model = PixelOverlayManager::INSTANCE.getModel(m_modelName);
131
132
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 16 times.
17 if (!m_model) {
133 1 LogErr(VB_CHANNELOUT, "Invalid Pixel Overlay Model: '%s'\n", m_modelName.c_str());
134
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, "Invalid Pixel Overlay Model: '" + m_modelName + "'");
135 1 return 0;
136 }
137
138 // the display is drawn as 3 byte RGB, and setData() below reads the
139 // model's own bytes per pixel from it (4 for an RGBW submodel)
140
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 14 times.
16 if (m_model->getBytesPerPixel() != 3) {
141 2 LogErr(VB_CHANNELOUT, "Pixel Overlay Model '%s' is not RGB\n", m_modelName.c_str());
142
3/6
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
2 AddOutputConfigWarning(35, "Virtual Display: model '" + m_modelName + "' is not an RGB model");
143 2 m_model = nullptr;
144 2 return 0;
145 }
146
147 14 m_width = m_model->getWidth();
148 14 m_height = m_model->getHeight();
149
150
2/4
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✗ Branch 3 not taken.
14 config["width"] = m_width;
151
2/4
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✗ Branch 3 not taken.
14 config["height"] = m_height;
152
153
3/6
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 14 times.
14 if (!VirtualDisplayBaseOutput::Init(config))
154 ✗ return 0;
155
156 14 size_t bufBytes = virtualDisplayBufferBytes(m_width, m_height, m_bytesPerPixel);
157
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 14 times.
14 if (bufBytes == 0) {
158 ✗ LogErr(VB_CHANNELOUT, "Invalid virtual display dimensions %dx%d\n",
159 m_width, m_height);
160 ✗ return 0;
161 }
162 14 m_virtualDisplay = (unsigned char*)malloc(bufBytes);
163
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 14 times.
14 if (!m_virtualDisplay) {
164 ✗ LogErr(VB_CHANNELOUT, "Unable to malloc buffer\n");
165 ✗ return 0;
166 }
167
168 14 memset(m_virtualDisplay, 0, bufBytes);
169
170 14 int result = InitializePixelMap();
171
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 14 times.
14 if (!result)
172 ✗ return 0;
173
174 // Put the background image onto the model and always call doOverlay
175 // to ensure display is enabled (even if model state is not Disabled).
176 // Only for a framebuffer model: doOverlay() writes the model's channels
177 // at its start channel into the buffer it is given, and a submodel
178 // forwards to its parent at the parent's channels - far past the end of
179 // this display-sized buffer.
180 14 m_model->setData(m_virtualDisplay);
181
2/2
✓ Branch 0 taken 13 times.
✓ Branch 1 taken 1 times.
14 if (m_model->getType() == "FB") {
182 13 m_model->doOverlay(m_virtualDisplay);
183 }
184
185
2/4
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✗ Branch 3 not taken.
14 m_modelListenerName = "VirtualDisplay" + std::to_string((uintptr_t)this);
186
1/2
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
14 PixelOverlayManager::INSTANCE.addModelListener(m_modelName, m_modelListenerName,
187 14 [this](PixelOverlayModel* m) {
188
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 std::lock_guard<std::mutex> lock(m_modelLock);
189 // only follow a replacement this display still fits
190
6/10
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 5 times.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 5 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 5 times.
✗ Branch 8 not taken.
✓ Branch 9 taken 10 times.
10 if (m && (m->getWidth() != m_width || m->getHeight() != m_height || m->getBytesPerPixel() != 3)) {
191 ✗ LogWarn(VB_CHANNELOUT, "VirtualDisplay: model '%s' changed shape; detaching\n", m_modelName.c_str());
192 ✗ m = nullptr;
193 }
194 10 m_model = m;
195 10 });
196 14 return 1;
197 }
198
199 /*
200 *
201 */
202 31 int VirtualDisplayOutput::Close(void) {
203 31 LogDebug(VB_CHANNELOUT, "VirtualDisplayOutput::Close()\n");
204
2/2
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 17 times.
31 if (!m_modelListenerName.empty()) {
205 14 PixelOverlayManager::INSTANCE.removeModelListener(m_modelName, m_modelListenerName);
206 14 m_modelListenerName.clear();
207 }
208 {
209
1/2
✓ Branch 0 taken 31 times.
✗ Branch 1 not taken.
31 std::lock_guard<std::mutex> lock(m_modelLock);
210 31 m_model = nullptr;
211 31 }
212
2/2
✓ Branch 0 taken 24 times.
✓ Branch 1 taken 7 times.
31 if (modelCreated) {
213 24 PixelOverlayManager::INSTANCE.removeAutoOverlayModel(m_modelName);
214 }
215
2/2
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 17 times.
31 if (m_virtualDisplay) {
216 14 free(m_virtualDisplay);
217 14 m_virtualDisplay = nullptr;
218 }
219
220 31 return VirtualDisplayBaseOutput::Close();
221 }
222
223 502 int VirtualDisplayOutput::SendData(unsigned char* channelData) {
224
1/2
✓ Branch 0 taken 502 times.
✗ Branch 1 not taken.
502 LogExcess(VB_CHANNELOUT, "VirtualDisplayOutput::SendData(%p)\n",
225 channelData);
226
1/2
✓ Branch 0 taken 502 times.
✗ Branch 1 not taken.
502 std::lock_guard<std::mutex> modelLock(m_modelLock);
227
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 502 times.
502 if (!m_model) {
228 ✗ return m_channelCount;
229 }
230 502 unsigned char r = 0;
231 502 unsigned char g = 0;
232 502 unsigned char b = 0;
233 502 int stride = m_width * m_bytesPerPixel;
234 VirtualDisplayPixel pixel;
235
236 502 int tx = 0;
237 502 int ty = 0;
238
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 502 times.
502 int radius = m_pixelSize >= 2 ? m_pixelSize / 2 : 1;
239
240 // Pre-calculate radius squared to avoid sqrt() in hot loop
241 502 int radiusSquared = radius * radius;
242
243
2/2
✓ Branch 0 taken 1152 times.
✓ Branch 1 taken 502 times.
1654 for (int i = 0; i < m_pixels.size(); i++) {
244 1152 pixel = m_pixels[i];
245
246
1/2
✓ Branch 0 taken 1152 times.
✗ Branch 1 not taken.
1152 GetPixelRGB(pixel, channelData, r, g, b);
247
248 // Optimize: For single pixel (radius=1), skip the expensive nested loops
249
1/2
✓ Branch 0 taken 1152 times.
✗ Branch 1 not taken.
1152 if (radius == 1) {
250
1/2
✓ Branch 0 taken 1152 times.
✗ Branch 1 not taken.
1152 m_model->setPixelValue(pixel.xs, pixel.ys, r, g, b);
251 } else {
252 ✗ for (int xo = 0 - radius; xo < radius; xo++) {
253 ✗ for (int yo = 0 - radius; yo < radius; yo++) {
254 // Replace expensive sqrt() with integer multiplication
255 ✗ if ((xo * xo + yo * yo) > radiusSquared)
256 ✗ continue;
257
258 ✗ tx = pixel.xs + xo;
259 ✗ ty = pixel.ys + yo;
260
261 ✗ if ((tx < 0) || (ty < 0) || (tx > (m_width - 1)) || (ty > (m_height - 1)))
262 ✗ continue;
263
264 ✗ m_model->setPixelValue(tx, ty, r, g, b);
265 }
266 }
267 }
268 }
269
1/2
✓ Branch 0 taken 502 times.
✗ Branch 1 not taken.
502 m_model->setBufferIsDirty(true);
270
2/4
✓ Branch 0 taken 502 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 502 times.
✗ Branch 3 not taken.
502 if (m_model->getState() == 0) {
271
2/4
✓ Branch 0 taken 502 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 502 times.
502 if (m_model->getType() != "FB") {
272 // a submodel's parent writes at its own absolute channels, so it
273 // needs the whole sequence buffer, not a display-sized one
274 ✗ m_model->doOverlay(channelData - m_startChannel);
275
2/4
✓ Branch 0 taken 502 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 502 times.
✗ Branch 3 not taken.
502 } else if (uint8_t* buf = m_model->getOverlayBuffer()) {
276
1/2
✓ Branch 0 taken 502 times.
✗ Branch 1 not taken.
502 m_model->doOverlay(buf);
277 }
278 }
279 502 return m_channelCount;
280 502 }
281