Skip to content

Commit 2bcea4f

Browse files
committed
feat(linux/wlr): implement SHM capture fallback for headless setups
When DMA-BUF capture fails (e.g. GBM cannot allocate buffers on headless NVIDIA without an active DRM output), fall back to SHM shared memory capture via wl_shm. The SHM path creates a memfd-backed wl_shm_pool, receives pixel data from the compositor via wlr-screencopy, and feeds it to the encoder through the existing wlr_ram_t CPU path. Supported SHM formats: - 4 bpp (XRGB8888/ARGB8888): direct memcpy - 3 bpp (BGR888): pixel conversion to BGRA8888 Key changes: - Bind wl_shm interface in screencopy path - Add create_and_copy_shm() with memfd + mmap allocation - Refactor create_and_copy_dmabuf() to return bool for fallback - Cache GBM failure to avoid per-frame retry - Handle SHM frames in wlr_ram_t::snapshot() via memcpy - Make EGL init non-fatal (SHM path does not require EGL) - Force wlr_ram_t on reinit when SHM fallback is active Tested on headless NVIDIA RTX 5060 Ti with labwc compositor, NVENC HEVC encoding, streaming to Moonlight client.
1 parent ba4db46 commit 2bcea4f

3 files changed

Lines changed: 257 additions & 44 deletions

File tree

src/platform/linux/wayland.cpp

Lines changed: 162 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include <fcntl.h>
1111
#include <gbm.h>
1212
#include <poll.h>
13+
#include <sys/mman.h>
1314
#include <unistd.h>
1415
#include <wayland-client.h>
1516
#include <wayland-util.h>
@@ -213,6 +214,9 @@ namespace wl {
213214
dmabuf_interface = (zwp_linux_dmabuf_v1 *) wl_registry_bind(registry, id, &zwp_linux_dmabuf_v1_interface, version);
214215

215216
this->interface[LINUX_DMABUF] = true;
217+
} else if (!std::strcmp(interface, wl_shm_interface.name)) {
218+
BOOST_LOG(info) << "[wayland] Found interface: "sv << interface << '(' << id << ") version "sv << version;
219+
shm_interface = (wl_shm *) wl_registry_bind(registry, id, &wl_shm_interface, 1);
216220
}
217221
}
218222

@@ -273,6 +277,110 @@ namespace wl {
273277
}
274278
}
275279

280+
void dmabuf_t::cleanup_shm() {
281+
if (shm_wl_buffer) {
282+
wl_buffer_destroy(shm_wl_buffer);
283+
shm_wl_buffer = nullptr;
284+
}
285+
if (shm_pool) {
286+
wl_shm_pool_destroy(shm_pool);
287+
shm_pool = nullptr;
288+
}
289+
}
290+
291+
void dmabuf_t::destroy_shm() {
292+
cleanup_shm();
293+
if (shm_mmap && shm_mmap_size > 0) {
294+
munmap(shm_mmap, shm_mmap_size);
295+
shm_mmap = nullptr;
296+
shm_mmap_size = 0;
297+
}
298+
if (shm_fd >= 0) {
299+
close(shm_fd);
300+
shm_fd = -1;
301+
}
302+
}
303+
304+
bool dmabuf_t::init_shm(size_t size) {
305+
if (size == shm_mmap_size) {
306+
return true;
307+
}
308+
309+
destroy_shm();
310+
311+
shm_fd = memfd_create("sunshine-shm", 0);
312+
if (shm_fd < 0) {
313+
BOOST_LOG(error) << "[wayland] memfd_create failed: "sv << strerror(errno);
314+
return false;
315+
}
316+
317+
if (ftruncate(shm_fd, size) < 0) {
318+
BOOST_LOG(error) << "[wayland] ftruncate failed: "sv << strerror(errno);
319+
close(shm_fd);
320+
shm_fd = -1;
321+
return false;
322+
}
323+
324+
shm_mmap = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
325+
if (shm_mmap == MAP_FAILED) {
326+
BOOST_LOG(error) << "[wayland] mmap failed: "sv << strerror(errno);
327+
shm_mmap = nullptr;
328+
close(shm_fd);
329+
shm_fd = -1;
330+
return false;
331+
}
332+
333+
shm_mmap_size = size;
334+
return true;
335+
}
336+
337+
void dmabuf_t::create_and_copy_shm(zwlr_screencopy_frame_v1 *frame) {
338+
size_t needed = (size_t) shm_info.stride * shm_info.height;
339+
340+
if (!init_shm(needed)) {
341+
zwlr_screencopy_frame_v1_destroy(frame);
342+
status = REINIT;
343+
return;
344+
}
345+
346+
// Recreate pool+buffer each frame (compositor may require fresh buffer)
347+
cleanup_shm();
348+
349+
shm_pool = wl_shm_create_pool(shm_interface, shm_fd, shm_mmap_size);
350+
if (!shm_pool) {
351+
BOOST_LOG(error) << "[wayland] wl_shm_pool_create failed"sv;
352+
zwlr_screencopy_frame_v1_destroy(frame);
353+
status = REINIT;
354+
return;
355+
}
356+
357+
shm_wl_buffer = wl_shm_pool_create_buffer(
358+
shm_pool,
359+
0,
360+
shm_info.width,
361+
shm_info.height,
362+
shm_info.stride,
363+
shm_info.format
364+
);
365+
if (!shm_wl_buffer) {
366+
BOOST_LOG(error) << "[wayland] wl_shm_pool_create_buffer failed"sv;
367+
cleanup_shm();
368+
zwlr_screencopy_frame_v1_destroy(frame);
369+
status = REINIT;
370+
return;
371+
}
372+
373+
shm_mode = true;
374+
375+
BOOST_LOG(info) << "[wayland] SHM capture: "sv
376+
<< shm_info.width << "x"sv << shm_info.height
377+
<< " stride="sv << shm_info.stride
378+
<< " format=0x"sv << std::hex << shm_info.format << std::dec;
379+
380+
// Tell compositor to copy the frame into our SHM buffer
381+
zwlr_screencopy_frame_v1_copy(frame, shm_wl_buffer);
382+
}
383+
276384
dmabuf_t::dmabuf_t():
277385
status {READY},
278386
frames {},
@@ -292,10 +400,12 @@ namespace wl {
292400
void dmabuf_t::listen(
293401
zwlr_screencopy_manager_v1 *screencopy_manager,
294402
zwp_linux_dmabuf_v1 *dmabuf_interface,
403+
wl_shm *shm_interface,
295404
wl_output *output,
296405
bool blend_cursor
297406
) {
298407
this->dmabuf_interface = dmabuf_interface;
408+
this->shm_interface = shm_interface;
299409
// Reset state
300410
shm_info.supported = false;
301411
dmabuf_info.supported = false;
@@ -318,13 +428,13 @@ namespace wl {
318428

319429
dmabuf_t::~dmabuf_t() {
320430
cleanup_gbm();
431+
destroy_shm();
321432

322433
for (auto &frame : frames) {
323434
frame.destroy();
324435
}
325436

326437
if (gbm_device) {
327-
// We should close the DRM FD, but it's owned by GBM
328438
gbm_device_destroy(gbm_device);
329439
gbm_device = nullptr;
330440
}
@@ -364,74 +474,78 @@ namespace wl {
364474
BOOST_LOG(verbose) << "Frame flags: "sv << flags << (y_invert ? " (y_invert)" : "");
365475
}
366476

367-
// DMA-BUF creation helper
368-
void dmabuf_t::create_and_copy_dmabuf(zwlr_screencopy_frame_v1 *frame) {
477+
// DMA-BUF creation helper — returns false on GBM failure (caller can fall back to SHM)
478+
bool dmabuf_t::create_and_copy_dmabuf(zwlr_screencopy_frame_v1 *frame) {
369479
if (!init_gbm()) {
370480
BOOST_LOG(error) << "Failed to initialize GBM"sv;
371-
zwlr_screencopy_frame_v1_destroy(frame);
372-
status = REINIT;
373-
return;
481+
return false;
374482
}
375483

376-
// Create GBM buffer
377484
current_bo = gbm_bo_create(gbm_device, dmabuf_info.width, dmabuf_info.height, dmabuf_info.format, GBM_BO_USE_RENDERING | GBM_BO_USE_LINEAR);
378485
if (!current_bo) {
379486
BOOST_LOG(error) << "Failed to create GBM buffer"sv;
380-
zwlr_screencopy_frame_v1_destroy(frame);
381-
status = REINIT;
382-
return;
487+
return false;
383488
}
384489

385-
// Get buffer info
386490
int fd = gbm_bo_get_fd(current_bo);
387491
if (fd < 0) {
388492
BOOST_LOG(error) << "Failed to get buffer FD"sv;
389493
gbm_bo_destroy(current_bo);
390494
current_bo = nullptr;
391-
zwlr_screencopy_frame_v1_destroy(frame);
392-
status = REINIT;
393-
return;
495+
return false;
394496
}
395497

396498
uint32_t stride = gbm_bo_get_stride(current_bo);
397499
uint64_t modifier = gbm_bo_get_modifier(current_bo);
398500

399-
// Store in surface descriptor for later use
400501
auto next_frame = get_next_frame();
401502
next_frame->sd.fds[0] = fd;
402503
next_frame->sd.pitches[0] = stride;
403504
next_frame->sd.offsets[0] = 0;
404505
next_frame->sd.modifier = modifier;
405506

406-
// Create linux-dmabuf buffer
407507
auto params = zwp_linux_dmabuf_v1_create_params(dmabuf_interface);
408508
zwp_linux_buffer_params_v1_add(params, fd, 0, 0, stride, modifier >> 32, modifier & 0xffffffff);
409-
410-
// Add listener for buffer creation
411509
zwp_linux_buffer_params_v1_add_listener(params, &params_listener, frame);
412-
413-
// Create Wayland buffer (async - callback will handle copy)
414510
zwp_linux_buffer_params_v1_create(params, dmabuf_info.width, dmabuf_info.height, dmabuf_info.format, 0);
511+
512+
return true;
415513
}
416514

417515
// Buffer done callback - time to create buffer
418516
void dmabuf_t::buffer_done(zwlr_screencopy_frame_v1 *frame) {
419517
auto next_frame = get_next_frame();
518+
shm_mode = false;
420519

421-
// Prefer DMA-BUF if supported
422-
if (dmabuf_info.supported && dmabuf_interface) {
423-
// Store format info first
520+
// Prefer DMA-BUF if supported (skip if GBM already failed once)
521+
if (dmabuf_info.supported && dmabuf_interface && !gbm_failed) {
424522
next_frame->sd.fourcc = dmabuf_info.format;
425523
next_frame->sd.width = dmabuf_info.width;
426524
next_frame->sd.height = dmabuf_info.height;
427525

428-
// Create and start copy
429-
create_and_copy_dmabuf(frame);
430-
} else if (shm_info.supported) {
431-
// SHM fallback would go here
432-
BOOST_LOG(warning) << "[wayland] SHM capture not implemented"sv;
526+
if (create_and_copy_dmabuf(frame)) {
527+
return; // async path continues via buffer_params callbacks
528+
}
529+
530+
// DMA-BUF failed (e.g. GBM on headless NVIDIA) — remember and fall back
531+
gbm_failed = true;
532+
cleanup_gbm();
533+
if (shm_info.supported && shm_interface) {
534+
BOOST_LOG(warning) << "[wayland] DMA-BUF capture failed, falling back to SHM permanently"sv;
535+
create_and_copy_shm(frame);
536+
return;
537+
}
538+
539+
BOOST_LOG(error) << "[wayland] DMA-BUF failed and no SHM fallback available"sv;
433540
zwlr_screencopy_frame_v1_destroy(frame);
434541
status = REINIT;
542+
} else if (shm_info.supported && shm_interface) {
543+
static bool shm_logged = false;
544+
if (!shm_logged) {
545+
BOOST_LOG(info) << "[wayland] Using SHM capture (no DMA-BUF available)"sv;
546+
shm_logged = true;
547+
}
548+
create_and_copy_shm(frame);
435549
} else {
436550
BOOST_LOG(error) << "[wayland] No supported buffer types"sv;
437551
zwlr_screencopy_frame_v1_destroy(frame);
@@ -479,7 +593,6 @@ namespace wl {
479593
std::uint32_t tv_sec_lo,
480594
std::uint32_t tv_nsec
481595
) {
482-
// Frame is ready for use, GBM buffer now contains screen content
483596
current_frame->destroy();
484597
current_frame = get_next_frame();
485598

@@ -489,13 +602,26 @@ namespace wl {
489602
std::chrono::duration_cast<std::chrono::steady_clock::duration>(ready_ts)
490603
};
491604

492-
// Keep the GBM buffer alive but destroy the Wayland objects
493-
if (current_wl_buffer) {
494-
wl_buffer_destroy(current_wl_buffer);
495-
current_wl_buffer = nullptr;
496-
}
605+
if (shm_mode) {
606+
// SHM frame: populate dimensions for wlr_t::snapshot() check
607+
current_frame->sd.width = shm_info.width;
608+
current_frame->sd.height = shm_info.height;
609+
current_frame->shm_data = shm_mmap;
610+
current_frame->shm_stride = shm_info.stride;
611+
current_frame->is_shm = true;
497612

498-
cleanup_gbm();
613+
// Destroy Wayland objects, keep memfd+mmap for next frame
614+
cleanup_shm();
615+
} else {
616+
current_frame->is_shm = false;
617+
618+
// DMA-BUF path: destroy Wayland buffer, keep GBM bo alive
619+
if (current_wl_buffer) {
620+
wl_buffer_destroy(current_wl_buffer);
621+
current_wl_buffer = nullptr;
622+
}
623+
cleanup_gbm();
624+
}
499625

500626
zwlr_screencopy_frame_v1_destroy(frame);
501627
status = READY;
@@ -505,8 +631,8 @@ namespace wl {
505631
void dmabuf_t::failed(zwlr_screencopy_frame_v1 *frame) {
506632
BOOST_LOG(error) << "[wayland] Frame capture failed"sv;
507633

508-
// Clean up resources
509634
cleanup_gbm();
635+
cleanup_shm();
510636
auto next_frame = get_next_frame();
511637
next_frame->destroy();
512638

src/platform/linux/wayland.h

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ namespace wl {
3232

3333
egl::surface_descriptor_t sd;
3434
std::optional<std::chrono::steady_clock::time_point> frame_timestamp;
35+
void *shm_data = nullptr;
36+
uint32_t shm_stride = 0;
37+
bool is_shm = false;
3538
};
3639

3740
class dmabuf_t {
@@ -50,7 +53,7 @@ namespace wl {
5053
dmabuf_t &operator=(const dmabuf_t &) = delete;
5154
dmabuf_t &operator=(dmabuf_t &&) = delete;
5255

53-
void listen(zwlr_screencopy_manager_v1 *screencopy_manager, zwp_linux_dmabuf_v1 *dmabuf_interface, wl_output *output, bool blend_cursor = false);
56+
void listen(zwlr_screencopy_manager_v1 *screencopy_manager, zwp_linux_dmabuf_v1 *dmabuf_interface, wl_shm *shm_interface, wl_output *output, bool blend_cursor = false);
5457
static void buffer_params_created(void *data, struct zwp_linux_buffer_params_v1 *params, struct wl_buffer *wl_buffer);
5558
static void buffer_params_failed(void *data, struct zwp_linux_buffer_params_v1 *params);
5659
void buffer(zwlr_screencopy_frame_v1 *frame, std::uint32_t format, std::uint32_t width, std::uint32_t height, std::uint32_t stride);
@@ -74,9 +77,14 @@ namespace wl {
7477
private:
7578
bool init_gbm();
7679
void cleanup_gbm();
77-
void create_and_copy_dmabuf(zwlr_screencopy_frame_v1 *frame);
80+
bool create_and_copy_dmabuf(zwlr_screencopy_frame_v1 *frame);
81+
void create_and_copy_shm(zwlr_screencopy_frame_v1 *frame);
82+
bool init_shm(size_t size);
83+
void cleanup_shm();
84+
void destroy_shm();
7885

7986
zwp_linux_dmabuf_v1 *dmabuf_interface {nullptr};
87+
bool gbm_failed {false}; // sticky: skip DMA-BUF after first GBM failure
8088

8189
struct {
8290
bool supported {false};
@@ -96,6 +104,14 @@ namespace wl {
96104
struct gbm_device *gbm_device {nullptr};
97105
struct gbm_bo *current_bo {nullptr};
98106
struct wl_buffer *current_wl_buffer {nullptr};
107+
108+
wl_shm *shm_interface {nullptr};
109+
int shm_fd {-1};
110+
void *shm_mmap {nullptr};
111+
size_t shm_mmap_size {0};
112+
wl_shm_pool *shm_pool {nullptr};
113+
wl_buffer *shm_wl_buffer {nullptr};
114+
bool shm_mode {false};
99115
};
100116

101117
class monitor_t {
@@ -162,6 +178,7 @@ namespace wl {
162178
zwlr_screencopy_manager_v1 *screencopy_manager {nullptr};
163179
zwp_linux_dmabuf_v1 *dmabuf_interface {nullptr};
164180
zxdg_output_manager_v1 *output_manager {nullptr};
181+
wl_shm *shm_interface {nullptr};
165182

166183
private:
167184
void add_interface(wl_registry *registry, std::uint32_t id, const char *interface, std::uint32_t version);

0 commit comments

Comments
 (0)