Skip to content

Commit 0262c5d

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

3 files changed

Lines changed: 252 additions & 44 deletions

File tree

src/platform/linux/wayland.cpp

Lines changed: 158 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,106 @@ 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+
void dmabuf_t::create_and_copy_shm(zwlr_screencopy_frame_v1 *frame) {
305+
size_t needed = (size_t) shm_info.stride * shm_info.height;
306+
307+
// (Re)allocate memfd+mmap if size changed
308+
if (needed != shm_mmap_size) {
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+
zwlr_screencopy_frame_v1_destroy(frame);
315+
status = REINIT;
316+
return;
317+
}
318+
319+
if (ftruncate(shm_fd, needed) < 0) {
320+
BOOST_LOG(error) << "[wayland] ftruncate failed: "sv << strerror(errno);
321+
close(shm_fd);
322+
shm_fd = -1;
323+
zwlr_screencopy_frame_v1_destroy(frame);
324+
status = REINIT;
325+
return;
326+
}
327+
328+
shm_mmap = mmap(nullptr, needed, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
329+
if (shm_mmap == MAP_FAILED) {
330+
BOOST_LOG(error) << "[wayland] mmap failed: "sv << strerror(errno);
331+
shm_mmap = nullptr;
332+
close(shm_fd);
333+
shm_fd = -1;
334+
zwlr_screencopy_frame_v1_destroy(frame);
335+
status = REINIT;
336+
return;
337+
}
338+
339+
shm_mmap_size = needed;
340+
}
341+
342+
// Recreate pool+buffer each frame (compositor may require fresh buffer)
343+
cleanup_shm();
344+
345+
shm_pool = wl_shm_create_pool(shm_interface, shm_fd, shm_mmap_size);
346+
if (!shm_pool) {
347+
BOOST_LOG(error) << "[wayland] wl_shm_pool_create failed"sv;
348+
zwlr_screencopy_frame_v1_destroy(frame);
349+
status = REINIT;
350+
return;
351+
}
352+
353+
shm_wl_buffer = wl_shm_pool_create_buffer(
354+
shm_pool,
355+
0,
356+
shm_info.width,
357+
shm_info.height,
358+
shm_info.stride,
359+
shm_info.format
360+
);
361+
if (!shm_wl_buffer) {
362+
BOOST_LOG(error) << "[wayland] wl_shm_pool_create_buffer failed"sv;
363+
cleanup_shm();
364+
zwlr_screencopy_frame_v1_destroy(frame);
365+
status = REINIT;
366+
return;
367+
}
368+
369+
shm_mode = true;
370+
371+
BOOST_LOG(info) << "[wayland] SHM capture: "sv
372+
<< shm_info.width << "x"sv << shm_info.height
373+
<< " stride="sv << shm_info.stride
374+
<< " format=0x"sv << std::hex << shm_info.format << std::dec;
375+
376+
// Tell compositor to copy the frame into our SHM buffer
377+
zwlr_screencopy_frame_v1_copy(frame, shm_wl_buffer);
378+
}
379+
276380
dmabuf_t::dmabuf_t():
277381
status {READY},
278382
frames {},
@@ -292,10 +396,12 @@ namespace wl {
292396
void dmabuf_t::listen(
293397
zwlr_screencopy_manager_v1 *screencopy_manager,
294398
zwp_linux_dmabuf_v1 *dmabuf_interface,
399+
wl_shm *shm_interface,
295400
wl_output *output,
296401
bool blend_cursor
297402
) {
298403
this->dmabuf_interface = dmabuf_interface;
404+
this->shm_interface = shm_interface;
299405
// Reset state
300406
shm_info.supported = false;
301407
dmabuf_info.supported = false;
@@ -318,13 +424,13 @@ namespace wl {
318424

319425
dmabuf_t::~dmabuf_t() {
320426
cleanup_gbm();
427+
destroy_shm();
321428

322429
for (auto &frame : frames) {
323430
frame.destroy();
324431
}
325432

326433
if (gbm_device) {
327-
// We should close the DRM FD, but it's owned by GBM
328434
gbm_device_destroy(gbm_device);
329435
gbm_device = nullptr;
330436
}
@@ -364,74 +470,78 @@ namespace wl {
364470
BOOST_LOG(verbose) << "Frame flags: "sv << flags << (y_invert ? " (y_invert)" : "");
365471
}
366472

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

376-
// Create GBM buffer
377480
current_bo = gbm_bo_create(gbm_device, dmabuf_info.width, dmabuf_info.height, dmabuf_info.format, GBM_BO_USE_RENDERING | GBM_BO_USE_LINEAR);
378481
if (!current_bo) {
379482
BOOST_LOG(error) << "Failed to create GBM buffer"sv;
380-
zwlr_screencopy_frame_v1_destroy(frame);
381-
status = REINIT;
382-
return;
483+
return false;
383484
}
384485

385-
// Get buffer info
386486
int fd = gbm_bo_get_fd(current_bo);
387487
if (fd < 0) {
388488
BOOST_LOG(error) << "Failed to get buffer FD"sv;
389489
gbm_bo_destroy(current_bo);
390490
current_bo = nullptr;
391-
zwlr_screencopy_frame_v1_destroy(frame);
392-
status = REINIT;
393-
return;
491+
return false;
394492
}
395493

396494
uint32_t stride = gbm_bo_get_stride(current_bo);
397495
uint64_t modifier = gbm_bo_get_modifier(current_bo);
398496

399-
// Store in surface descriptor for later use
400497
auto next_frame = get_next_frame();
401498
next_frame->sd.fds[0] = fd;
402499
next_frame->sd.pitches[0] = stride;
403500
next_frame->sd.offsets[0] = 0;
404501
next_frame->sd.modifier = modifier;
405502

406-
// Create linux-dmabuf buffer
407503
auto params = zwp_linux_dmabuf_v1_create_params(dmabuf_interface);
408504
zwp_linux_buffer_params_v1_add(params, fd, 0, 0, stride, modifier >> 32, modifier & 0xffffffff);
409-
410-
// Add listener for buffer creation
411505
zwp_linux_buffer_params_v1_add_listener(params, &params_listener, frame);
412-
413-
// Create Wayland buffer (async - callback will handle copy)
414506
zwp_linux_buffer_params_v1_create(params, dmabuf_info.width, dmabuf_info.height, dmabuf_info.format, 0);
507+
508+
return true;
415509
}
416510

417511
// Buffer done callback - time to create buffer
418512
void dmabuf_t::buffer_done(zwlr_screencopy_frame_v1 *frame) {
419513
auto next_frame = get_next_frame();
514+
shm_mode = false;
420515

421-
// Prefer DMA-BUF if supported
422-
if (dmabuf_info.supported && dmabuf_interface) {
423-
// Store format info first
516+
// Prefer DMA-BUF if supported (skip if GBM already failed once)
517+
if (dmabuf_info.supported && dmabuf_interface && !gbm_failed) {
424518
next_frame->sd.fourcc = dmabuf_info.format;
425519
next_frame->sd.width = dmabuf_info.width;
426520
next_frame->sd.height = dmabuf_info.height;
427521

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;
522+
if (create_and_copy_dmabuf(frame)) {
523+
return; // async path continues via buffer_params callbacks
524+
}
525+
526+
// DMA-BUF failed (e.g. GBM on headless NVIDIA) — remember and fall back
527+
gbm_failed = true;
528+
cleanup_gbm();
529+
if (shm_info.supported && shm_interface) {
530+
BOOST_LOG(warning) << "[wayland] DMA-BUF capture failed, falling back to SHM permanently"sv;
531+
create_and_copy_shm(frame);
532+
return;
533+
}
534+
535+
BOOST_LOG(error) << "[wayland] DMA-BUF failed and no SHM fallback available"sv;
433536
zwlr_screencopy_frame_v1_destroy(frame);
434537
status = REINIT;
538+
} else if (shm_info.supported && shm_interface) {
539+
static bool shm_logged = false;
540+
if (!shm_logged) {
541+
BOOST_LOG(info) << "[wayland] Using SHM capture (no DMA-BUF available)"sv;
542+
shm_logged = true;
543+
}
544+
create_and_copy_shm(frame);
435545
} else {
436546
BOOST_LOG(error) << "[wayland] No supported buffer types"sv;
437547
zwlr_screencopy_frame_v1_destroy(frame);
@@ -479,7 +589,6 @@ namespace wl {
479589
std::uint32_t tv_sec_lo,
480590
std::uint32_t tv_nsec
481591
) {
482-
// Frame is ready for use, GBM buffer now contains screen content
483592
current_frame->destroy();
484593
current_frame = get_next_frame();
485594

@@ -489,13 +598,26 @@ namespace wl {
489598
std::chrono::duration_cast<std::chrono::steady_clock::duration>(ready_ts)
490599
};
491600

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-
}
601+
if (shm_mode) {
602+
// SHM frame: populate dimensions for wlr_t::snapshot() check
603+
current_frame->sd.width = shm_info.width;
604+
current_frame->sd.height = shm_info.height;
605+
current_frame->shm_data = shm_mmap;
606+
current_frame->shm_stride = shm_info.stride;
607+
current_frame->is_shm = true;
497608

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

500622
zwlr_screencopy_frame_v1_destroy(frame);
501623
status = READY;
@@ -505,8 +627,8 @@ namespace wl {
505627
void dmabuf_t::failed(zwlr_screencopy_frame_v1 *frame) {
506628
BOOST_LOG(error) << "[wayland] Frame capture failed"sv;
507629

508-
// Clean up resources
509630
cleanup_gbm();
631+
cleanup_shm();
510632
auto next_frame = get_next_frame();
511633
next_frame->destroy();
512634

src/platform/linux/wayland.h

Lines changed: 18 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,13 @@ 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+
void cleanup_shm();
83+
void destroy_shm();
7884

7985
zwp_linux_dmabuf_v1 *dmabuf_interface {nullptr};
86+
bool gbm_failed {false}; // sticky: skip DMA-BUF after first GBM failure
8087

8188
struct {
8289
bool supported {false};
@@ -96,6 +103,14 @@ namespace wl {
96103
struct gbm_device *gbm_device {nullptr};
97104
struct gbm_bo *current_bo {nullptr};
98105
struct wl_buffer *current_wl_buffer {nullptr};
106+
107+
wl_shm *shm_interface {nullptr};
108+
int shm_fd {-1};
109+
void *shm_mmap {nullptr};
110+
size_t shm_mmap_size {0};
111+
wl_shm_pool *shm_pool {nullptr};
112+
wl_buffer *shm_wl_buffer {nullptr};
113+
bool shm_mode {false};
99114
};
100115

101116
class monitor_t {
@@ -162,6 +177,7 @@ namespace wl {
162177
zwlr_screencopy_manager_v1 *screencopy_manager {nullptr};
163178
zwp_linux_dmabuf_v1 *dmabuf_interface {nullptr};
164179
zxdg_output_manager_v1 *output_manager {nullptr};
180+
wl_shm *shm_interface {nullptr};
165181

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

0 commit comments

Comments
 (0)