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