GCC Code Coverage Report


Directory: ./
File: channeloutput/VirtualDisplayBase.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 215 377 57.0%
Functions: 8 10 80.0%
Branches: 160 428 37.4%

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 "fpp-json.h"
16
17 #include <cctype>
18 #include <climits>
19 #include <cstdint>
20
21 #include <set>
22 #include <tuple>
23
24 #include <sys/ioctl.h>
25 #include <sys/mman.h>
26 #include <sys/stat.h>
27 #include <sys/types.h>
28 #include <fcntl.h>
29
30 #include <Magick++.h>
31
32 #include <magick/type.h>
33
34 #include "../Sequence.h"
35 #include "../common.h"
36 #include "../log.h"
37 #include "../settings.h"
38
39 #include "VirtualDisplayBase.h"
40 #include "ChannelOutputSetup.h"
41
42 /////////////////////////////////////////////////////////////////////////////
43 // To disable interpolated scaling on the GPU, add this to config.txt under /boot:
44 // scaling_kernel=8
45
46 /*
47 *
48 */
49 51 VirtualDisplayBaseOutput::VirtualDisplayBaseOutput(unsigned int startChannel,
50 51 unsigned int channelCount) :
51 ChannelOutput(startChannel, channelCount),
52
1/2
✓ Branch 0 taken 51 times.
✗ Branch 1 not taken.
51 m_backgroundFilename("virtualdisplaybackground.jpg"),
53 51 m_backgroundBrightness(0.5),
54 51 m_width(1280),
55 51 m_height(1024),
56 51 m_bytesPerPixel(3),
57 51 m_bpp(24),
58 51 m_scale(1.0),
59 51 m_previewWidth(800),
60 51 m_previewHeight(600),
61
1/2
✓ Branch 0 taken 51 times.
✗ Branch 1 not taken.
102 m_colorOrder("RGB"),
62 51 m_virtualDisplay(NULL),
63 51 m_pixelSize(2),
64 51 m_rgb565map(nullptr),
65 153 m_allowDuplicatePixels(false) { // Default: filter duplicates for 2D mode
66
1/2
✓ Branch 0 taken 51 times.
✗ Branch 1 not taken.
51 LogDebug(VB_CHANNELOUT, "VirtualDisplayBaseOutput::VirtualDisplayBaseOutput(%u, %u)\n",
67 startChannel, channelCount);
68 51 }
69
70 /*
71 *
72 */
73 102 VirtualDisplayBaseOutput::~VirtualDisplayBaseOutput() {
74 102 LogDebug(VB_CHANNELOUT, "VirtualDisplayBaseOutput::~VirtualDisplayBaseOutput()\n");
75
76
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 51 times.
102 if (m_rgb565map) {
77 ✗ for (int r = 0; r < 32; r++) {
78 ✗ for (int g = 0; g < 64; g++) {
79 ✗ delete[] m_rgb565map[r][g];
80 }
81
82 ✗ delete[] m_rgb565map[r];
83 }
84
85 ✗ delete[] m_rgb565map;
86 }
87
88
2/2
✓ Branch 0 taken 32 times.
✓ Branch 1 taken 19 times.
102 if (m_virtualDisplay) {
89 64 free(m_virtualDisplay);
90 }
91 102 }
92
93 /*
94 *
95 */
96 48 int VirtualDisplayBaseOutput::Init(Json::Value config) {
97 48 LogDebug(VB_CHANNELOUT, "VirtualDisplayBaseOutput::Init()\n");
98
99 48 m_width = config["width"].asInt();
100
2/2
✓ Branch 0 taken 34 times.
✓ Branch 1 taken 14 times.
48 if (m_width <= 0)
101 34 m_width = 1280;
102
103 48 m_height = config["height"].asInt();
104
2/2
✓ Branch 0 taken 34 times.
✓ Branch 1 taken 14 times.
48 if (m_height <= 0)
105 34 m_height = 1024;
106
107
2/2
✓ Branch 0 taken 40 times.
✓ Branch 1 taken 8 times.
48 if (config.isMember("pixelSize"))
108 40 m_pixelSize = config["pixelSize"].asInt();
109
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 else if (config.isMember("PixelSize"))
110 ✗ m_pixelSize = config["PixelSize"].asInt();
111
112
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 48 times.
48 if (!m_pixelSize)
113 ✗ m_pixelSize = 2;
114
115
2/2
✓ Branch 0 taken 26 times.
✓ Branch 1 taken 22 times.
48 if (config.isMember("colorOrder"))
116
2/4
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 26 times.
✗ Branch 3 not taken.
26 m_colorOrder = config["colorOrder"].asString();
117
118
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 47 times.
48 if (config.isMember("backgroundFilename")) {
119
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 m_backgroundFilename = config["backgroundFilename"].asString();
120 // Backgrounds live flat in images/: reject separators and parent
121 // references so a crafted config cannot escape into FileExists/
122 // GraphicsMagick reads elsewhere. Falls back to no background, the
123 // same as a missing file below.
124
3/6
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 1 times.
2 if (m_backgroundFilename.find_first_of("/\\") != std::string::npos ||
125 1 m_backgroundFilename.find("..") != std::string::npos) {
126 ✗ LogErr(VB_CHANNELOUT, "Invalid background filename '%s', ignoring\n",
127 m_backgroundFilename.c_str());
128 ✗ m_backgroundFilename.clear();
129 }
130 }
131
132
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 48 times.
48 if (config.isMember("backgroundBrightness"))
133 ✗ m_backgroundBrightness = 1.0 * config["backgroundBrightness"].asInt() / 100;
134
135
2/4
✓ Branch 0 taken 48 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 48 times.
✗ Branch 3 not taken.
48 return ChannelOutput::Init(config);
136 }
137
138 /*
139 *
140 */
141 110 size_t VirtualDisplayBaseOutput::virtualDisplayBufferBytes(int w, int h, int bpp) {
142
3/6
✓ Branch 0 taken 110 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 110 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 110 times.
110 if (w <= 0 || h <= 0 || bpp <= 0) {
143 ✗ return 0;
144 }
145 110 uint64_t bytes = (uint64_t)w * (uint64_t)h * (uint64_t)bpp;
146
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 110 times.
110 if (bytes > (uint64_t)INT_MAX) {
147 ✗ return 0;
148 }
149 110 return (size_t)bytes;
150 }
151
152 48 int VirtualDisplayBaseOutput::InitializePixelMap(void) {
153
3/6
✓ Branch 0 taken 48 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 48 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 48 times.
✗ Branch 5 not taken.
48 std::string virtualDisplayMapFilename = FPP_DIR_CONFIG("/virtualdisplaymap");
154
155
3/4
✓ Branch 0 taken 48 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 47 times.
48 if (!FileExists(virtualDisplayMapFilename)) {
156
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogErr(VB_CHANNELOUT, "Error: %s does not exist\n",
157 virtualDisplayMapFilename.c_str());
158
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 AddOutputConfigWarning(41, "Virtual Display map file is missing: " + virtualDisplayMapFilename);
159 1 return 0;
160 }
161
162
1/2
✓ Branch 0 taken 47 times.
✗ Branch 1 not taken.
47 FILE* file = fopen(virtualDisplayMapFilename.c_str(), "r");
163
164
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 47 times.
47 if (file == NULL) {
165 ✗ LogErr(VB_CHANNELOUT, "Error: unable to open %s: %s\n",
166 virtualDisplayMapFilename.c_str(), FPPstrerror(errno));
167 ✗ AddOutputConfigWarning(41, "Virtual Display map file could not be opened: " + virtualDisplayMapFilename);
168 ✗ return 0;
169 }
170
171 47 char* line = NULL;
172 47 size_t len = 0;
173 ssize_t read;
174
175 47 std::string pixel;
176 47 std::vector<std::string> parts;
177 47 int x = 0;
178 47 int y = 0;
179 47 int z = 0;
180 47 int xs = 0;
181 47 int ys = 0;
182 47 int zs = 0;
183 47 int ch = 0;
184 47 int s = 0;
185 47 int r = 0;
186 47 int g = 0;
187 47 int b = 0;
188 47 int rowOffset = 0;
189 47 int colOffset = 0;
190 47 int first = 1;
191 47 int i = 0;
192 47 int found = 0;
193 unsigned char customR;
194 unsigned char customG;
195 unsigned char customB;
196 47 VirtualPixelColor vpc = kVPC_RGB;
197 47 std::string colorPart;
198 47 int BPP = 3;
199
200 // Use set for O(log n) duplicate checking instead of O(n) linear search
201 47 std::set<std::tuple<int, int, int>> seenPixels;
202 47 int offscreen = 0;
203 47 int badChannel = 0;
204
205
3/4
✓ Branch 0 taken 20366 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20319 times.
✓ Branch 3 taken 47 times.
20366 while ((read = getline(&line, &len, file)) != -1) {
206
3/6
✓ Branch 0 taken 20319 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20319 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 20319 times.
20319 if ((!line) || (!read) || (read == 1))
207 ✗ continue;
208
209
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 20318 times.
20319 if (line[0] == '#')
210 1 continue;
211
212 // Strip the line feed
213 20318 line[strlen(line) - 1] = '\0';
214
215
1/2
✓ Branch 0 taken 20318 times.
✗ Branch 1 not taken.
20318 pixel = line;
216
1/2
✓ Branch 0 taken 20318 times.
✗ Branch 1 not taken.
20318 parts = split(pixel, ',');
217
218
2/2
✓ Branch 0 taken 46 times.
✓ Branch 1 taken 20272 times.
20318 if (first) {
219
1/2
✓ Branch 0 taken 46 times.
✗ Branch 1 not taken.
46 if (parts.size() == 2) {
220 46 m_previewWidth = atoi(parts[0].c_str());
221 46 m_previewHeight = atoi(parts[1].c_str());
222
223 // A corrupt header (e.g. "0,0" or non-numeric) would divide by
224 // zero below and convert NaN/Inf to int (UB). Fail cleanly
225 // instead; valid exporters always write positive dimensions.
226
2/4
✓ Branch 0 taken 46 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 46 times.
46 if (m_previewWidth <= 0 || m_previewHeight <= 0) {
227 ✗ LogErr(VB_CHANNELOUT, "Invalid virtual display preview dimensions %dx%d\n",
228 m_previewWidth, m_previewHeight);
229 ✗ free(line);
230 ✗ fclose(file);
231 ✗ return 0;
232 }
233
234 // Only HTTPVirtualDisplay uses this where buffer is based on preview size
235
3/4
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 32 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 14 times.
46 if ((m_width == -1) || (m_height == -1)) {
236 32 m_width = m_previewWidth;
237 32 m_height = m_previewHeight;
238
239
1/2
✓ Branch 0 taken 32 times.
✗ Branch 1 not taken.
32 size_t bufBytes = virtualDisplayBufferBytes(m_width, m_height, m_bytesPerPixel);
240
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 32 times.
32 if (bufBytes == 0) {
241 ✗ LogErr(VB_CHANNELOUT, "Invalid virtual display dimensions %dx%d\n",
242 m_width, m_height);
243 ✗ free(line);
244 ✗ fclose(file);
245 ✗ return 0;
246 }
247 32 m_virtualDisplay = (unsigned char*)malloc(bufBytes);
248
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 32 times.
32 if (!m_virtualDisplay) {
249 ✗ LogErr(VB_CHANNELOUT, "Unable to malloc buffer\n");
250 ✗ free(line);
251 ✗ fclose(file);
252 ✗ return 0;
253 }
254 }
255
256
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 46 times.
46 if ((1.0 * m_width / m_previewWidth) > (1.0 * m_height / m_previewHeight)) {
257 // Virtual Display is wider than the preview aspect, so background
258 // image height should equal virtual display height
259 ✗ m_scale = 1.0 * m_height / m_previewHeight;
260 ✗ rowOffset = 0;
261 ✗ colOffset = ((m_width - (m_previewWidth * m_scale)) / 2);
262 } else {
263 // Virtual Display is taller than the preview aspect, so background
264 // image width should equal virtual display width
265 46 m_scale = 1.0 * m_width / m_previewWidth;
266 46 colOffset = 0;
267 46 rowOffset = ((m_height - (m_previewHeight * m_scale)) / 2);
268 }
269 }
270
271 46 first = 0;
272 46 continue;
273 46 }
274
275
1/2
✓ Branch 0 taken 20272 times.
✗ Branch 1 not taken.
20272 if (parts.size() >= 5) {
276 20272 x = atoi(parts[0].c_str());
277 20272 y = atoi(parts[1].c_str());
278
279
2/2
✓ Branch 0 taken 13 times.
✓ Branch 1 taken 20259 times.
20272 if (parts.size() >= 6) {
280 13 z = atoi(parts[2].c_str());
281 13 ch = atoi(parts[3].c_str());
282 13 BPP = atoi(parts[4].c_str());
283
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 colorPart = parts[5];
284
1/2
✓ Branch 0 taken 20259 times.
✗ Branch 1 not taken.
20259 } else if (parts.size() >= 5) {
285 20259 z = 0;
286 20259 ch = atoi(parts[2].c_str());
287 20259 BPP = atoi(parts[3].c_str());
288
1/2
✓ Branch 0 taken 20259 times.
✗ Branch 1 not taken.
20259 colorPart = parts[4];
289 } else {
290 3 continue;
291 }
292
293 20272 customR = customG = customB = 0;
294
295 // Every channel of the pixel is read straight out of the sequence
296 // buffer (GetPixelRGB), and GetRequiredChannelRanges() lost a pixel
297 // whose ch + 3 overflowed, so a channel outside the buffer read
298 // past it each frame.
299
3/4
✓ Branch 0 taken 20272 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 20271 times.
20272 if (ch < 0 || ch > (int)FPPD_MAX_CHANNELS - 4) {
300 1 badChannel++;
301 1 continue;
302 }
303
304 // The map is written by the exporter and not trusted: a model with
305 // no defined layout position arrives as INT_MIN, and pushing that
306 // through the scale into an int is undefined and lands the pixel
307 // gigabytes outside m_virtualDisplay. Everything that later
308 // indexes the buffer through r/g/b (HTTPVirtualDisplay::PrepData,
309 // DrawPixel) relies on the offsets being in range, so enforce that
310 // here, in double, before converting.
311 20271 double fys = m_height - (y * m_scale + rowOffset) - 1;
312 20271 double fxs = x * m_scale + colOffset;
313
5/8
✓ Branch 0 taken 20270 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 20270 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 20270 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 20270 times.
20271 if (!(fxs > -1.0 && fxs < m_width && fys > -1.0 && fys < m_height)) {
314 1 offscreen++;
315
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (!m_allowDuplicatePixels) {
316 1 continue;
317 }
318 // 3D mode addresses pixels by their index in the file, so the
319 // entry must stay; it never touches the 2D buffer.
320 ✗ fxs = fys = 0;
321 }
322 20270 ys = (int)fys;
323 20270 xs = (int)fxs;
324 20270 s = (ys * m_width + xs) * m_bytesPerPixel;
325
326
2/4
✓ Branch 0 taken 20270 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20270 times.
✗ Branch 3 not taken.
20270 if (m_colorOrder == "RGB") {
327 20270 r = s + 0;
328 20270 g = s + 1;
329 20270 b = s + 2;
330 ✗ } else if (m_colorOrder == "RBG") {
331 ✗ r = s + 0;
332 ✗ g = s + 2;
333 ✗ b = s + 1;
334 ✗ } else if (m_colorOrder == "GRB") {
335 ✗ r = s + 1;
336 ✗ g = s + 0;
337 ✗ b = s + 2;
338 ✗ } else if (m_colorOrder == "GBR") {
339 ✗ r = s + 2;
340 ✗ g = s + 0;
341 ✗ b = s + 1;
342 ✗ } else if (m_colorOrder == "BRG") {
343 ✗ r = s + 1;
344 ✗ g = s + 2;
345 ✗ b = s + 0;
346 ✗ } else if (m_colorOrder == "BGR") {
347 ✗ r = s + 2;
348 ✗ g = s + 1;
349 ✗ b = s + 0;
350 }
351
352
3/4
✓ Branch 0 taken 20270 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20268 times.
✓ Branch 3 taken 2 times.
20270 if (colorPart == "RGB")
353 20268 vpc = kVPC_RGB;
354
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
2 else if (colorPart == "RBG")
355 ✗ vpc = kVPC_RBG;
356
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
2 else if (colorPart == "GRB")
357 ✗ vpc = kVPC_GRB;
358
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
2 else if (colorPart == "GBR")
359 ✗ vpc = kVPC_GBR;
360
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
2 else if (colorPart == "BRG")
361 ✗ vpc = kVPC_BRG;
362
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
2 else if (colorPart == "BGR")
363 ✗ vpc = kVPC_BGR;
364
3/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 1 times.
2 else if (colorPart == "RGBW")
365 1 vpc = kVPC_RGBW;
366
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
1 else if (colorPart == "Red")
367 ✗ vpc = kVPC_Red;
368
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
1 else if (colorPart == "Green")
369 ✗ vpc = kVPC_Green;
370
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
1 else if (colorPart == "Blue")
371 ✗ vpc = kVPC_Blue;
372
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
1 else if (colorPart == "White")
373 ✗ vpc = kVPC_White;
374
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.
2 else if (startsWith(colorPart, "#")) {
375 // "#RRGGBB" exactly: substr/stoi below throw on anything
376 // shorter or non-hex, which would terminate fppd on a corrupt
377 // map line. Skip the pixel (logged) instead.
378 1 bool okColor = (colorPart.size() == 7);
379
2/6
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 1 times.
1 for (size_t ci = 1; okColor && ci < colorPart.size(); ci++) {
380 ✗ if (!isxdigit((unsigned char)colorPart[ci])) {
381 ✗ okColor = false;
382 }
383 }
384
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (!okColor) {
385
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogErr(VB_CHANNELOUT, "Virtual Display: invalid color '%s', skipping pixel\n",
386 colorPart.c_str());
387 1 continue;
388 }
389 ✗ std::string tmpColor;
390
391 ✗ vpc = kVPC_Custom;
392
393 ✗ tmpColor = colorPart.substr(1, 2);
394 ✗ customR = std::stoi(tmpColor, NULL, 16);
395
396 ✗ tmpColor = colorPart.substr(3, 2);
397 ✗ customG = std::stoi(tmpColor, NULL, 16);
398
399 ✗ tmpColor = colorPart.substr(5, 2);
400 ✗ customB = std::stoi(tmpColor, NULL, 16);
401 ✗ }
402
403 // Use set for O(log n) duplicate check instead of O(n) linear search
404 // But skip duplicate checking in 3D mode where multiple pixels can exist at same coords
405 20269 auto pixelKey = std::make_tuple(x, y, z);
406
5/8
✓ Branch 0 taken 20255 times.
✓ Branch 1 taken 14 times.
✓ Branch 2 taken 20255 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 20255 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 20269 times.
✗ Branch 7 not taken.
20269 if (m_allowDuplicatePixels || seenPixels.find(pixelKey) == seenPixels.end()) {
407
2/2
✓ Branch 0 taken 20255 times.
✓ Branch 1 taken 14 times.
20269 if (!m_allowDuplicatePixels) {
408
1/2
✓ Branch 0 taken 20255 times.
✗ Branch 1 not taken.
20255 seenPixels.insert(pixelKey);
409 }
410
1/2
✓ Branch 0 taken 20269 times.
✗ Branch 1 not taken.
20269 m_pixels.push_back({ x, y, z, xs, ys, zs, ch, r, g, b, BPP, customR, customG, customB, vpc });
411 }
412 }
413 }
414
415
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 46 times.
47 if (badChannel) {
416
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogWarn(VB_CHANNELOUT, "Virtual Display: %d pixel(s) in %s use channels outside 1-%d and were skipped\n",
417 badChannel, virtualDisplayMapFilename.c_str(), FPPD_MAX_CHANNELS);
418 }
419
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 46 times.
47 if (offscreen) {
420
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogWarn(VB_CHANNELOUT, "Virtual Display: %d pixel(s) in %s lie outside the %dx%d preview and were skipped\n",
421 offscreen, virtualDisplayMapFilename.c_str(), m_width, m_height);
422 }
423
424
1/2
✓ Branch 0 taken 47 times.
✗ Branch 1 not taken.
47 LoadBackgroundImage();
425
426 47 free(line);
427
1/2
✓ Branch 0 taken 47 times.
✗ Branch 1 not taken.
47 fclose(file);
428
429 47 return 1;
430 48 }
431
432 /*
433 *
434 */
435 47 void VirtualDisplayBaseOutput::LoadBackgroundImage(void) {
436 // Nothing to draw into (e.g. an empty map left no buffer): callers check
437 // too, but never write through a null display from here.
438
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 46 times.
47 if (m_virtualDisplay == nullptr) {
439 47 return;
440 }
441
442
1/2
✓ Branch 0 taken 46 times.
✗ Branch 1 not taken.
46 std::string bgFile = "/home/fpp/media/images/";
443
1/2
✓ Branch 0 taken 46 times.
✗ Branch 1 not taken.
46 bgFile += m_backgroundFilename;
444
445
3/4
✓ Branch 0 taken 46 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 45 times.
✓ Branch 3 taken 1 times.
46 if (!FileExists(bgFile)) {
446
1/2
✓ Branch 0 taken 45 times.
✗ Branch 1 not taken.
45 LogWarn(VB_CHANNELOUT, "Background image does not exist: %s\n",
447 bgFile.c_str());
448 45 return;
449 }
450
451
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 Magick::Image image;
452
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 Magick::Blob blob;
453
454 try {
455
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 image.quiet(true);
456
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 image.read(bgFile.c_str());
457
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 image.modifyImage();
458
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 image.scale(Magick::Geometry(m_width, m_height));
459
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 image.type(Magick::TrueColorType);
460
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 image.magick("RGBA");
461
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 image.write(&blob);
462
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 } catch (Magick::Exception& error_) {
463 // the image can come back partly decoded and unscaled, with no blob
464
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogErr(VB_CHANNELOUT, "GraphicsMagick exception reading %s: %s\n",
465 bgFile.c_str(), error_.what());
466 1 return;
467 1 }
468
469 ✗ unsigned char* imgData = (unsigned char*)blob.data();
470 ✗ unsigned int imgWidth = image.columns();
471 ✗ unsigned int imgHeight = image.rows();
472 ✗ unsigned int imgBytes = imgWidth * imgHeight * 4; // RGBA
473 // the copy below assumes a scaled RGBA image that fits the display
474 ✗ if (!imgData || blob.length() < imgBytes || imgWidth > (unsigned int)m_width || imgHeight > (unsigned int)m_height) {
475 ✗ LogErr(VB_CHANNELOUT, "Background image %s could not be converted for display\n", bgFile.c_str());
476 ✗ return;
477 }
478 ✗ unsigned long RGBx = 0;
479 ✗ bool letterboxed = false;
480 ✗ int offset = 0;
481 ✗ int colOffset = (m_width - imgWidth) / 2;
482 ✗ int r = 0;
483 ✗ int c = 0;
484
485 ✗ if ((1.0 * m_width / m_previewWidth) > (1.0 * m_height / m_previewHeight))
486 ✗ letterboxed = true;
487
488 ✗ offset = (m_height - imgHeight) / 2 * m_width * m_bytesPerPixel;
489
490 ✗ for (int i = 0; i < imgBytes;) {
491 ✗ if ((letterboxed) && (c == 0)) {
492 ✗ offset = ((r * m_width) + colOffset) * m_bytesPerPixel;
493 }
494
495 ✗ if ((m_bpp == 24) || (m_bpp == 32)) {
496 ✗ m_virtualDisplay[offset++] = imgData[i++] * m_backgroundBrightness;
497 ✗ m_virtualDisplay[offset++] = imgData[i++] * m_backgroundBrightness;
498 ✗ m_virtualDisplay[offset++] = imgData[i++] * m_backgroundBrightness;
499 ✗ i++;
500
501 ✗ if (m_bpp == 32)
502 ✗ offset++;
503 } else // 16bpp RGB565
504 {
505 ✗ RGBx = *(unsigned long*)(imgData + i);
506 ✗ *(uint16_t*)(m_virtualDisplay + offset) =
507 ✗ (((int)((RGBx & 0x000000FF) * m_backgroundBrightness) << 8) & 0b1111100000000000) |
508 ✗ (((int)((RGBx & 0x0000FF00) * m_backgroundBrightness) >> 5) & 0b0000011111100000) |
509 ✗ (((int)((RGBx & 0x00FF0000) * m_backgroundBrightness) >> 19)); // & 0b0000000000011111);
510
511 ✗ i += 4;
512 ✗ offset += 2;
513 }
514
515 ✗ if (letterboxed) {
516 ✗ c++;
517
518 ✗ if (c >= imgWidth) {
519 ✗ c = 0;
520 ✗ r++;
521 }
522 }
523 }
524
3/6
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 46 times.
48 }
525
526 /*
527 *
528 */
529 ✗ void VirtualDisplayBaseOutput::DrawPixel(int rOffset, int gOffset, int bOffset,
530 unsigned char r, unsigned char g, unsigned char b) {
531 ✗ if (m_bpp == 16) {
532 ✗ if ((rOffset < gOffset) && (gOffset < bOffset)) {
533 ✗ *((uint16_t*)(m_virtualDisplay + rOffset)) = m_rgb565map[r >> 3][g >> 2][b >> 3];
534 ✗ } else if ((rOffset < gOffset) && (bOffset < gOffset)) {
535 ✗ *((uint16_t*)(m_virtualDisplay + rOffset)) = m_rgb565map[r >> 3][b >> 2][g >> 3];
536 ✗ } else if ((gOffset < rOffset) && (rOffset < bOffset)) {
537 ✗ *((uint16_t*)(m_virtualDisplay + gOffset)) = m_rgb565map[g >> 3][r >> 2][b >> 3];
538 ✗ } else if ((gOffset < rOffset) && (bOffset < rOffset)) {
539 ✗ *((uint16_t*)(m_virtualDisplay + gOffset)) = m_rgb565map[g >> 3][b >> 2][r >> 3];
540 ✗ } else if ((bOffset < gOffset) && (rOffset < gOffset)) {
541 ✗ *((uint16_t*)(m_virtualDisplay + bOffset)) = m_rgb565map[b >> 3][r >> 2][g >> 3];
542 ✗ } else if ((bOffset < gOffset) && (gOffset < rOffset)) {
543 ✗ *((uint16_t*)(m_virtualDisplay + bOffset)) = m_rgb565map[b >> 3][g >> 2][r >> 3];
544 }
545 } else {
546 ✗ m_virtualDisplay[rOffset] = r;
547 ✗ m_virtualDisplay[gOffset] = g;
548 ✗ m_virtualDisplay[bOffset] = b;
549 }
550 ✗ }
551
552 97 void VirtualDisplayBaseOutput::GetRequiredChannelRanges(const std::function<void(int, int)>& addRange) {
553 97 int min = FPPD_MAX_CHANNELS;
554 97 int max = 0;
555
2/2
✓ Branch 0 taken 40553 times.
✓ Branch 1 taken 97 times.
40650 for (auto& pixel : m_pixels) {
556 40553 min = std::min(min, pixel.ch);
557
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 40551 times.
40553 max = std::max(max, pixel.ch + (pixel.cpp == 4 ? 3 : 2));
558 }
559
1/2
✓ Branch 0 taken 97 times.
✗ Branch 1 not taken.
97 addRange(min, max);
560 97 }
561
562 /*
563 *
564 */
565 ✗ void VirtualDisplayBaseOutput::DrawPixels(unsigned char* channelData) {
566 ✗ unsigned char r = 0;
567 ✗ unsigned char g = 0;
568 ✗ unsigned char b = 0;
569 ✗ int stride = m_width * m_bytesPerPixel;
570 VirtualDisplayPixel pixel;
571
572 ✗ for (int i = 0; i < m_pixels.size(); i++) {
573 ✗ pixel = m_pixels[i];
574
575 ✗ GetPixelRGB(pixel, channelData, r, g, b);
576
577 ✗ DrawPixel(pixel.r, pixel.g, pixel.b, r, g, b);
578
579 ✗ if (m_pixelSize >= 2) {
580 ✗ if (m_pixelSize == 2) {
581 ✗ r /= 4;
582 ✗ g /= 4;
583 ✗ b /= 4;
584 ✗ r *= 3;
585 ✗ g *= 3;
586 ✗ b *= 3;
587 }
588
589 // Neighbours are bounded by the pixel's buffer position (xs/ys),
590 // not its layout coordinates: +/-m_bytesPerPixel moves along the
591 // row, +/-stride moves between rows.
592 ✗ if (pixel.xs < (m_width - 1))
593 ✗ DrawPixel(pixel.r + m_bytesPerPixel,
594 ✗ pixel.g + m_bytesPerPixel,
595 ✗ pixel.b + m_bytesPerPixel,
596 r, g, b);
597
598 ✗ if (pixel.xs > 0)
599 ✗ DrawPixel(pixel.r - m_bytesPerPixel,
600 ✗ pixel.g - m_bytesPerPixel,
601 ✗ pixel.b - m_bytesPerPixel,
602 r, g, b);
603
604 ✗ if (pixel.ys < (m_height - 1))
605 ✗ DrawPixel(pixel.r + stride,
606 ✗ pixel.g + stride,
607 ✗ pixel.b + stride,
608 r, g, b);
609
610 ✗ if (pixel.ys > 0)
611 ✗ DrawPixel(pixel.r - stride,
612 ✗ pixel.g - stride,
613 ✗ pixel.b - stride,
614 r, g, b);
615 }
616 }
617 ✗ }
618
619 /*
620 *
621 */
622 48 void VirtualDisplayBaseOutput::DumpConfig(void) {
623 48 LogDebug(VB_CHANNELOUT, "VirtualDisplayBaseOutput::DumpConfig()\n");
624 48 LogDebug(VB_CHANNELOUT, " width : %d\n", m_width);
625 48 LogDebug(VB_CHANNELOUT, " height : %d\n", m_height);
626 48 LogDebug(VB_CHANNELOUT, " scale : %0.3f\n", m_scale);
627 48 LogDebug(VB_CHANNELOUT, " preview width : %d\n", m_previewWidth);
628 48 LogDebug(VB_CHANNELOUT, " preview height: %d\n", m_previewHeight);
629 48 LogDebug(VB_CHANNELOUT, " color Order : %s\n", m_colorOrder.c_str());
630 48 LogDebug(VB_CHANNELOUT, " pixel count : %d\n", m_pixels.size());
631 48 LogDebug(VB_CHANNELOUT, " pixel size : %d\n", m_pixelSize);
632 48 ChannelOutput::DumpConfig();
633 48 }
634