GCC Code Coverage Report


Directory: ./
File: framebuffer/KMSFrameBuffer.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 619 839 73.8%
Functions: 33 39 84.6%
Branches: 469 1026 45.7%

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 #include "fpp-pch.h"
13
14 #include "KMSFrameBuffer.h"
15
16 #ifdef HAS_KMS_FB
17 #include <libdrm/drm.h>
18 #include <libdrm/drm_fourcc.h>
19 #include <sys/ioctl.h>
20 #include <sys/mman.h>
21 #include <cmath>
22 #include <cstring>
23 #include <cerrno>
24 #include <fcntl.h>
25 #include <linux/netlink.h>
26 #include <poll.h>
27 #include <sys/eventfd.h>
28 #include <sys/socket.h>
29 #include <unistd.h>
30 #include <algorithm>
31 #include <thread>
32
33 #include "../common.h"
34 #include "../common_mini.h"
35 #include "../Warnings.h"
36 #include "../log.h"
37 #include "../mediaoutput/DrmCards.h"
38 #include "../mediaoutput/mediaoutput.h"
39
40 std::atomic_int KMSFrameBuffer::FRAMEBUFFER_COUNT(0);
41 std::vector<CardInfo*> KMSFrameBuffer::CARDS;
42 std::map<uint32_t, KMSFrameBuffer*> KMSFrameBuffer::FLIP_TARGETS;
43 uint32_t KMSFrameBuffer::NEXT_FLIP_TOKEN = 0;
44
45 23 std::string KMSFrameBuffer::ConnectorFullName(int fd, drmModeConnectorPtr conn) {
46 23 const char* typeName = drmModeGetConnectorTypeName(conn->connector_type);
47
2/4
✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 23 times.
✗ Branch 3 not taken.
40 std::string name = typeName ? typeName : "Unknown";
48
2/4
✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 23 times.
✗ Branch 3 not taken.
23 name += "-" + std::to_string(conn->connector_type_id);
49 23 return name;
50 ✗ }
51
52 // A plugged-in display: HDMI, DVI, DisplayPort and VGA can be unplugged, and a
53 // sink on them answers with an EDID. Panels (DPI, DSI, ...) are there for good.
54 42 static bool IsExternalConnector(uint32_t type) {
55
2/2
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 28 times.
42 switch (type) {
56 14 case DRM_MODE_CONNECTOR_HDMIA:
57 case DRM_MODE_CONNECTOR_HDMIB:
58 case DRM_MODE_CONNECTOR_DVII:
59 case DRM_MODE_CONNECTOR_DVID:
60 case DRM_MODE_CONNECTOR_DVIA:
61 case DRM_MODE_CONNECTOR_DisplayPort:
62 case DRM_MODE_CONNECTOR_VGA:
63 14 return true;
64 28 default:
65 28 return false;
66 }
67 }
68
69 // Whether a display is actually on the connector. For an external connector
70 // that takes an EDID: vc4.force_hotplug (the ForceHDMI setting) pins it to
71 // "connected" and the kernel then makes up a few modes (up to 1024x768) even
72 // with nothing plugged in.
73 24 bool KMSFrameBuffer::SinkPresent(int fd, drmModeConnectorPtr conn) {
74
4/6
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20 times.
✓ Branch 3 taken 4 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 20 times.
24 if (!conn || conn->connection != DRM_MODE_CONNECTED || conn->count_modes == 0) {
75 4 return false;
76 }
77
2/2
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 6 times.
20 if (!IsExternalConnector(conn->connector_type)) {
78 14 return true;
79 }
80 6 bool edid = false;
81
4/4
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 5 times.
✓ Branch 2 taken 6 times.
✓ Branch 3 taken 1 times.
12 for (int i = 0; i < conn->count_props && !edid; i++) {
82 6 drmModePropertyPtr prop = drmModeGetProperty(fd, conn->props[i]);
83
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 if (prop) {
84
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
6 edid = !strcmp(prop->name, "EDID") && conn->prop_values[i] != 0;
85 6 drmModeFreeProperty(prop);
86 }
87 }
88 6 return edid;
89 }
90
91 // The mode forced with video=<connector>:<mode> on the kernel command line,
92 // which is what the Force HDMI Resolution settings write. The kernel flags
93 // the matching probed mode USERDEF, or adds one of its own if none matches.
94 22 static bool CommandLineMode(drmModeConnectorPtr conn, drmModeModeInfo& mode) {
95
3/4
✓ Branch 0 taken 97 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 75 times.
✓ Branch 3 taken 22 times.
97 for (int mi = 0; conn && mi < conn->count_modes; mi++) {
96
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 75 times.
75 if (conn->modes[mi].type & DRM_MODE_TYPE_USERDEF) {
97 ✗ mode = conn->modes[mi];
98 ✗ return true;
99 }
100 }
101 22 return false;
102 }
103
104 // The mode forced on the kernel command line, else the display's preferred
105 // mode, else its first. Not the preferred mode first: with two displays that
106 // prefer different modes, that ran each in its own one whatever was forced.
107 20 bool KMSFrameBuffer::PickMode(drmModeConnectorPtr conn, drmModeModeInfo& mode) {
108
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 20 times.
20 if (CommandLineMode(conn, mode)) {
109 ✗ return true;
110 }
111
1/2
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
20 for (int mi = 0; mi < conn->count_modes; mi++) {
112
1/2
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
20 if (conn->modes[mi].type & DRM_MODE_TYPE_PREFERRED) {
113 20 mode = conn->modes[mi];
114 20 return true;
115 }
116 }
117 ✗ if (conn->count_modes > 0) {
118 ✗ mode = conn->modes[0];
119 ✗ return true;
120 }
121 ✗ return false;
122 }
123
124 // What an external connector runs until a display shows up: CEA-861 1080p60,
125 // which any HDMI sink takes.
126 2 static drmModeModeInfo FallbackMode() {
127 2 drmModeModeInfo m = {};
128 2 m.clock = 148500;
129 2 m.hdisplay = 1920;
130 2 m.hsync_start = 2008;
131 2 m.hsync_end = 2052;
132 2 m.htotal = 2200;
133 2 m.vdisplay = 1080;
134 2 m.vsync_start = 1084;
135 2 m.vsync_end = 1089;
136 2 m.vtotal = 1125;
137 2 m.vrefresh = 60;
138 2 m.flags = DRM_MODE_FLAG_PHSYNC | DRM_MODE_FLAG_PVSYNC;
139 2 m.type = DRM_MODE_TYPE_DRIVER;
140 2 snprintf(m.name, sizeof(m.name), "1920x1080");
141 2 return m;
142 }
143
144 // "1920x1080@60 148500kHz h 1920/2008/2052/2200 v 1080/1084/1089/1125 flags 0x5"
145 ✗ static std::string ModeLine(const drmModeModeInfo& m) {
146 char buf[160];
147 ✗ snprintf(buf, sizeof(buf), "%dx%d@%d %ukHz h %d/%d/%d/%d v %d/%d/%d/%d flags 0x%x",
148 ✗ m.hdisplay, m.vdisplay, m.vrefresh, m.clock, m.hdisplay, m.hsync_start, m.hsync_end, m.htotal,
149 ✗ m.vdisplay, m.vsync_start, m.vsync_end, m.vtotal, m.flags);
150 ✗ return buf;
151 }
152
153 ✗ static bool SameTiming(const drmModeModeInfo& a, const drmModeModeInfo& b) {
154 ✗ return a.clock == b.clock && a.hdisplay == b.hdisplay && a.hsync_start == b.hsync_start &&
155 ✗ a.hsync_end == b.hsync_end && a.htotal == b.htotal && a.vdisplay == b.vdisplay &&
156 ✗ a.vsync_start == b.vsync_start && a.vsync_end == b.vsync_end && a.vtotal == b.vtotal &&
157 ✗ a.flags == b.flags;
158 }
159
160 // Up while an output is running with no display on its connector, which on a
161 // show night usually means a projector that is off or lost its signal. Comes
162 // down when the display appears and when the output is torn down; built by one
163 // function so the add and the remove always match.
164 static constexpr int WARNING_ID_NO_DISPLAY = 71;
165 2 static std::string NoDisplayWarning(const std::string& connector) {
166
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 return "Video output " + connector + ": no display detected - check that the projector or monitor is on and connected";
167 }
168
169 // The flag and the banner move together; caller holds drmMasterLock.
170 24 void KMSFrameBuffer::SetSinkPresentLocked(bool present) {
171 24 m_sinkPresent = present;
172
4/4
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 20 times.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 2 times.
24 if (!present && !m_noDisplayWarned) {
173
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 WarningHolder::AddWarning(WARNING_ID_NO_DISPLAY, NoDisplayWarning(m_connectorName));
174 2 m_noDisplayWarned = true;
175
3/4
✓ Branch 0 taken 20 times.
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 20 times.
22 } else if (present && m_noDisplayWarned) {
176 ✗ WarningHolder::RemoveWarning(WARNING_ID_NO_DISPLAY, NoDisplayWarning(m_connectorName));
177 ✗ m_noDisplayWarned = false;
178 }
179 24 }
180
181 // Only the DRM master gets a real probe; for anyone else the kernel reads back
182 // its cached state, which after a hotplug can be "connected" with no modes -
183 // the HPD interrupt updates the status but not the mode list. The real probe
184 // reads the EDID over DDC and also refreshes the sysfs mode list. The card's
185 // shared fd is master (see DrmCards.h). Caller holds drmMasterLock.
186 24 drmModeConnectorPtr KMSFrameBuffer::ProbeConnectorLocked(int fd, uint32_t connectorId) {
187 24 DrmCards::EnsureMaster(fd);
188 24 return drmModeGetConnector(fd, connectorId);
189 }
190
191 53 bool KMSFrameBuffer::CreateDumbBuffer(int fd, uint32_t width, uint32_t height, uint32_t format, DumbBuffer& buf) {
192 uint32_t bpp;
193
2/2
✓ Branch 0 taken 44 times.
✓ Branch 1 taken 9 times.
53 if (format == DRM_FORMAT_RGB888) {
194 44 bpp = 24;
195
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 } else if (format == DRM_FORMAT_RGB565) {
196 9 bpp = 16;
197 } else {
198 ✗ bpp = 32;
199 }
200
201 53 struct drm_mode_create_dumb creq = {};
202 53 creq.width = width;
203 53 creq.height = height;
204 53 creq.bpp = bpp;
205
206
2/4
✓ Branch 0 taken 53 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 53 times.
53 if (drmIoctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &creq) < 0) {
207 ✗ return false;
208 }
209
210 53 uint32_t handles[4] = { creq.handle, 0, 0, 0 };
211 53 uint32_t pitches[4] = { creq.pitch, 0, 0, 0 };
212 53 uint32_t offsets[4] = { 0, 0, 0, 0 };
213 53 uint32_t fb_id = 0;
214
215
2/4
✓ Branch 0 taken 53 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 53 times.
53 if (drmModeAddFB2(fd, width, height, format, handles, pitches, offsets, &fb_id, 0) < 0) {
216 ✗ struct drm_mode_destroy_dumb dreq = {};
217 ✗ dreq.handle = creq.handle;
218 ✗ drmIoctl(fd, DRM_IOCTL_MODE_DESTROY_DUMB, &dreq);
219 ✗ return false;
220 }
221
222 53 struct drm_mode_map_dumb mreq = {};
223 53 mreq.handle = creq.handle;
224
2/4
✓ Branch 0 taken 53 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 53 times.
53 if (drmIoctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &mreq) < 0) {
225 ✗ drmModeRmFB(fd, fb_id);
226 ✗ struct drm_mode_destroy_dumb dreq = {};
227 ✗ dreq.handle = creq.handle;
228 ✗ drmIoctl(fd, DRM_IOCTL_MODE_DESTROY_DUMB, &dreq);
229 ✗ return false;
230 }
231
232 53 void* mapped = mmap(nullptr, creq.size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, mreq.offset);
233
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 53 times.
53 if (mapped == MAP_FAILED) {
234 ✗ drmModeRmFB(fd, fb_id);
235 ✗ struct drm_mode_destroy_dumb dreq = {};
236 ✗ dreq.handle = creq.handle;
237 ✗ drmIoctl(fd, DRM_IOCTL_MODE_DESTROY_DUMB, &dreq);
238 ✗ return false;
239 }
240
241 53 buf.handle = creq.handle;
242 53 buf.fb_id = fb_id;
243 53 buf.width = width;
244 53 buf.height = height;
245 53 buf.stride = creq.pitch;
246 53 buf.format = format;
247 53 buf.size = creq.size;
248 53 buf.mapped = mapped;
249 53 return true;
250 }
251
252 45 void KMSFrameBuffer::DestroyDumbBuffer(int fd, DumbBuffer& buf) {
253
1/2
✓ Branch 0 taken 45 times.
✗ Branch 1 not taken.
45 if (buf.mapped) {
254 45 munmap(buf.mapped, buf.size);
255 45 buf.mapped = nullptr;
256 }
257
1/2
✓ Branch 0 taken 45 times.
✗ Branch 1 not taken.
45 if (buf.fb_id) {
258 45 drmModeRmFB(fd, buf.fb_id);
259 45 buf.fb_id = 0;
260 }
261
1/2
✓ Branch 0 taken 45 times.
✗ Branch 1 not taken.
45 if (buf.handle) {
262 45 struct drm_mode_destroy_dumb dreq = {};
263 45 dreq.handle = buf.handle;
264
1/2
✓ Branch 0 taken 45 times.
✗ Branch 1 not taken.
45 drmIoctl(fd, DRM_IOCTL_MODE_DESTROY_DUMB, &dreq);
265 45 buf.handle = 0;
266 }
267 45 buf = {};
268 45 }
269
270 22 uint32_t KMSFrameBuffer::FindPlaneForCrtc(int fd, uint32_t crtcId, uint32_t format) {
271 22 drmModeResPtr res = drmModeGetResources(fd);
272
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 22 times.
22 if (!res) {
273 ✗ return 0;
274 }
275
276 22 int crtcIndex = -1;
277
1/2
✓ Branch 0 taken 29 times.
✗ Branch 1 not taken.
29 for (int i = 0; i < res->count_crtcs; i++) {
278
2/2
✓ Branch 0 taken 22 times.
✓ Branch 1 taken 7 times.
29 if (res->crtcs[i] == crtcId) {
279 22 crtcIndex = i;
280 22 break;
281 }
282 }
283 22 drmModeFreeResources(res);
284
285
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 22 times.
22 if (crtcIndex < 0) {
286 ✗ return 0;
287 }
288
289 22 drmModePlaneResPtr planes = drmModeGetPlaneResources(fd);
290
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 22 times.
22 if (!planes) {
291 ✗ return 0;
292 }
293
294 22 uint32_t result = 0;
295
3/4
✓ Branch 0 taken 53 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 31 times.
✓ Branch 3 taken 22 times.
53 for (uint32_t i = 0; i < planes->count_planes && result == 0; i++) {
296 31 drmModePlanePtr plane = drmModeGetPlane(fd, planes->planes[i]);
297
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 31 times.
31 if (!plane) {
298 ✗ continue;
299 }
300
301
2/2
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 24 times.
31 if (!(plane->possible_crtcs & (1u << crtcIndex))) {
302 7 drmModeFreePlane(plane);
303 7 continue;
304 }
305
306 // Check if already reserved
307 24 bool reserved = false;
308
2/2
✓ Branch 0 taken 27 times.
✓ Branch 1 taken 24 times.
51 for (auto* card : CARDS) {
309
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 27 times.
27 for (auto pid : card->reservedPlanes) {
310 ✗ if (pid == plane->plane_id) {
311 ✗ reserved = true;
312 ✗ break;
313 }
314 }
315
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 27 times.
27 if (reserved) break;
316 }
317 // Or that a media pipeline's lease holds.
318
5/6
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 22 times.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 22 times.
24 if (reserved || DrmCards::InUse(fd, plane->plane_id)) {
319 2 drmModeFreePlane(plane);
320 2 continue;
321 }
322
323
1/2
✓ Branch 0 taken 96 times.
✗ Branch 1 not taken.
96 for (uint32_t j = 0; j < plane->count_formats; j++) {
324
2/2
✓ Branch 0 taken 22 times.
✓ Branch 1 taken 74 times.
96 if (plane->formats[j] == format) {
325 22 result = plane->plane_id;
326 22 break;
327 }
328 }
329 22 drmModeFreePlane(plane);
330 }
331 22 drmModeFreePlaneResources(planes);
332 22 return result;
333 }
334
335 23 KMSFrameBuffer::KMSFrameBuffer() {
336
1/2
✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
23 LogDebug(VB_CHANNELOUT, "KMSFrameBuffer::KMSFrameBuffer()\n");
337
338
1/2
✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
23 if (FRAMEBUFFER_COUNT == 0) {
339
2/2
✓ Branch 0 taken 230 times.
✓ Branch 1 taken 23 times.
253 for (int cn = 0; cn < 10; cn++) {
340
1/2
✓ Branch 0 taken 230 times.
✗ Branch 1 not taken.
230 std::string path = "/dev/dri/card" + std::to_string(cn);
341
3/4
✓ Branch 0 taken 230 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 25 times.
✓ Branch 3 taken 205 times.
230 if (FileExists(path)) {
342 // The card's one fd for all of fppd, which stays DRM master:
343 // media pipelines get leases from it rather than a master of
344 // their own, so nothing has to take turns (DrmCards.h).
345
1/2
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
25 int fd = DrmCards::Acquire(path);
346
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 25 times.
25 if (fd < 0) {
347 ✗ continue;
348 }
349
2/4
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 25 times.
25 CardInfo* info = new CardInfo();
350 25 info->fd = fd;
351
1/2
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
25 info->path = path;
352
1/2
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
25 CARDS.push_back(info);
353 }
354
1/2
✓ Branch 0 taken 230 times.
✗ Branch 1 not taken.
230 }
355
1/2
✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
23 StartHotplugMonitor();
356
1/2
✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
23 DrmCards::SetMediaHoldListener(&KMSFrameBuffer::MediaHoldChanged);
357 }
358 23 ++FRAMEBUFFER_COUNT;
359
360
1/2
✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
23 std::unique_lock<std::mutex> lock(drmMasterLock);
361 23 m_flipToken = ++NEXT_FLIP_TOKEN;
362
1/2
✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
23 FLIP_TARGETS[m_flipToken] = this;
363 23 }
364
365 76 KMSFrameBuffer::~KMSFrameBuffer() {
366 // The draw loop writes into the mapped dumb buffers and calls the virtual
367 // SyncDisplay(); it has to be dead before this destructor releases any of
368 // that. ~FrameBuffer() joins far too late - by then the vtable is already
369 // demoted and the card fds below may be closed.
370 38 StopDrawLoop();
371
372 {
373 38 std::unique_lock<std::mutex> lock(drmMasterLock);
374 // Retire our last flip while this object can still take its event.
375 // Past this point one that is still queued is dropped by token.
376
2/2
✓ Branch 0 taken 17 times.
✓ Branch 1 taken 2 times.
38 if (m_flipPending) {
377
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
34 int vr = m_mode.vrefresh > 0 ? m_mode.vrefresh : 60;
378 34 WaitForPendingFlip((1000 / vr) + 20);
379 }
380 38 FLIP_TARGETS.erase(m_flipToken);
381
382
4/6
✓ Branch 0 taken 18 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 18 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 18 times.
✗ Branch 5 not taken.
38 if (m_displayEnabled && m_crtcId && m_planeId) {
383 36 LogInfo(VB_CHANNELOUT, "KMSFrameBuffer::~KMSFrameBuffer() disabling display before destruction\n");
384
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
36 if (DrmCards::EnsureMaster(m_cardFd)) {
385 36 drmModeSetPlane(m_cardFd, m_planeId, m_crtcId, m_fb[m_cPage].fb_id, 0,
386 0, 0, 0, 0, 0, 0, 0, 0);
387 36 m_displayEnabled = false;
388 }
389 }
390 38 }
391
392 // Must run here, not from ~FrameBuffer(): the base destructor can only reach
393 // FrameBuffer::DestroyFrameBuffer(), so the dumb buffers and the connector /
394 // CRTC / plane reservations were never released on delete. It also has to
395 // happen before the card fds are closed below.
396 38 KMSFrameBuffer::DestroyFrameBuffer();
397
398 38 --FRAMEBUFFER_COUNT;
399
1/2
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
38 if (FRAMEBUFFER_COUNT == 0) {
400 38 StopHotplugMonitor();
401
2/2
✓ Branch 0 taken 18 times.
✓ Branch 1 taken 19 times.
74 for (auto* card : CARDS) {
402
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
36 if (card->fd >= 0) {
403 36 DrmCards::Release(card->path);
404 }
405
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
36 delete card;
406 }
407 38 CARDS.clear();
408 }
409 76 }
410
411 23 int KMSFrameBuffer::InitializeFrameBuffer(void) {
412 23 LogDebug(VB_CHANNELOUT, "KMSFrameBuffer::InitializeFrameBuffer()\n");
413
414
2/2
✓ Branch 0 taken 25 times.
✓ Branch 1 taken 1 times.
26 for (auto* card : CARDS) {
415
1/2
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
25 drmModeResPtr res = drmModeGetResources(card->fd);
416
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 22 times.
25 if (!res) {
417 3 continue;
418 }
419
420
1/2
✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
23 for (int ci = 0; ci < res->count_connectors; ci++) {
421
1/2
✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
23 drmModeConnectorPtr conn = drmModeGetConnectorCurrent(card->fd, res->connectors[ci]);
422
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 23 times.
23 if (!conn) {
423 1 continue;
424 }
425
1/2
✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
23 std::string cname = ConnectorFullName(card->fd, conn);
426
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 22 times.
23 if (m_device != cname) {
427
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 drmModeFreeConnector(conn);
428 1 continue;
429 }
430
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
22 drmModeFreeConnector(conn);
431 {
432
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
22 std::unique_lock<std::mutex> lock(drmMasterLock);
433
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
22 conn = ProbeConnectorLocked(card->fd, res->connectors[ci]);
434 22 }
435
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 22 times.
22 if (!conn) {
436 ✗ continue;
437 }
438
439 // An external display that is off or unplugged right now is not a
440 // reason to have no output: run a standard mode until it shows up
441 // and the hotplug handler adopts its own. Before, the output was
442 // simply never created, and only a restart brought it back.
443
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
22 bool present = SinkPresent(card->fd, conn);
444 22 bool external = IsExternalConnector(conn->connector_type);
445
3/6
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 20 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
22 if (!present && !external && conn->connection != DRM_MODE_CONNECTED) {
446 ✗ drmModeFreeConnector(conn);
447 ✗ continue;
448 }
449
450 // Check if already reserved
451 22 bool alreadyReserved = false;
452
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 22 times.
22 for (auto cid : card->reservedConnectors) {
453 ✗ if (cid == conn->connector_id) {
454 ✗ alreadyReserved = true;
455 ✗ break;
456 }
457 }
458
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 22 times.
22 if (alreadyReserved) {
459 ✗ drmModeFreeConnector(conn);
460 ✗ continue;
461 }
462
463 // Find a CRTC for this connector
464 22 uint32_t crtcId = 0;
465
466 // Try the encoder's current CRTC first
467
2/2
✓ Branch 0 taken 20 times.
✓ Branch 1 taken 2 times.
22 if (conn->encoder_id) {
468
1/2
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
20 drmModeEncoderPtr enc = drmModeGetEncoder(card->fd, conn->encoder_id);
469
1/2
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
20 if (enc) {
470 // Check if this CRTC is already reserved
471 20 bool crtcReserved = false;
472
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 20 times.
20 for (auto rid : card->reservedCrtcs) {
473 ✗ if (rid == enc->crtc_id) {
474 ✗ crtcReserved = true;
475 ✗ break;
476 }
477 }
478
3/4
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 19 times.
✓ Branch 3 taken 1 times.
20 if (!crtcReserved && enc->crtc_id) {
479 19 crtcId = enc->crtc_id;
480 }
481
1/2
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
20 drmModeFreeEncoder(enc);
482 }
483 }
484
485 // If no CRTC yet, try all encoders for this connector
486
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 19 times.
22 if (crtcId == 0) {
487
3/4
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
6 for (int ei = 0; ei < conn->count_encoders && crtcId == 0; ei++) {
488
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 drmModeEncoderPtr enc = drmModeGetEncoder(card->fd, conn->encoders[ei]);
489
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (!enc) continue;
490
4/4
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 7 times.
✓ Branch 3 taken 2 times.
10 for (int ci2 = 0; ci2 < res->count_crtcs && crtcId == 0; ci2++) {
491
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 4 times.
7 if (enc->possible_crtcs & (1u << ci2)) {
492 3 bool crtcReserved = false;
493
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 for (auto rid : card->reservedCrtcs) {
494 ✗ if (rid == res->crtcs[ci2]) {
495 ✗ crtcReserved = true;
496 ✗ break;
497 }
498 }
499
4/8
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 3 times.
✗ Branch 7 not taken.
3 if (!crtcReserved && !DrmCards::InUse(card->fd, res->crtcs[ci2])) {
500 3 crtcId = res->crtcs[ci2];
501 }
502 }
503 }
504
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 drmModeFreeEncoder(enc);
505 }
506 }
507
508
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 22 times.
22 if (crtcId == 0) {
509 ✗ drmModeFreeConnector(conn);
510 ✗ drmModeFreeResources(res);
511 ✗ return 0;
512 }
513
514 22 drmModeModeInfo mode = {};
515 22 bool foundMode = false;
516
3/4
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 20 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
22 if (present || !external) {
517
1/2
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
20 foundMode = PickMode(conn, mode);
518 } else {
519 // A forced connector (video=...e, or ForceHDMI) with no EDID
520 // still carries the command line's mode: run that.
521
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (!CommandLineMode(conn, mode)) {
522 2 mode = FallbackMode();
523 }
524 2 foundMode = true;
525
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 LogInfo(VB_CHANNELOUT, "KMSFrameBuffer: no display on %s yet, running %dx%d @ %dHz until one appears\n",
526 cname.c_str(), mode.hdisplay, mode.vdisplay, mode.vrefresh);
527 }
528
529
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 22 times.
22 if (!foundMode) {
530 ✗ drmModeFreeConnector(conn);
531 ✗ drmModeFreeResources(res);
532 ✗ return 0;
533 }
534
535 22 m_mode = mode;
536 22 m_connectorId = conn->connector_id;
537 22 m_crtcId = crtcId;
538 22 m_crtcPipe = 0;
539
2/2
✓ Branch 0 taken 31 times.
✓ Branch 1 taken 22 times.
53 for (int ci = 0; ci < res->count_crtcs; ci++) {
540
2/2
✓ Branch 0 taken 22 times.
✓ Branch 1 taken 9 times.
31 if (res->crtcs[ci] == crtcId) {
541 22 m_crtcPipe = ci;
542 }
543 }
544
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
22 m_connectorName = cname;
545 22 m_cardFd = card->fd;
546 22 m_cardInfo = card;
547
548 // The panel refresh caps the sustainable output frame rate: page
549 // flips are vblank-paced, so a sequence faster than this will be
550 // limited to the refresh rate.
551
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
22 LogInfo(VB_CHANNELOUT, "KMSFrameBuffer: %s mode %dx%d @ %dHz\n",
552 cname.c_str(), mode.hdisplay, mode.vdisplay, mode.vrefresh);
553
554
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
22 card->reservedConnectors.push_back(m_connectorId);
555
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
22 card->reservedCrtcs.push_back(m_crtcId);
556
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
22 DrmCards::Claim(card->fd, m_connectorId);
557
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
22 DrmCards::Claim(card->fd, m_crtcId);
558
559
2/2
✓ Branch 0 taken 13 times.
✓ Branch 1 taken 9 times.
22 if (m_variableRefresh) {
560 // DPI Pixels: drive our own vertical timing. Keep the
561 // connector's horizontal timing and pixel clock (they define the
562 // WS281x bit timing) but set vactive to the caller's requested
563 // scanline count (sized to the longest string) and start at the
564 // maximum refresh rate. DPIPixelsOutput lowers it per-sequence
565 // via SetRefreshRate() by only varying the vertical blanking, so
566 // the framebuffer and its contents never have to be rebuilt.
567
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 if (m_width == 0) {
568 13 m_width = m_mode.hdisplay;
569 }
570
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 13 times.
13 if (m_height == 0) {
571 ✗ m_height = m_mode.vdisplay;
572 }
573 13 m_pixelSize = 1;
574
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 ApplyVerticalTiming(m_mode, m_height, 1000000); // clamped to max fps
575 } else {
576
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (m_width == 0) {
577 ✗ m_width = m_mode.hdisplay;
578 }
579
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (m_height == 0) {
580 ✗ m_height = m_mode.vdisplay;
581 }
582
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 if (m_pixelSize == 0) {
583 9 int mw = m_mode.hdisplay / m_width;
584 9 int mh = m_mode.vdisplay / m_height;
585 9 m_pixelSize = std::min(mw, mh);
586
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (m_pixelSize < 1) {
587 ✗ m_pixelSize = 1;
588 }
589 9 m_width *= m_pixelSize;
590 9 m_height *= m_pixelSize;
591 }
592 }
593
594 22 m_bpp = 24;
595 22 uint32_t format = DRM_FORMAT_RGB888;
596
2/2
✓ Branch 0 taken 44 times.
✓ Branch 1 taken 22 times.
66 for (int x = 0; x < 2; x++) {
597
2/4
✓ Branch 0 taken 44 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 44 times.
44 if (!CreateDumbBuffer(card->fd, m_width, m_height, format, m_fb[x])) {
598 ✗ format = DRM_FORMAT_XRGB8888;
599 ✗ m_bpp = 32;
600 ✗ if (!CreateDumbBuffer(card->fd, m_width, m_height, format, m_fb[x])) {
601 ✗ LogErr(VB_CHANNELOUT, "Failed to create dumb buffer\n");
602 // Free any buffer(s) already created and undo the
603 // connector/CRTC reservations pushed above so we don't
604 // leak CMA or permanently reserve this output.
605 ✗ for (int y = 0; y < x; y++) {
606 ✗ DestroyDumbBuffer(card->fd, m_fb[y]);
607 ✗ m_pageBuffers[y] = nullptr;
608 }
609 ✗ auto& rc = card->reservedConnectors;
610 ✗ rc.erase(std::remove(rc.begin(), rc.end(), m_connectorId), rc.end());
611 ✗ auto& rcr = card->reservedCrtcs;
612 ✗ rcr.erase(std::remove(rcr.begin(), rcr.end(), m_crtcId), rcr.end());
613 ✗ DrmCards::Unclaim(card->fd, m_connectorId);
614 ✗ DrmCards::Unclaim(card->fd, m_crtcId);
615 ✗ m_connectorId = 0;
616 ✗ m_crtcId = 0;
617 ✗ drmModeFreeConnector(conn);
618 ✗ drmModeFreeResources(res);
619 ✗ return 0;
620 }
621 }
622 44 m_pageBuffers[x] = (uint8_t*)m_fb[x].mapped;
623 }
624
625
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
22 m_planeId = FindPlaneForCrtc(card->fd, m_crtcId, format);
626
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
22 if (m_planeId) {
627
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
22 card->reservedPlanes.push_back(m_planeId);
628
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
22 DrmCards::Claim(card->fd, m_planeId);
629 }
630 22 m_pages = 2;
631
632 22 m_cPage = 0;
633 22 m_pPage = 0;
634 22 m_rowStride = m_fb[0].stride;
635 22 m_rowPadding = m_rowStride - (m_width * m_bpp / 8);
636 22 m_pageSize = m_fb[0].size;
637 22 m_bufferSize = m_pageSize;
638
639
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
22 if (m_planeId) {
640 22 drmModeSetPlane(m_cardFd, m_planeId, m_crtcId, m_fb[0].fb_id, 0,
641 22 0, 0, m_mode.hdisplay, m_mode.vdisplay,
642
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
22 0, 0, (uint32_t)m_width << 16, (uint32_t)m_height << 16);
643 }
644
645
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
22 drmModeFreeConnector(conn);
646
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
22 drmModeFreeResources(res);
647
648 {
649
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
22 std::unique_lock<std::mutex> lock(drmMasterLock);
650
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
22 SetSinkPresentLocked(present);
651 22 m_hotplugReady = true;
652 22 }
653 22 return 1;
654
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 22 times.
23 }
655 ✗ drmModeFreeResources(res);
656 }
657 1 return 0;
658 }
659
660 19 void KMSFrameBuffer::DestroyFrameBuffer(void) {
661 {
662
1/2
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
19 std::unique_lock<std::mutex> lock(drmMasterLock);
663 19 m_hotplugReady = false;
664
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 19 times.
19 if (m_noDisplayWarned) {
665 ✗ WarningHolder::RemoveWarning(WARNING_ID_NO_DISPLAY, NoDisplayWarning(m_connectorName));
666 ✗ m_noDisplayWarned = false;
667 }
668 19 }
669
2/2
✓ Branch 0 taken 57 times.
✓ Branch 1 taken 19 times.
76 for (int x = 0; x < 3; x++) {
670
2/2
✓ Branch 0 taken 36 times.
✓ Branch 1 taken 21 times.
57 if (m_fb[x].handle) {
671 36 DestroyDumbBuffer(m_cardFd, m_fb[x]);
672 36 m_pageBuffers[x] = nullptr;
673 }
674 }
675
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 19 times.
19 if (m_blankFb.handle) {
676 ✗ DestroyDumbBuffer(m_cardFd, m_blankFb);
677 }
678 19 m_mediaBlanked = false;
679
2/2
✓ Branch 0 taken 18 times.
✓ Branch 1 taken 1 times.
19 if (m_cardInfo) {
680
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 if (m_planeId) {
681 18 auto& planes = m_cardInfo->reservedPlanes;
682
2/4
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 18 times.
✗ Branch 3 not taken.
18 planes.erase(std::remove(planes.begin(), planes.end(), m_planeId), planes.end());
683 18 DrmCards::Unclaim(m_cardInfo->fd, m_planeId);
684 }
685
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 if (m_crtcId) {
686 18 auto& crtcs = m_cardInfo->reservedCrtcs;
687
2/4
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 18 times.
✗ Branch 3 not taken.
18 crtcs.erase(std::remove(crtcs.begin(), crtcs.end(), m_crtcId), crtcs.end());
688 18 DrmCards::Unclaim(m_cardInfo->fd, m_crtcId);
689 }
690
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 if (m_connectorId) {
691 18 auto& conns = m_cardInfo->reservedConnectors;
692
2/4
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 18 times.
✗ Branch 3 not taken.
18 conns.erase(std::remove(conns.begin(), conns.end(), m_connectorId), conns.end());
693 18 DrmCards::Unclaim(m_cardInfo->fd, m_connectorId);
694 }
695 }
696
697 19 m_connectorId = 0;
698 19 m_crtcId = 0;
699 19 m_planeId = 0;
700 19 m_cardInfo = nullptr;
701 19 FrameBuffer::DestroyFrameBuffer();
702 19 }
703
704 ✗ void KMSFrameBuffer::SyncLoop() {
705 ✗ if (m_pages == 1) {
706 ✗ return;
707 }
708
709 ✗ while (m_runLoop) {
710 // Allow the producer to catch up if needed
711 ✗ if (!m_dirtyPages[m_cPage] && m_dirtyPages[(m_cPage + 1) % m_pages])
712 ✗ NextPage();
713
714 ✗ if (m_dirtyPages[m_cPage]) {
715 // SyncDisplay() blocks on the vblank completion event, so it paces
716 // this loop to the panel refresh without spinning.
717 ✗ SyncDisplay(true);
718
719 ✗ m_dirtyPages[m_cPage] = 0;
720 ✗ NextPage();
721 } else {
722 // Nothing to flip yet. Sleep briefly instead of busy-spinning so an
723 // idle output doesn't peg a CPU core (the original reason AUTO_SYNC
724 // was abandoned).
725 ✗ std::this_thread::sleep_for(std::chrono::microseconds(250));
726 }
727 }
728 }
729
730 // Whether a media pipeline is showing on this connector, so this output must
731 // leave it alone. Its kmssink holds the connector through a DRM lease from
732 // before the pipeline starts until it is gone, whichever slot it plays on and
733 // whatever the output is called -- the old test compared slot 1's status to
734 // the connector name, which Play Media and "--HDMI--" never matched.
735 2072 bool KMSFrameBuffer::MediaHoldsConnector() const {
736
3/4
✓ Branch 0 taken 2072 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 180 times.
✓ Branch 3 taken 1892 times.
2072 return m_connectorId && DrmCards::HeldByMedia(m_cardFd, m_connectorId);
737 }
738
739 // Take our plane off the CRTC, which stays lit for the video on top of it.
740 // Black would hide the last frame as well, but a plane showing black still
741 // counts against the HVS load: on a Pi 0-3 a framebuffer scaled to the mode
742 // under a full-screen 1080p video is over the kernel's limit, and the video's
743 // first frame fails with ENOSPC. Caller holds drmMasterLock.
744 91 void KMSFrameBuffer::BlankForMediaLocked() {
745
8/10
✓ Branch 0 taken 89 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 89 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✓ Branch 5 taken 86 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 3 times.
✓ Branch 8 taken 88 times.
✓ Branch 9 taken 3 times.
91 if (m_mediaBlanked || !m_planeId || !m_displayEnabled || !DrmCards::EnsureMaster(m_cardFd)) {
746 88 return;
747 }
748
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 times.
3 if (m_flipPending) {
749
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 int vr = m_mode.vrefresh > 0 ? m_mode.vrefresh : 60;
750 2 WaitForPendingFlip((1000 / vr) + 20);
751 }
752
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (drmModeSetPlane(m_cardFd, m_planeId, m_crtcId, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) != 0) {
753 // A driver that needs a plane on a lit CRTC: black, then.
754 ✗ LogDebug(VB_CHANNELOUT, "KMSFrameBuffer %s: can't take the plane off (%s), blanking it\n",
755 m_connectorName.c_str(), strerror(errno));
756 ✗ if (!m_blankFb.handle) {
757 ✗ if (!CreateDumbBuffer(m_cardFd, m_fb[0].width, m_fb[0].height, m_fb[0].format, m_blankFb)) {
758 ✗ return;
759 }
760 ✗ memset(m_blankFb.mapped, 0, m_blankFb.size);
761 }
762 ✗ drmModeSetPlane(m_cardFd, m_planeId, m_crtcId, m_blankFb.fb_id, 0,
763 ✗ 0, 0, m_mode.hdisplay, m_mode.vdisplay,
764 ✗ 0, 0, (uint32_t)m_width << 16, (uint32_t)m_height << 16);
765 }
766 3 m_mediaBlanked = true;
767 3 LogInfo(VB_CHANNELOUT, "KMSFrameBuffer %s: media is playing here, standing aside\n", m_connectorName.c_str());
768 }
769
770 // The media is gone. Its kmssink may have left the CRTC in another state, so
771 // put our page back with a plane commit rather than trusting a flip from what
772 // was there before. Caller holds drmMasterLock.
773 1862 void KMSFrameBuffer::ShowAfterMediaLocked() {
774
2/2
✓ Branch 0 taken 1859 times.
✓ Branch 1 taken 3 times.
1862 if (!m_mediaBlanked) {
775 1859 return;
776 }
777 3 m_mediaBlanked = false;
778 3 m_flipPending = false;
779
4/8
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 3 times.
6 if (m_displayEnabled && m_planeId && DrmCards::EnsureMaster(m_cardFd) &&
780
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 drmModeSetPlane(m_cardFd, m_planeId, m_crtcId, m_fb[m_cPage].fb_id, 0,
781 3 0, 0, m_mode.hdisplay, m_mode.vdisplay,
782 3 0, 0, (uint32_t)m_width << 16, (uint32_t)m_height << 16) != 0) {
783 ✗ m_displayEnabled = false; // relit with a full modeset on the next flip
784 }
785 3 LogInfo(VB_CHANNELOUT, "KMSFrameBuffer %s: media finished, showing output again\n", m_connectorName.c_str());
786 }
787
788 // At once, not on our next flip: an output nothing is sending data to has
789 // none, and would sit under the video at full cost. Caller (DrmCards) holds
790 // drmMasterLock, which also guards FLIP_TARGETS.
791 15 void KMSFrameBuffer::MediaHoldChanged(int fd, uint32_t connectorId) {
792
2/2
✓ Branch 0 taken 15 times.
✓ Branch 1 taken 15 times.
30 for (auto& [token, fb] : FLIP_TARGETS) {
793
3/4
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✓ Branch 3 taken 9 times.
15 if (fb->m_cardFd != fd || fb->m_connectorId != connectorId) {
794 6 continue;
795 }
796
3/4
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✓ Branch 3 taken 4 times.
9 if (fb->MediaHoldsConnector()) {
797
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 fb->BlankForMediaLocked();
798 } else {
799
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 fb->ShowAfterMediaLocked();
800 }
801 }
802 15 }
803
804 1855 void KMSFrameBuffer::PageFlipHandler(int fd, unsigned int frame, unsigned int sec, unsigned int usec, void* data) {
805 // data is the flip token of the instance that queued this flip, so even if
806 // we drain an event that belongs to another KMSFrameBuffer sharing the same
807 // card fd, the correct instance's pending flag is cleared. Runs inside
808 // drmHandleEvent under drmMasterLock, which also guards FLIP_TARGETS.
809 1855 uint32_t token = (uint32_t)(uintptr_t)data;
810
1/2
✓ Branch 0 taken 1855 times.
✗ Branch 1 not taken.
1855 auto it = FLIP_TARGETS.find(token);
811
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1855 times.
1855 if (it == FLIP_TARGETS.end()) {
812 ✗ LogWarn(VB_CHANNELOUT, "KMSFrameBuffer: dropped the flip event of a deleted framebuffer (token %u)\n", token);
813 ✗ return;
814 }
815 1855 KMSFrameBuffer* self = it->second;
816 {
817 1855 self->m_flipPending = false;
818 // "frame" is the vblank sequence number at flip completion; track the
819 // cadence. Runs inside drmHandleEvent under drmMasterLock.
820
3/4
✓ Branch 0 taken 1826 times.
✓ Branch 1 taken 29 times.
✓ Branch 2 taken 1826 times.
✗ Branch 3 not taken.
1855 if (self->m_lastFlipSeq != 0 && frame > self->m_lastFlipSeq) {
821 1826 uint32_t delta = frame - self->m_lastFlipSeq;
822 1826 self->m_vblankDeltaCounts[delta > 4 ? 4 : delta]++;
823
4/4
✓ Branch 0 taken 1800 times.
✓ Branch 1 taken 26 times.
✓ Branch 2 taken 64 times.
✓ Branch 3 taken 1736 times.
1826 if (self->m_prevVblankDelta != 0 && delta != self->m_prevVblankDelta) {
824 64 self->m_vblankCadenceBreaks++;
825 }
826 1826 self->m_prevVblankDelta = delta;
827 }
828 1855 self->m_lastFlipSeq = frame;
829 1855 self->m_lastFlipTimeUS = (uint64_t)sec * 1000000ULL + usec;
830 }
831 }
832
833 1855 void KMSFrameBuffer::WaitForPendingFlip(int timeoutMs) {
834
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1855 times.
1855 if (!m_flipPending) {
835 ✗ return;
836 }
837 1855 struct pollfd pfd = {};
838 1855 pfd.fd = m_cardFd;
839 1855 pfd.events = POLLIN;
840
841 1855 drmEventContext evctx = {};
842 1855 evctx.version = 2;
843 1855 evctx.page_flip_handler = &KMSFrameBuffer::PageFlipHandler;
844
845 // Normally the vblank event is already queued (or arrives within one refresh
846 // interval) so poll() returns almost immediately. The timeout is only a
847 // safety net so a dropped/lost event can never wedge the output thread.
848
1/2
✓ Branch 0 taken 1855 times.
✗ Branch 1 not taken.
1855 int r = poll(&pfd, 1, timeoutMs);
849
2/4
✓ Branch 0 taken 1855 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1855 times.
✗ Branch 3 not taken.
1855 if (r > 0 && (pfd.revents & POLLIN)) {
850
1/2
✓ Branch 0 taken 1855 times.
✗ Branch 1 not taken.
1855 drmHandleEvent(m_cardFd, &evctx);
851 } else {
852 ✗ m_flipWaitTimeouts++;
853 }
854 // If we timed out (or errored) give up on this flip so the next frame can
855 // proceed rather than blocking forever.
856 1855 m_flipPending = false;
857 }
858
859 767 void KMSFrameBuffer::WaitForPageFree() {
860
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 767 times.
767 if (m_pages == 1) {
861 ✗ return;
862 }
863
1/2
✓ Branch 0 taken 767 times.
✗ Branch 1 not taken.
767 std::unique_lock<std::mutex> lock(drmMasterLock);
864
2/2
✓ Branch 0 taken 671 times.
✓ Branch 1 taken 96 times.
767 if (m_flipPending) {
865
1/2
✓ Branch 0 taken 671 times.
✗ Branch 1 not taken.
671 int vr = m_mode.vrefresh > 0 ? m_mode.vrefresh : 60;
866 671 uint32_t timeoutsBefore = m_flipWaitTimeouts;
867
1/2
✓ Branch 0 taken 671 times.
✗ Branch 1 not taken.
671 WaitForPendingFlip((1000 / vr) + 20);
868
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 671 times.
671 if (m_flipWaitTimeouts != timeoutsBefore) {
869 // The completion event never came, so it is unknown whether the
870 // hardware has switched pages yet. A queued flip lands at the next
871 // vblank at the latest, so wait one out before the caller writes
872 // into the page it is about to leave. Ask for the vblank as an
873 // event on OUR crtc (the pipe bits select it; without them this
874 // would be pipe 0, which on a Pi 4 with HDMI up is not the DPI) and
875 // poll for it with the same bound as the flip wait, so a display
876 // whose vblanks have stopped cannot wedge the output thread here.
877 ✗ drmVBlank vbl = {};
878 ✗ unsigned int type = DRM_VBLANK_RELATIVE | DRM_VBLANK_EVENT;
879 ✗ if (m_crtcPipe > 0) {
880 ✗ type |= (unsigned int)((m_crtcPipe << _DRM_VBLANK_HIGH_CRTC_SHIFT) & _DRM_VBLANK_HIGH_CRTC_MASK);
881 }
882 ✗ vbl.request.type = (drmVBlankSeqType)type;
883 ✗ vbl.request.sequence = 1;
884 ✗ vbl.request.signal = 0;
885 ✗ if (drmWaitVBlank(m_cardFd, &vbl) == 0) {
886 ✗ struct pollfd pfd = {};
887 ✗ pfd.fd = m_cardFd;
888 ✗ pfd.events = POLLIN;
889 ✗ if (poll(&pfd, 1, (1000 / vr) + 20) > 0 && (pfd.revents & POLLIN)) {
890 ✗ drmEventContext evctx = {};
891 ✗ evctx.version = 2;
892 ✗ evctx.page_flip_handler = &KMSFrameBuffer::PageFlipHandler;
893 ✗ evctx.vblank_handler = &KMSFrameBuffer::VBlankHandler;
894 ✗ drmHandleEvent(m_cardFd, &evctx);
895 }
896 }
897 }
898 }
899 767 }
900
901 ✗ void KMSFrameBuffer::VBlankHandler(int fd, unsigned int frame, unsigned int sec, unsigned int usec, void* data) {
902 // Nothing to record; the wait in WaitForPageFree() only needs the event
903 // to have been delivered.
904 ✗ }
905
906 1944 void KMSFrameBuffer::SyncDisplay(bool pageChanged) {
907
2/4
✓ Branch 0 taken 1944 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1944 times.
1944 if (!pageChanged || m_pages == 1)
908 86 return;
909
910
1/2
✓ Branch 0 taken 1944 times.
✗ Branch 1 not taken.
1944 std::unique_lock<std::mutex> lock(drmMasterLock);
911
3/4
✓ Branch 0 taken 1944 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 86 times.
✓ Branch 3 taken 1858 times.
1944 if (MediaHoldsConnector()) {
912
1/2
✓ Branch 0 taken 86 times.
✗ Branch 1 not taken.
86 BlankForMediaLocked();
913 86 return;
914 }
915
1/2
✓ Branch 0 taken 1858 times.
✗ Branch 1 not taken.
1858 ShowAfterMediaLocked();
916 {
917
1/2
✓ Branch 0 taken 1858 times.
✗ Branch 1 not taken.
1858 long long syncStart = GetMonotonicTimeMS();
918
2/4
✓ Branch 0 taken 1858 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1858 times.
✗ Branch 3 not taken.
1858 if (DrmCards::EnsureMaster(m_cardFd)) {
919 // If the previous flip hasn't retired yet, wait for its vblank
920 // completion event instead of letting this flip return EBUSY and
921 // fall into the expensive SetPlane/SetCrtc modeset path. poll()
922 // sleeps until the flip completes, so this paces us to the panel's
923 // refresh with no busy-waiting.
924
2/2
✓ Branch 0 taken 1155 times.
✓ Branch 1 taken 703 times.
1858 if (m_flipPending) {
925
1/2
✓ Branch 0 taken 1155 times.
✗ Branch 1 not taken.
1155 int vr = m_mode.vrefresh > 0 ? m_mode.vrefresh : 60;
926 1155 int timeoutMs = (1000 / vr) + 20; // one refresh interval + slack
927
1/2
✓ Branch 0 taken 1155 times.
✗ Branch 1 not taken.
1155 WaitForPendingFlip(timeoutMs);
928 }
929
930 // Phase alignment: fppd's software frame clock and the panel's
931 // vblank clock are never exactly the same rate, so the submission
932 // time slowly drifts across the vblank grid. When it straddles a
933 // boundary, scheduling jitter makes alternate frames catch/miss
934 // the vblank and frames display for e.g. 25ms/75ms instead of a
935 // steady 50ms - a visible judder burst until the drift clears the
936 // boundary (confirmed via the vblank-delta counters: 1,3,1,3
937 // bursts exactly at observed stutters). A "nudge only near the
938 // boundary" zone just moves the discontinuity to the zone edge
939 // (confirmed in testing: crossings leaked at the edge while the
940 // phase transited the zone), so instead align EVERY submission to
941 // the middle of the vblank interval - scheduling jitter can never
942 // reach a boundary from there, and the clock drift shows up as
943 // this sleep slowly varying. When the sleep wraps, the
944 // unavoidable one-vblank phase slip happens once, cleanly, per
945 // beat cycle instead of oscillating. Align unconditionally:
946 // gating this on "slack since the last completion" feeds back on
947 // itself (an aligned flip completes later in the cycle, the gate
948 // then skips the next frame, which lands early - producing a
949 // permanent 1,3,1,3 oscillation; confirmed in testing). The
950 // completion timestamps are vblank-quantized, so aligning every
951 // frame pins submissions to the true grid midpoint and is stable
952 // at any frame rate at or below the refresh rate.
953 1858 long long alignSleepUS = 0;
954
3/4
✓ Branch 0 taken 1836 times.
✓ Branch 1 taken 22 times.
✓ Branch 2 taken 1836 times.
✗ Branch 3 not taken.
1858 if (m_lastFlipTimeUS != 0 && m_mode.vrefresh > 0) {
955 struct timespec tsm;
956 1836 clock_gettime(CLOCK_MONOTONIC, &tsm);
957 1836 uint64_t nowUS = (uint64_t)tsm.tv_sec * 1000000ULL + tsm.tv_nsec / 1000;
958 1836 uint64_t periodUS = 1000000ULL / m_mode.vrefresh;
959 // The 60s cap also skips alignment if the driver reported
960 // non-monotonic timestamps - elapsed would be implausible.
961
1/2
✓ Branch 0 taken 1836 times.
✗ Branch 1 not taken.
1836 if (nowUS > m_lastFlipTimeUS &&
962
1/2
✓ Branch 0 taken 1836 times.
✗ Branch 1 not taken.
1836 (nowUS - m_lastFlipTimeUS) < 60000000ULL) {
963 1836 uint64_t phaseUS = (nowUS - m_lastFlipTimeUS) % periodUS;
964 1836 uint64_t sleepUS = (periodUS / 2 + periodUS - phaseUS) % periodUS;
965 // Already within 2ms past mid-interval: close enough,
966 // don't sleep a nearly full period chasing the target.
967
3/4
✓ Branch 0 taken 1836 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1823 times.
✓ Branch 3 taken 13 times.
1836 if (sleepUS > 0 && sleepUS < periodUS - 2000) {
968 1823 m_phaseNudges++;
969 1823 alignSleepUS = sleepUS;
970
1/2
✓ Branch 0 taken 1823 times.
✗ Branch 1 not taken.
1823 std::this_thread::sleep_for(std::chrono::microseconds(sleepUS));
971 }
972 }
973 }
974
975 1858 bool wasEnabled = m_displayEnabled;
976 1858 int ret = -1;
977
2/2
✓ Branch 0 taken 1856 times.
✓ Branch 1 taken 2 times.
1858 if (m_displayEnabled) {
978 1856 ret = drmModePageFlip(m_cardFd, m_crtcId, m_fb[m_cPage].fb_id,
979
1/2
✓ Branch 0 taken 1856 times.
✗ Branch 1 not taken.
1856 DRM_MODE_PAGE_FLIP_EVENT, (void*)(uintptr_t)m_flipToken);
980 }
981
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1856 times.
1858 if (ret) {
982
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (wasEnabled) {
983 // Flip rejected on an already-enabled display; the modeset
984 // fallback below costs multiple vblanks.
985 ✗ m_flipRejects++;
986 }
987 // First flip after enable, or the flip was rejected: bring the
988 // display up with a full modeset/plane commit, then queue a flip.
989
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (!m_displayEnabled) {
990
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 ModesetLocked();
991 2 m_displayEnabled = true;
992 ✗ } else if (m_planeId) {
993 ✗ drmModeSetPlane(m_cardFd, m_planeId, m_crtcId, m_fb[m_cPage].fb_id, 0,
994 ✗ 0, 0, m_mode.hdisplay, m_mode.vdisplay,
995 ✗ 0, 0, (uint32_t)m_width << 16, (uint32_t)m_height << 16);
996 }
997 2 ret = drmModePageFlip(m_cardFd, m_crtcId, m_fb[m_cPage].fb_id,
998
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 DRM_MODE_PAGE_FLIP_EVENT, (void*)(uintptr_t)m_flipToken);
999 }
1000 1858 m_flipPending = (ret == 0);
1001
1002 // Track flip-path health. A single sync should normally complete
1003 // well inside one refresh interval; count the ones that don't and
1004 // surface them (rate-limited) at Warn level so output stutters can
1005 // be correlated with (or ruled out of) the KMS flip path without
1006 // needing debug logging.
1007
1/2
✓ Branch 0 taken 1858 times.
✗ Branch 1 not taken.
1858 long long syncEnd = GetMonotonicTimeMS();
1008 // The phase-alignment sleep is intentional; don't count it as a
1009 // flip-path delay.
1010 1858 int tookMS = (int)(syncEnd - syncStart) - (int)(alignSleepUS / 1000);
1011
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1858 times.
1858 if (tookMS > 20) {
1012 ✗ m_slowSyncs++;
1013 ✗ m_lastSlowSyncMS = tookMS;
1014 ✗ if (tookMS > m_maxSyncMS) {
1015 ✗ m_maxSyncMS = tookMS;
1016 }
1017 }
1018 // Warn only when a new delay event has occurred since the last
1019 // warning (rate-limited), so each warning line timestamps actual
1020 // trouble rather than repeating stale totals.
1021 1858 uint64_t eventTotal = (uint64_t)m_slowSyncs + m_flipWaitTimeouts + m_flipRejects + m_vblankCadenceBreaks;
1022
2/2
✓ Branch 0 taken 717 times.
✓ Branch 1 taken 1141 times.
1858 if (eventTotal != m_warnedEventTotal &&
1023
2/2
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 708 times.
717 (syncEnd - m_lastFlipWarnMS) > 10000) {
1024 9 m_lastFlipWarnMS = syncEnd;
1025 9 m_warnedEventTotal = eventTotal;
1026
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 LogWarn(VB_CHANNELOUT, "KMSFrameBuffer %s flip-path event; totals since start: %u syncs >20ms (max %dms, last %dms), %u wait timeouts, %u flip rejects, %u vblank cadence breaks (deltas 1/2/3/4+: %u/%u/%u/%u), %u phase nudges\n",
1027 m_connectorName.c_str(), m_slowSyncs, m_maxSyncMS, m_lastSlowSyncMS, m_flipWaitTimeouts, m_flipRejects,
1028 m_vblankCadenceBreaks,
1029 m_vblankDeltaCounts[1], m_vblankDeltaCounts[2], m_vblankDeltaCounts[3], m_vblankDeltaCounts[4],
1030 m_phaseNudges);
1031 }
1032 }
1033 }
1034
2/2
✓ Branch 0 taken 1858 times.
✓ Branch 1 taken 86 times.
1944 }
1035
1036 ✗ void KMSFrameBuffer::DisableDisplay() {
1037 ✗ std::unique_lock<std::mutex> lock(drmMasterLock);
1038 ✗ if (MediaHoldsConnector()) {
1039 ✗ return;
1040 }
1041 {
1042 ✗ if (DrmCards::EnsureMaster(m_cardFd)) {
1043 ✗ if (m_crtcId && m_planeId) {
1044 ✗ drmModeSetPlane(m_cardFd, m_planeId, m_crtcId, m_fb[m_cPage].fb_id, 0,
1045 0, 0, 0, 0, 0, 0, 0, 0);
1046 ✗ drmModeSetCrtc(m_cardFd, m_crtcId, 0, 0, 0, nullptr, 0, nullptr);
1047 ✗ m_displayEnabled = false;
1048 ✗ m_flipPending = false;
1049 }
1050 }
1051 }
1052 ✗ }
1053
1054 11 int KMSFrameBuffer::ModesetLocked() {
1055
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 11 times.
11 if (!m_planeId) {
1056 ✗ return drmModeSetCrtc(m_cardFd, m_crtcId, m_fb[m_cPage].fb_id, 0, 0,
1057 ✗ &m_connectorId, 1, &m_mode);
1058 }
1059
1060 // Legacy SetCrtc scans the CRTC's primary plane out at the full mode size,
1061 // so it rejects (ENOSPC) a framebuffer smaller than the mode - and ours is
1062 // only the model times its pixel size, which is often short of the panel.
1063 // Without this, a CRTC that went off could never be turned back on: every
1064 // earlier "modeset" here failed and only worked because the console had
1065 // left the CRTC running. Light it with a blank mode-sized buffer instead,
1066 // put the picture on our (scaling) plane, then take the blank one away.
1067 11 DumbBuffer blank;
1068
4/4
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 6 times.
✓ Branch 3 taken 2 times.
11 bool useBlank = m_fb[m_cPage].width < m_mode.hdisplay || m_fb[m_cPage].height < m_mode.vdisplay;
1069
5/8
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 9 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 9 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 11 times.
11 if (useBlank && !CreateDumbBuffer(m_cardFd, m_mode.hdisplay, m_mode.vdisplay, DRM_FORMAT_RGB565, blank) &&
1070 ✗ !CreateDumbBuffer(m_cardFd, m_mode.hdisplay, m_mode.vdisplay, DRM_FORMAT_XRGB8888, blank)) {
1071 ✗ useBlank = false;
1072 }
1073
2/2
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 2 times.
11 if (useBlank) {
1074 9 memset(blank.mapped, 0, blank.size);
1075 }
1076
3/4
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 11 times.
✗ Branch 3 not taken.
11 int ret = drmModeSetCrtc(m_cardFd, m_crtcId, useBlank ? blank.fb_id : m_fb[m_cPage].fb_id, 0, 0,
1077 &m_connectorId, 1, &m_mode);
1078
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 11 times.
11 if (ret) {
1079 // Still place the plane: that is enough when the CRTC is already
1080 // running this mode, which is all the old code ever managed.
1081 ✗ LogWarn(VB_CHANNELOUT, "KMSFrameBuffer %s: modeset failed: %d (%s)\n", m_connectorName.c_str(), ret, strerror(errno));
1082 }
1083 22 ret = drmModeSetPlane(m_cardFd, m_planeId, m_crtcId, m_fb[m_cPage].fb_id, 0,
1084 11 0, 0, m_mode.hdisplay, m_mode.vdisplay,
1085
1/2
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
11 0, 0, (uint32_t)m_width << 16, (uint32_t)m_height << 16);
1086
2/2
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 2 times.
11 if (useBlank) {
1087 // Unless our plane is the primary (and has just replaced it), the blank
1088 // buffer is still on the primary plane. Take it off explicitly: removing
1089 // an in-use framebuffer can make the kernel turn the whole CRTC off.
1090
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 drmModePlaneResPtr planes = drmModeGetPlaneResources(m_cardFd);
1091
3/4
✓ Branch 0 taken 201 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 192 times.
✓ Branch 3 taken 9 times.
201 for (uint32_t i = 0; planes && i < planes->count_planes; i++) {
1092
1/2
✓ Branch 0 taken 192 times.
✗ Branch 1 not taken.
192 drmModePlanePtr plane = drmModeGetPlane(m_cardFd, planes->planes[i]);
1093
2/4
✓ Branch 0 taken 192 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 192 times.
192 if (plane && plane->fb_id == blank.fb_id) {
1094 ✗ drmModeSetPlane(m_cardFd, plane->plane_id, m_crtcId, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
1095 }
1096
1/2
✓ Branch 0 taken 192 times.
✗ Branch 1 not taken.
192 if (plane) {
1097
1/2
✓ Branch 0 taken 192 times.
✗ Branch 1 not taken.
192 drmModeFreePlane(plane);
1098 }
1099 }
1100
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 if (planes) {
1101
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 drmModeFreePlaneResources(planes);
1102 }
1103
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 DestroyDumbBuffer(m_cardFd, blank);
1104 }
1105 11 return ret;
1106 }
1107
1108 9 void KMSFrameBuffer::EnableDisplay() {
1109
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 LogDebug(VB_CHANNELOUT, "KMSFrameBuffer::EnableDisplay() called for %s\n", m_connectorName.c_str());
1110
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 std::unique_lock<std::mutex> lock(drmMasterLock);
1111
2/4
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 9 times.
9 if (MediaHoldsConnector()) {
1112 ✗ LogDebug(VB_CHANNELOUT, " Skipping: media is playing on this connector\n");
1113 ✗ return;
1114 }
1115 {
1116
2/4
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✗ Branch 3 not taken.
9 if (DrmCards::EnsureMaster(m_cardFd)) {
1117
2/4
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✗ Branch 3 not taken.
9 if (m_crtcId && m_planeId) {
1118
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 int ret = ModesetLocked();
1119
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (ret) {
1120 ✗ LogErr(VB_CHANNELOUT, "KMSFrameBuffer::EnableDisplay set_plane failed: %d\n", ret);
1121 } else {
1122 9 m_displayEnabled = true;
1123
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 LogInfo(VB_CHANNELOUT, "KMSFrameBuffer::EnableDisplay: SUCCESS for %s\n", m_connectorName.c_str());
1124 }
1125 }
1126 } else {
1127 ✗ LogWarn(VB_CHANNELOUT, "KMSFrameBuffer::EnableDisplay: Failed to get DRM master for %s\n", m_connectorName.c_str());
1128 }
1129 }
1130
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 }
1131
1132 // Minimum vertical blanking (scanlines) to keep between vactive and vtotal. The
1133 // RP1 DPI generates a low signal on the data pins during blanking, so this also
1134 // contributes to the WS281x reset gap; the caller (DPIPixelsOutput) additionally
1135 // pads vactive with blank scanlines so the reset is guaranteed regardless.
1136 static constexpr int KMS_MIN_VBLANK_LINES = 3;
1137
1138 13 void KMSFrameBuffer::ApplyVerticalTiming(drmModeModeInfo& mode, int vactive, int fps) {
1139
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 13 times.
13 if (mode.htotal == 0) {
1140 ✗ return;
1141 }
1142 13 int minVtotal = vactive + KMS_MIN_VBLANK_LINES;
1143 13 int vtotal = minVtotal;
1144
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 if (fps > 0) {
1145 // pixel clock is kHz; vtotal = clockHz / (htotal * fps)
1146 13 long long v = llround((double)mode.clock * 1000.0 / ((double)mode.htotal * (double)fps));
1147
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 13 times.
13 if (v > minVtotal) {
1148 ✗ vtotal = (int)v;
1149 }
1150 }
1151 13 mode.vdisplay = vactive;
1152 13 mode.vsync_start = vactive + 1;
1153 13 mode.vsync_end = vactive + 2;
1154 13 mode.vtotal = vtotal;
1155 13 mode.vrefresh = (uint32_t)llround((double)mode.clock * 1000.0 / ((double)mode.htotal * (double)vtotal));
1156 13 snprintf(mode.name, sizeof(mode.name), "%dx%d", mode.hdisplay, vactive);
1157 }
1158
1159 13 int KMSFrameBuffer::GetMaxRefreshRate() {
1160
2/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 13 times.
13 if (!m_variableRefresh || m_mode.htotal == 0) {
1161 ✗ return 0;
1162 }
1163 13 int minVtotal = m_mode.vdisplay + KMS_MIN_VBLANK_LINES;
1164 13 return (int)((double)m_mode.clock * 1000.0 / ((double)m_mode.htotal * (double)minVtotal));
1165 }
1166
1167 110 bool KMSFrameBuffer::SetRefreshRate(int fps) {
1168
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 (!m_variableRefresh || fps <= 0 || m_mode.htotal == 0) {
1169 ✗ return false;
1170 }
1171
1172 110 int minVtotal = m_mode.vdisplay + KMS_MIN_VBLANK_LINES;
1173 110 long long v = llround((double)m_mode.clock * 1000.0 / ((double)m_mode.htotal * (double)fps));
1174
1/2
✓ Branch 0 taken 110 times.
✗ Branch 1 not taken.
110 int newVtotal = (v > minVtotal) ? (int)v : minVtotal;
1175
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 110 times.
110 if (newVtotal == m_mode.vtotal) {
1176 ✗ return true; // already at this rate
1177 }
1178
1179 // Only the vertical blanking changes; horizontal timing and the pixel clock
1180 // (and therefore the WS bit timing and the framebuffer layout) are untouched.
1181 // Built in a copy: m_mode is what the CRTC is running, which the check
1182 // above and the flip timeouts depend on. Updating it before the modeset
1183 // happened made a refused attempt look applied - the next call returned
1184 // "already at this rate" and the new rate was never set.
1185 110 drmModeModeInfo mode = m_mode;
1186 110 mode.vsync_start = mode.vdisplay + 1;
1187 110 mode.vsync_end = mode.vdisplay + 2;
1188 110 mode.vtotal = newVtotal;
1189 110 mode.vrefresh = (uint32_t)llround((double)mode.clock * 1000.0 / ((double)mode.htotal * (double)newVtotal));
1190
1191
1/2
✓ Branch 0 taken 110 times.
✗ Branch 1 not taken.
110 std::unique_lock<std::mutex> lock(drmMasterLock);
1192
3/4
✓ Branch 0 taken 110 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 89 times.
✓ Branch 3 taken 21 times.
110 if (MediaHoldsConnector()) {
1193 // The connector is currently presenting media playback; don't fight it.
1194 // Report "not applied" so the caller's idea of the live rate stays
1195 // honest and it asks again next frame.
1196 89 return false;
1197 }
1198
2/4
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 21 times.
21 if (!DrmCards::EnsureMaster(m_cardFd)) {
1199 // Couldn't grab master right now. As above, not applied; the caller
1200 // asks again.
1201 ✗ return false;
1202 }
1203 // Drain any in-flight flip so the modeset starts from a known state.
1204
2/2
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 11 times.
21 if (m_flipPending) {
1205
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 int vr = m_mode.vrefresh > 0 ? m_mode.vrefresh : 60;
1206
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 WaitForPendingFlip((1000 / vr) + 20);
1207 }
1208
1/2
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
21 int ret = drmModeSetCrtc(m_cardFd, m_crtcId, m_fb[m_cPage].fb_id, 0, 0,
1209 &m_connectorId, 1, &mode);
1210
2/2
✓ Branch 0 taken 19 times.
✓ Branch 1 taken 2 times.
21 if (ret == 0) {
1211 19 m_mode = mode;
1212 19 m_displayEnabled = true;
1213
1/2
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
19 if (m_planeId) {
1214 19 drmModeSetPlane(m_cardFd, m_planeId, m_crtcId, m_fb[m_cPage].fb_id, 0,
1215 19 0, 0, m_mode.hdisplay, m_mode.vdisplay,
1216
1/2
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
19 0, 0, (uint32_t)m_width << 16, (uint32_t)m_height << 16);
1217 }
1218 // A mode change restarts the vblank stream; clear the cadence trackers so
1219 // the transition isn't miscounted as a stutter.
1220 19 m_flipPending = false;
1221 19 m_lastFlipSeq = 0;
1222 19 m_prevVblankDelta = 0;
1223
1/2
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
19 LogInfo(VB_CHANNELOUT, "KMSFrameBuffer %s: refresh set to %dHz (vtotal=%d)\n",
1224 m_connectorName.c_str(), m_mode.vrefresh, newVtotal);
1225 } else {
1226
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 LogErr(VB_CHANNELOUT, "KMSFrameBuffer::SetRefreshRate modeset failed: %d (%s)\n", ret, strerror(errno));
1227 }
1228 21 return ret == 0;
1229 110 }
1230
1231 // ---------------------------------------------------------------------------
1232 // Display hotplug
1233 // ---------------------------------------------------------------------------
1234
1235 // Quiet time after the last uevent before acting on a batch. A sink powering
1236 // up can bounce HPD a few times, and acting once on the settled state avoids a
1237 // modeset per bounce.
1238 static constexpr int HOTPLUG_SETTLE_MS = 1000;
1239 // How often an output without a display re-probes for one. Events cover
1240 // most of it, but vc4.force_hotplug (the ForceHDMI setting) makes the kernel
1241 // ignore HPD: a projector switched on later sends nothing, and without this
1242 // the output would run the stand-in mode, under a stale warning, for good.
1243 static constexpr int ABSENT_RECHECK_MS = 10000;
1244
1245 static std::mutex s_hotplugLock; // guards the three below
1246 static std::thread* s_hotplugThread = nullptr;
1247 static int s_hotplugSock = -1;
1248 static int s_hotplugWakeFd = -1;
1249
1250 23 void KMSFrameBuffer::StartHotplugMonitor() {
1251
1/2
✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
23 std::unique_lock<std::mutex> lock(s_hotplugLock);
1252
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 23 times.
23 if (s_hotplugThread) {
1253 ✗ return;
1254 }
1255 // The kernel's own uevent multicast group (1), not udevd's re-broadcast:
1256 // it needs no udevd and no libudev, and fppd runs as root.
1257 23 int sock = socket(AF_NETLINK, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, NETLINK_KOBJECT_UEVENT);
1258
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 23 times.
23 if (sock < 0) {
1259 ✗ LogWarn(VB_CHANNELOUT, "KMSFrameBuffer: no uevent socket, display hotplug will not be handled: %s\n", strerror(errno));
1260 ✗ return;
1261 }
1262 23 struct sockaddr_nl addr = {};
1263 23 addr.nl_family = AF_NETLINK;
1264 23 addr.nl_groups = 1;
1265
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 23 times.
23 if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
1266 ✗ LogWarn(VB_CHANNELOUT, "KMSFrameBuffer: could not bind the uevent socket, display hotplug will not be handled: %s\n", strerror(errno));
1267 ✗ close(sock);
1268 ✗ return;
1269 }
1270 23 int wakeFd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
1271
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 23 times.
23 if (wakeFd < 0) {
1272 ✗ close(sock);
1273 ✗ return;
1274 }
1275 23 s_hotplugSock = sock;
1276 23 s_hotplugWakeFd = wakeFd;
1277
3/8
✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 23 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 23 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
23 s_hotplugThread = new std::thread(HotplugMonitorLoop, sock, wakeFd);
1278
1/2
✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
23 }
1279
1280 19 void KMSFrameBuffer::StopHotplugMonitor() {
1281
1/2
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
19 std::unique_lock<std::mutex> lock(s_hotplugLock);
1282
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 19 times.
19 if (!s_hotplugThread) {
1283 ✗ return;
1284 }
1285 19 uint64_t one = 1;
1286
1/2
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
19 write(s_hotplugWakeFd, &one, sizeof(one));
1287
1/2
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
19 s_hotplugThread->join();
1288
1/2
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
19 delete s_hotplugThread;
1289 19 s_hotplugThread = nullptr;
1290
1/2
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
19 close(s_hotplugSock);
1291
1/2
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
19 close(s_hotplugWakeFd);
1292 19 s_hotplugSock = -1;
1293 19 s_hotplugWakeFd = -1;
1294
1/2
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
19 }
1295
1296 23 void KMSFrameBuffer::HotplugMonitorLoop(int sock, int wakeFd) {
1297
2/4
✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 23 times.
✗ Branch 3 not taken.
23 SetThreadName("FPP-DRMHotplug");
1298
1299 // card path ("/dev/dri/card1") -> connector ids named by the batch's
1300 // events; 0 for an event that named none (a sysfs status write, polled
1301 // connectors). Any event re-probes every output on its card.
1302 23 std::map<std::string, std::set<uint32_t>> pending;
1303 23 long long deadline = 0;
1304
1/2
✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
23 long long nextRecheck = GetMonotonicTimeMS() + ABSENT_RECHECK_MS;
1305 char buf[8192];
1306
1307 while (true) {
1308 32 struct pollfd pfds[2] = {};
1309 32 pfds[0].fd = sock;
1310 32 pfds[0].events = POLLIN;
1311 32 pfds[1].fd = wakeFd;
1312 32 pfds[1].events = POLLIN;
1313
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 32 times.
32 long long wake = deadline ? std::min(deadline, nextRecheck) : nextRecheck;
1314
1/2
✓ Branch 0 taken 32 times.
✗ Branch 1 not taken.
32 int timeout = (int)std::max(0LL, wake - GetMonotonicTimeMS());
1315
1/2
✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
32 int r = poll(pfds, 2, timeout);
1316
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 28 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
28 if (r < 0 && errno != EINTR) {
1317 ✗ LogWarn(VB_CHANNELOUT, "KMSFrameBuffer: uevent poll failed, display hotplug will no longer be handled: %s\n", strerror(errno));
1318 ✗ return;
1319 }
1320
2/2
✓ Branch 0 taken 19 times.
✓ Branch 1 taken 9 times.
28 if (pfds[1].revents & POLLIN) {
1321 19 return;
1322 }
1323
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 5 times.
9 if (pfds[0].revents & POLLIN) {
1324 ssize_t len;
1325
3/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 4 times.
8 while ((len = recv(sock, buf, sizeof(buf) - 1, 0)) > 0) {
1326 // "change@/devices/...\0ACTION=change\0SUBSYSTEM=drm\0HOTPLUG=1\0..."
1327 4 buf[len] = 0;
1328 4 std::string subsystem, devname;
1329 4 bool hotplug = false;
1330 4 uint32_t connectorId = 0;
1331
2/2
✓ Branch 0 taken 40 times.
✓ Branch 1 taken 4 times.
44 for (char* p = buf; p < buf + len; p += strlen(p) + 1) {
1332
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 36 times.
40 if (!strncmp(p, "SUBSYSTEM=", 10)) {
1333
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 subsystem = p + 10;
1334
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 32 times.
36 } else if (!strncmp(p, "DEVNAME=", 8)) {
1335
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 devname = p + 8;
1336
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 32 times.
32 } else if (!strcmp(p, "HOTPLUG=1")) {
1337 ✗ hotplug = true;
1338
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 32 times.
32 } else if (!strncmp(p, "CONNECTOR=", 10)) {
1339 ✗ connectorId = (uint32_t)strtoul(p + 10, nullptr, 10);
1340 }
1341 }
1342
4/10
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 4 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✓ Branch 8 taken 4 times.
✗ Branch 9 not taken.
4 if (subsystem != "drm" || !hotplug || devname.empty()) {
1343 4 continue;
1344 }
1345 ✗ LogDebug(VB_CHANNELOUT, "KMSFrameBuffer: hotplug uevent on %s connector %u\n", devname.c_str(), connectorId);
1346 ✗ pending["/dev/" + devname].insert(connectorId);
1347 ✗ deadline = GetMonotonicTimeMS() + HOTPLUG_SETTLE_MS;
1348
2/4
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 4 times.
8 }
1349 }
1350
2/8
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 9 times.
9 if (deadline && GetMonotonicTimeMS() >= deadline) {
1351 ✗ for (auto& [cardPath, connectorIds] : pending) {
1352 ✗ HandleHotplug(cardPath, connectorIds, false);
1353 }
1354 ✗ pending.clear();
1355 ✗ deadline = 0;
1356 }
1357
3/4
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✓ Branch 3 taken 4 times.
9 if (GetMonotonicTimeMS() >= nextRecheck) {
1358 5 std::set<std::string> cards;
1359 {
1360
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 std::unique_lock<std::mutex> lock(drmMasterLock);
1361
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 5 times.
10 for (auto& [token, fb] : FLIP_TARGETS) {
1362
3/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 3 times.
5 if (fb->m_hotplugReady && !fb->m_sinkPresent) {
1363
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 cards.insert(fb->m_cardInfo->path);
1364 }
1365 }
1366 5 }
1367
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 5 times.
7 for (auto& cardPath : cards) {
1368
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 HandleHotplug(cardPath, {}, true);
1369 }
1370
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 nextRecheck = GetMonotonicTimeMS() + ABSENT_RECHECK_MS;
1371 5 }
1372 9 }
1373 19 }
1374
1375 2 void KMSFrameBuffer::HandleHotplug(const std::string& cardPath, const std::set<uint32_t>& connectorIds, bool onlyAbsent) {
1376
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 std::unique_lock<std::mutex> lock(drmMasterLock);
1377 2 std::vector<std::pair<uint32_t, std::string>> outputs;
1378
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
4 for (auto& [token, fb] : FLIP_TARGETS) {
1379
5/10
✓ 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.
✓ Branch 6 taken 2 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2 times.
✗ Branch 9 not taken.
2 if (fb->m_hotplugReady && fb->m_cardInfo->path == cardPath && !(onlyAbsent && fb->m_sinkPresent)) {
1380
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 outputs.emplace_back(token, fb->m_connectorName);
1381 }
1382 }
1383
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
4 for (auto& [token, name] : outputs) {
1384
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 auto it = FLIP_TARGETS.find(token);
1385
3/6
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 2 times.
2 if (it == FLIP_TARGETS.end() || !it->second->m_hotplugReady) {
1386 2 continue;
1387 }
1388 2 KMSFrameBuffer* fb = it->second;
1389
1390 // A real probe (see ProbeConnectorLocked()), so it also refills the
1391 // sysfs mode list that was emptied while the display was away. Every
1392 // output's flips wait out this EDID read, once per hotplug.
1393
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 drmModeConnectorPtr conn = ProbeConnectorLocked(fb->m_cardFd, fb->m_connectorId);
1394
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 bool present = SinkPresent(fb->m_cardFd, conn);
1395 2 drmModeModeInfo mode = {};
1396
1/6
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
2 bool haveMode = present && PickMode(conn, mode);
1397
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (conn) {
1398
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 drmModeFreeConnector(conn);
1399 }
1400
1401 2 bool wasPresent = fb->m_sinkPresent;
1402
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 fb->SetSinkPresentLocked(present);
1403
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (!present) {
1404
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (wasPresent) {
1405 ✗ LogInfo(VB_CHANNELOUT, "KMSFrameBuffer %s: display disconnected\n", name.c_str());
1406 }
1407 2 continue;
1408 }
1409 // Back after we saw it gone. An event naming this connector while it
1410 // looks present also counts: the sink went away and came back before
1411 // the batch settled, or (with force_hotplug) its EDID changed. A
1412 // card-wide event alone does not: it may be about another connector,
1413 // and a needless modeset blanks this one (or glitches DPI pixels).
1414 ✗ if (wasPresent && !connectorIds.count(fb->m_connectorId)) {
1415 ✗ continue;
1416 }
1417 ✗ LogInfo(VB_CHANNELOUT, "KMSFrameBuffer %s: display %s\n", name.c_str(),
1418 wasPresent ? "re-announced itself" : "reconnected");
1419 ✗ if (fb->MediaHoldsConnector()) {
1420 ✗ LogInfo(VB_CHANNELOUT, "KMSFrameBuffer %s: media owns the connector, not restarting the display\n", name.c_str());
1421 ✗ continue;
1422 }
1423 // Run the display's own mode: the one we have may have been the stand-in
1424 // for a display that wasn't there yet, or this may be another display.
1425 // The framebuffer stays as it is; the plane scales it onto whatever mode
1426 // the CRTC runs. (DPI Pixels sets its own timing, but is never
1427 // external, so never gets here.)
1428 ✗ if (haveMode && !fb->m_variableRefresh && !SameTiming(mode, fb->m_mode)) {
1429 ✗ LogInfo(VB_CHANNELOUT, "KMSFrameBuffer %s: switching to the display's own mode %s (was %s)\n",
1430 name.c_str(), ModeLine(mode).c_str(), ModeLine(fb->m_mode).c_str());
1431 ✗ fb->m_mode = mode;
1432 }
1433 ✗ if (fb->m_displayEnabled) {
1434 ✗ fb->RestartDisplayLocked();
1435 }
1436 // Otherwise it isn't lit yet, and EnableDisplay() or SyncDisplay()
1437 // will light it in m_mode.
1438 }
1439 2 }
1440
1441 ✗ void KMSFrameBuffer::RestartDisplayLocked() {
1442 ✗ if (!DrmCards::EnsureMaster(m_cardFd)) {
1443 ✗ LogWarn(VB_CHANNELOUT, "KMSFrameBuffer %s: could not get DRM master to restart the display\n", m_connectorName.c_str());
1444 ✗ return;
1445 }
1446 ✗ if (m_flipPending) {
1447 ✗ int vr = m_mode.vrefresh > 0 ? m_mode.vrefresh : 60;
1448 ✗ WaitForPendingFlip((1000 / vr) + 20);
1449 }
1450 // Off first. SetCrtc with the mode the CRTC already runs is not a modeset
1451 // to the atomic helpers - it just swaps the framebuffer - and it is the
1452 // modeset that restarts the HDMI link the sink has to lock onto.
1453 ✗ if (m_planeId) {
1454 ✗ drmModeSetPlane(m_cardFd, m_planeId, m_crtcId, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
1455 }
1456 ✗ drmModeSetCrtc(m_cardFd, m_crtcId, 0, 0, 0, nullptr, 0, nullptr);
1457
1458 ✗ int ret = ModesetLocked();
1459 ✗ if (ret == 0) {
1460 ✗ LogInfo(VB_CHANNELOUT, "KMSFrameBuffer %s: display restarted at %dx%d @ %dHz\n",
1461 m_connectorName.c_str(), m_mode.hdisplay, m_mode.vdisplay, m_mode.vrefresh);
1462 } else {
1463 // Leave it disabled so the next SyncDisplay() tries the modeset again.
1464 ✗ LogWarn(VB_CHANNELOUT, "KMSFrameBuffer %s: display restart failed: %d (%s)\n",
1465 m_connectorName.c_str(), ret, strerror(errno));
1466 }
1467 ✗ m_displayEnabled = (ret == 0);
1468 // The modeset restarts the vblank stream; don't count it as a stutter.
1469 ✗ m_lastFlipSeq = 0;
1470 ✗ m_prevVblankDelta = 0;
1471 ✗ m_lastFlipTimeUS = 0;
1472 }
1473
1474 #endif
1475