| 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 |