ohmu
·
2026-10-15
1#include "../displayserver_linux.h"
2#include "common/io.h"
3#include "common/edidHelper.h"
4#include "common/strutil.h"
5
6#include <stdlib.h>
7#include <string.h>
8
9#ifdef FF_HAVE_WAYLAND
10
11 #include <sys/socket.h>
12
13 #include "common/properties.h"
14
15 #include "wayland.h"
16 #include "kde-output-device-v2-client-protocol.h"
17 #include "xdg-output-unstable-v1-client-protocol.h"
18 #include "wp-color-management-v1-client-protocol.h"
19
20 #if __FreeBSD__
21 #include <sys/un.h>
22 #include <sys/ucred.h>
23 #include <sys/sysctl.h>
24 #endif
25
26static bool waylandDetectWM(int fd, FFDisplayServerResult* result) {
27 #if __linux__ || __GNU__ || (__FreeBSD__ && !__DragonFly__)
28
29 #if __linux__ || __GNU__
30 struct ucred ucred = {};
31 socklen_t len = sizeof(ucred);
32 if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &ucred, &len) == -1 || ucred.pid <= 0) {
33 return false;
34 }
35
36 FF_STRBUF_AUTO_DESTROY procPath = ffStrbufCreate();
37 ffStrbufAppendF(&procPath, "/proc/%d/cmdline", ucred.pid); // We check the cmdline for the process name, because it is not trimmed.
38 if (!ffReadFileBuffer(procPath.chars, &result->wmProcessName)) {
39 return false;
40 }
41 #else
42 struct xucred ucred = {};
43 socklen_t len = sizeof(ucred);
44 if (getsockopt(fd, AF_UNSPEC, LOCAL_PEERCRED, &ucred, &len) == -1 || ucred.cr_pid <= 0) {
45 return false;
46 }
47
48 size_t size = 4096;
49 ffStrbufEnsureFixedLengthFree(&result->wmProcessName, (uint32_t) size);
50
51 if (sysctl((int[]) { CTL_KERN, KERN_PROC, KERN_PROC_ARGS, ucred.cr_pid }, 4, result->wmProcessName.chars, &size, nullptr, 0) != 0) {
52 return false;
53 }
54 result->wmProcessName.length = (uint32_t) size - 1;
55 #endif
56
57 // #1135: wl-restart is a special case
58 const char* filename = strrchr(result->wmProcessName.chars, '/');
59 if (filename) {
60 filename++;
61 } else {
62 filename = result->wmProcessName.chars;
63 }
64
65 if (ffStrEquals(filename, "wl-restart")) {
66 ffStrbufSubstrAfterLastC(&result->wmProcessName, '\0');
67 }
68
69 ffStrbufSubstrBeforeFirstC(&result->wmProcessName, '\0'); // Trim the arguments
70 ffStrbufSubstrAfterLastC(&result->wmProcessName, '/'); // Trim the path
71
72 return true;
73
74 #else
75 FF_UNUSED(fd, result);
76 return false;
77 #endif
78}
79
80static void waylandGlobalAddListener(void* data, struct wl_registry* registry, uint32_t name, const char* interface, uint32_t version) {
81 WaylandData* wldata = data;
82
83 if ((wldata->protocolType == FF_WAYLAND_PROTOCOL_TYPE_NONE || wldata->protocolType == FF_WAYLAND_PROTOCOL_TYPE_GLOBAL) && ffStrEquals(interface, wl_output_interface.name)) {
84 wldata->protocolType = FF_WAYLAND_PROTOCOL_TYPE_GLOBAL;
85 if (ffWaylandHandleGlobalOutput(wldata, registry, name, version) != nullptr) {
86 wldata->protocolType = FF_WAYLAND_PROTOCOL_TYPE_NONE;
87 }
88 } else if ((wldata->protocolType == FF_WAYLAND_PROTOCOL_TYPE_NONE) && ffStrEquals(interface, kde_output_device_registry_v2_interface.name)) {
89 wldata->protocolType = FF_WAYLAND_PROTOCOL_TYPE_KDE_REGISTRY;
90 if (ffWaylandHandleKdeOutputRegistry(wldata, registry, name, version) != nullptr) {
91 wldata->protocolType = FF_WAYLAND_PROTOCOL_TYPE_NONE;
92 }
93 } else if ((wldata->protocolType == FF_WAYLAND_PROTOCOL_TYPE_NONE || wldata->protocolType == FF_WAYLAND_PROTOCOL_TYPE_KDE_DEPRECATED) && ffStrEquals(interface, kde_output_device_v2_interface.name)) {
94 wldata->protocolType = FF_WAYLAND_PROTOCOL_TYPE_KDE_DEPRECATED;
95 if (ffWaylandHandleKdeOutput(wldata, registry, name, version) != nullptr) {
96 wldata->protocolType = FF_WAYLAND_PROTOCOL_TYPE_NONE;
97 }
98 } else if ((wldata->protocolType == FF_WAYLAND_PROTOCOL_TYPE_GLOBAL || wldata->protocolType == FF_WAYLAND_PROTOCOL_TYPE_NONE) && ffStrEquals(interface, zxdg_output_manager_v1_interface.name)) {
99 ffWaylandHandleZxdgOutput(wldata, registry, name, version);
100 } else if ((wldata->protocolType == FF_WAYLAND_PROTOCOL_TYPE_GLOBAL || wldata->protocolType == FF_WAYLAND_PROTOCOL_TYPE_NONE) && ffStrEquals(interface, wp_color_manager_v1_interface.name)) {
101 ffWaylandHandleWpColor(wldata, registry, name, version);
102 }
103}
104
105[[maybe_unused]] static bool matchDrmConnector(const char* connName, WaylandDisplay* wldata) {
106 // https://wayland.freedesktop.org/docs/html/apa.html#protocol-spec-wl_output-event-name
107 // The doc says that "do not assume that the name is a reflection of an underlying DRM connector, X11 connection, etc."
108 // However I can't find a better method to get the edid data
109 const char* drmDirPath = "/sys/class/drm/";
110
111 FF_AUTO_CLOSE_DIR DIR* dirp = opendir(drmDirPath);
112 if (dirp == nullptr) {
113 return false;
114 }
115
116 struct dirent* entry;
117 while ((entry = readdir(dirp)) != nullptr) {
118 const char* plainName = entry->d_name;
119 if (ffStrStartsWith(plainName, "card")) {
120 const char* tmp = strchr(plainName + strlen("card"), '-');
121 if (tmp) {
122 plainName = tmp + 1;
123 }
124 }
125 if (ffStrEquals(plainName, connName)) {
126 FF_STRBUF_AUTO_DESTROY path = ffStrbufCreateF("%s%s/edid", drmDirPath, entry->d_name);
127
128 uint8_t edidData[512];
129 ssize_t edidLength = ffReadFileData(path.chars, ARRAY_SIZE(edidData), edidData);
130 if (edidLength > 0 && ffEdidIsValid(edidData, (uint32_t) edidLength)) {
131 ffEdidGetName(edidData, &wldata->edidName);
132 wldata->hdrSupported = ffEdidGetHdrCompatible(edidData, (uint32_t) edidLength);
133 ffEdidGetManufactureDate((const uint8_t*) edidData, &wldata->myear, &wldata->mweek);
134 ffEdidGetSerial((const uint8_t*) edidData, &wldata->serial);
135 if (wldata->preferredWidth == 0) {
136 uint32_t preferredWidth = 0, preferredHeight = 0;
137 double preferredRefreshRate = 0;
138 ffEdidGetPreferredResolutionAndRefreshRate(edidData, &preferredWidth, &preferredHeight, &preferredRefreshRate);
139 wldata->preferredWidth = (int32_t) preferredWidth;
140 wldata->preferredHeight = (int32_t) preferredHeight;
141 wldata->preferredRefreshRate = (int32_t) (preferredRefreshRate * 1000.);
142 }
143 wldata->hdrInfoAvailable = true;
144 return true;
145 }
146 break;
147 }
148 }
149 return false;
150}
151
152void ffWaylandOutputNameListener(void* data, [[maybe_unused]] void* output, const char* name) {
153 WaylandDisplay* display = data;
154 if (display->id) {
155 return;
156 }
157
158 display->type = ffdsGetDisplayType(name);
159 #if __linux__
160 if (!display->edidName.length) {
161 matchDrmConnector(name, display);
162 }
163 #endif
164 display->id = ffWaylandGenerateIdFromName(name);
165 ffStrbufAppendS(&display->name, name);
166}
167
168void ffWaylandOutputDescriptionListener(void* data, [[maybe_unused]] void* output, const char* description) {
169 WaylandDisplay* display = data;
170 if (display->description.length) {
171 return;
172 }
173
174 while (*description == ' ') {
175 ++description;
176 }
177 if (!ffStrEquals(description, "Unknown Display") && !ffStrContains(description, "(null)")) {
178 ffStrbufAppendS(&display->description, description);
179 }
180}
181
182uint32_t ffWaylandHandleRotation(WaylandDisplay* display) {
183 uint32_t rotation;
184 switch (display->transform) {
185 case WL_OUTPUT_TRANSFORM_FLIPPED_90:
186 case WL_OUTPUT_TRANSFORM_90:
187 rotation = 90;
188 break;
189 case WL_OUTPUT_TRANSFORM_FLIPPED_180:
190 case WL_OUTPUT_TRANSFORM_180:
191 rotation = 180;
192 break;
193 case WL_OUTPUT_TRANSFORM_FLIPPED_270:
194 case WL_OUTPUT_TRANSFORM_270:
195 rotation = 270;
196 break;
197 default:
198 rotation = 0;
199 break;
200 }
201
202 switch (rotation) {
203 case 90:
204 case 270: {
205 int32_t temp = display->width;
206 display->width = display->height;
207 display->height = temp;
208
209 temp = display->physicalWidth;
210 display->physicalWidth = display->physicalHeight;
211 display->physicalHeight = temp;
212 break;
213 }
214 default:
215 break;
216 }
217 return rotation;
218}
219
220const char* ffWaylandWaitForDone(WaylandDisplay* display) {
221 int32_t timeout = instance.config.general.processingTimeout;
222 struct timespec ts;
223 if (timeout == 0) {
224 return "timeout is disabled";
225 } else if (timeout > 0) {
226 if (!display->parent->ffwl_display_dispatch_timeout) {
227 return "timeout is enabled, but the libwayland-client version is too old to support it";
228 }
229 ts.tv_sec = timeout / 1000;
230 ts.tv_nsec = (timeout % 1000) * 1000000;
231 }
232
233 WaylandData* wldata = display->parent;
234
235 // Some compositors emit the final output state asynchronously after the roundtrip callback (#2074)
236 while (!display->done) {
237 if (timeout > 0) {
238 if (wldata->ffwl_display_dispatch_timeout(wldata->display, &ts) < 0) {
239 return "wl_display_dispatch_timeout() failed";
240 }
241 } else if (timeout < 0) {
242 if (wldata->ffwl_display_dispatch(wldata->display) < 0) {
243 return "wl_display_dispatch() failed";
244 }
245 } else {
246 return "Timeout exceeded, but the compositor did not send a done event";
247 }
248 }
249
250 return nullptr;
251}
252
253const char* ffdsConnectWayland(FFDisplayServerResult* result) {
254 if (getenv("XDG_RUNTIME_DIR") == nullptr) {
255 return "Wayland requires $XDG_RUNTIME_DIR being set";
256 }
257
258 FF_LIBRARY_LOAD_MESSAGE(wayland, "libwayland-client" FF_LIBRARY_EXTENSION, 1)
259
260 FF_LIBRARY_LOAD_SYMBOL_MESSAGE(wayland, wl_display_connect)
261 FF_LIBRARY_LOAD_SYMBOL_MESSAGE(wayland, wl_display_get_fd)
262 FF_LIBRARY_LOAD_SYMBOL_MESSAGE(wayland, wl_proxy_marshal_constructor)
263 FF_LIBRARY_LOAD_SYMBOL_MESSAGE(wayland, wl_display_disconnect)
264
265 WaylandData data = {};
266
267 FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(wayland, data, wl_proxy_marshal_constructor_versioned)
268 FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(wayland, data, wl_proxy_add_listener)
269 FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(wayland, data, wl_proxy_destroy)
270 FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(wayland, data, wl_display_roundtrip)
271
272 if (instance.config.general.processingTimeout < 0) {
273 FF_LIBRARY_LOAD_SYMBOL_VAR_LAZY(wayland, data, wl_display_dispatch)
274 } else if (instance.config.general.processingTimeout > 0) {
275 FF_LIBRARY_LOAD_SYMBOL_VAR_LAZY(wayland, data, wl_display_dispatch_timeout)
276 }
277
278 data.display = ffwl_display_connect(nullptr);
279 if (data.display == nullptr) {
280 return "wl_display_connect returned nullptr";
281 }
282
283 waylandDetectWM(ffwl_display_get_fd(data.display), result);
284
285 struct wl_proxy* registry = ffwl_proxy_marshal_constructor((struct wl_proxy*) data.display, WL_DISPLAY_GET_REGISTRY, &wl_registry_interface, nullptr);
286 if (registry == nullptr) {
287 ffwl_display_disconnect(data.display);
288 return "wl_display_get_registry returned nullptr";
289 }
290
291 data.result = result;
292
293 struct wl_registry_listener registry_listener = {
294 .global = waylandGlobalAddListener,
295 .global_remove = (void*) ffUnused
296 };
297
298 data.ffwl_proxy_add_listener(registry, (void (**)(void)) ®istry_listener, &data);
299 data.ffwl_display_roundtrip(data.display);
300
301 if (data.zxdgOutputManager) {
302 data.ffwl_proxy_destroy(data.zxdgOutputManager);
303 }
304
305 if (data.wpColorManager) {
306 data.ffwl_proxy_destroy(data.wpColorManager);
307 }
308
309 data.ffwl_proxy_destroy(registry);
310 ffwl_display_disconnect(data.display);
311
312 {
313 const char* fileName = ffStrbufEqualS(&result->wmProcessName, "gnome-shell")
314 ? "monitors.xml"
315 : ffStrbufEqualS(&result->wmProcessName, "cinnamon")
316 ? "cinnamon-monitors.xml"
317 : nullptr;
318 if (fileName) {
319 FF_STRBUF_AUTO_DESTROY monitorsXml = ffStrbufCreate();
320 FF_LIST_FOR_EACH (FFstrbuf, basePath, instance.state.platform.configDirs) {
321 char path[1024];
322 snprintf(path, ARRAY_SIZE(path), "%s%s", basePath->chars, fileName);
323 if (ffReadFileBuffer(path, &monitorsXml)) {
324 break;
325 }
326 }
327 if (monitorsXml.length) {
328 // <monitors version="2">
329 // <configuration>
330 // <logicalmonitor>
331 // <x>0</x>
332 // <y>0</y>
333 // <scale>1.7489879131317139</scale>
334 // <primary>yes</primary>
335 // <monitor>
336 // <monitorspec>
337 // <connector>Virtual-1</connector>
338 // <vendor>unknown</vendor>
339 // <product>unknown</product>
340 // <serial>unknown</serial>
341 // </monitorspec>
342 // <mode>
343 // <width>3456</width>
344 // <height>2160</height>
345 // <rate>60.000068664550781</rate>
346 // </mode>
347 // </monitor>
348 // </logicalmonitor>
349 // </configuration>
350 // </monitors>
351 uint32_t start = ffStrbufFirstIndexS(&monitorsXml, "<primary>yes</primary>");
352 if (start < monitorsXml.length) {
353 start = ffStrbufNextIndexS(&monitorsXml, start, "<connector>");
354 if (start < monitorsXml.length) {
355 uint32_t end = ffStrbufNextIndexS(&monitorsXml, start, "</connector>");
356 if (end < monitorsXml.length) {
357 ffStrbufSubstrBefore(&monitorsXml, end);
358 const char* name = monitorsXml.chars + start + strlen("<connector>");
359 uint64_t primaryDisplayId = ffWaylandGenerateIdFromName(name);
360
361 FF_LIST_FOR_EACH (FFDisplayResult, d, data.result->displays) {
362 if (d->id == primaryDisplayId) {
363 d->primary = true;
364 break;
365 }
366 }
367 }
368 }
369 }
370 }
371 }
372 }
373
374 // We successfully connected to wayland and detected the display.
375 // So we can set set the session type to wayland.
376 // This is used as an indicator that we are running wayland by the x11 backends.
377 ffStrbufSetStatic(&result->wmProtocolName, FF_WM_PROTOCOL_WAYLAND);
378 return nullptr;
379}
380
381#else
382
383const char* ffdsConnectWayland([[maybe_unused]] FFDisplayServerResult* result) {
384 return "Fastfetch was compiled without Wayland support";
385}
386
387#endif